Add load testing setup for Elemes LMS using Locust
- Create .gitignore to exclude virtual environments and generated reports - Add README.md with detailed instructions for load testing - Implement locustfile.py to simulate user behavior for students and teachers - Define user classes: FrontendUser, StudentUser, TeacherUser, and APIStressTest - Include requirements.txt for Locust and Faker dependencies - Develop run.sh script for easy execution of load tests with virtual environment setup
This commit is contained in:
parent
5d91087426
commit
885980f276
|
|
@ -0,0 +1,19 @@
|
|||
# Virtual environment
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
|
||||
# Locust generated reports
|
||||
*.html
|
||||
results_*.csv
|
||||
report.html
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,454 @@
|
|||
# Load Test untuk Elemes LMS
|
||||
|
||||
Load test ini menggunakan **Locust** untuk mensimulasikan traffic pengguna pada LMS Elemes (Sinau-C).
|
||||
|
||||
## ⚠️ Penting: Keamanan Data Siswa
|
||||
|
||||
Load test ini menggunakan **token siswa test** yang ditunjuk khusus untuk testing:
|
||||
|
||||
**Student Test Tokens (rows 40-72 di `tokens_siswa.csv`):**
|
||||
- CC90965E (ACHMAD RAFFY IRSYAD RAMADHAN)
|
||||
- F84E3336 (AHMAD GHAUZAN AZRIL IRAWAN)
|
||||
- D78F6DBD (AINI BINTANG PRAMESWARI)
|
||||
- ... dan 30 siswa lainnya sampai SATRIYO WICAKSONO
|
||||
|
||||
**Teacher Token:**
|
||||
- 141214 (Anggoro Dwi - untuk view-only operations)
|
||||
|
||||
Token-token ini aman untuk testing karena:
|
||||
1. ✅ Bukan siswa aktif yang sedang belajar
|
||||
2. ✅ Progress mereka bisa di-reset dengan mudah
|
||||
3. ✅ Teacher token hanya untuk read-only operations
|
||||
4. ✅ Semua write operations (track-progress) menggunakan test student tokens
|
||||
|
||||
## Struktur File
|
||||
|
||||
```
|
||||
test-load/
|
||||
├── locustfile.py # Script load test utama
|
||||
├── requirements.txt # Dependencies Python
|
||||
└── README.md # Dokumentasi ini
|
||||
```
|
||||
|
||||
## Instalasi
|
||||
|
||||
### 1. Buat Virtual Environment (Opsional tapi Direkomendasikan)
|
||||
|
||||
```bash
|
||||
cd test-load
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# atau
|
||||
venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Cara Menjalankan
|
||||
|
||||
### 🚀 Quick Start (Menggunakan Script Helper)
|
||||
|
||||
Cara termudah untuk menjalankan load test adalah menggunakan script `run.sh` yang sudah menyertakan setup venv otomatis:
|
||||
|
||||
```bash
|
||||
cd test-load
|
||||
|
||||
# Test API (Flask backend :5000)
|
||||
./run.sh
|
||||
|
||||
# Test Frontend (SvelteKit :3000) - HTML pages
|
||||
./run.sh frontend
|
||||
|
||||
# Test Full Stack (Frontend + API via proxy)
|
||||
./run.sh fullstack
|
||||
|
||||
# Jalankan headless mode (default: 50 users, 5 spawn rate, 60 detik)
|
||||
./run.sh headless
|
||||
|
||||
# Custom parameters
|
||||
./run.sh headless -u 100 -r 10 -t 5m
|
||||
./run.sh headless frontend -u 200 -r 20 -t 300s
|
||||
|
||||
# Hapus virtual environment
|
||||
./run.sh clean
|
||||
```
|
||||
|
||||
### Mode Web UI (Interaktif)
|
||||
|
||||
```bash
|
||||
# Menggunakan script helper (recommended)
|
||||
|
||||
# Test API only (Flask backend :5000)
|
||||
./run.sh
|
||||
|
||||
# Test Frontend only (SvelteKit :3000) - loads HTML pages
|
||||
./run.sh frontend
|
||||
|
||||
# Test Full Stack (Frontend proxies /api to Flask)
|
||||
./run.sh fullstack
|
||||
|
||||
# Atau manual dengan venv
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# API testing
|
||||
locust -f locustfile.py --host=http://localhost:5000
|
||||
|
||||
# Frontend testing
|
||||
locust -f locustfile.py --host=http://localhost:3000
|
||||
```
|
||||
|
||||
Kemudian buka browser di: **http://localhost:8089**
|
||||
|
||||
Di Web UI, Anda bisa:
|
||||
- Set jumlah user (Total Users)
|
||||
- Set spawn rate (user per detik)
|
||||
- Start, Stop, dan Edit load test secara real-time
|
||||
- Melihat grafik response time, requests/detik, failures
|
||||
|
||||
### Mode Headless (CLI/Scripted)
|
||||
|
||||
```bash
|
||||
# Menggunakan script helper
|
||||
|
||||
# Test API
|
||||
./run.sh headless -u 100 -r 10 -t 300s
|
||||
|
||||
# Test Frontend (HTML pages)
|
||||
./run.sh headless frontend -u 100 -r 10 -t 5m
|
||||
|
||||
# Test Full Stack
|
||||
./run.sh headless fullstack -u 200 -r 20 -t 10m
|
||||
|
||||
# Atau manual
|
||||
source venv/bin/activate
|
||||
|
||||
# API testing (Flask :5000)
|
||||
locust -f locustfile.py --host=http://localhost:5000 --headless -u 100 -r 10 -t 300s
|
||||
|
||||
# Frontend testing (SvelteKit :3000)
|
||||
locust -f locustfile.py --host=http://localhost:3000 --headless -u 100 -r 10 -t 300s
|
||||
```
|
||||
|
||||
### Mode Distributed (Multi-Machine)
|
||||
|
||||
Untuk load test skala besar:
|
||||
|
||||
```bash
|
||||
# Master node
|
||||
locust -f locustfile.py --headless -u 1000 -r 100 --master
|
||||
|
||||
# Worker node 1
|
||||
locust -f locustfile.py --worker --master-host=192.168.1.100
|
||||
|
||||
# Worker node 2
|
||||
locust -f locustfile.py --worker --master-host=192.168.1.100
|
||||
```
|
||||
|
||||
## Parameter CLI
|
||||
|
||||
| Parameter | Deskripsi | Contoh |
|
||||
|-----------|-----------|--------|
|
||||
| `-u` | Total jumlah user simulasi | `-u 100` |
|
||||
| `-r` | Spawn rate (user per detik) | `-r 10` |
|
||||
| `-t` | Durasi test | `-t 300s`, `-t 5m`, `-t 1h` |
|
||||
| `--host` | Target URL | `--host=http://localhost:5000` |
|
||||
| `--headless` | Jalankan tanpa UI | `--headless` |
|
||||
| `--html` | Export hasil ke HTML | `--html=report.html` |
|
||||
|
||||
## User Classes
|
||||
|
||||
Load test ini memiliki 4 kelas user:
|
||||
|
||||
### 1. FrontendUser (Weight: 5)
|
||||
Mensimulasikan user yang browsing halaman web frontend:
|
||||
- Load homepage (/)
|
||||
- Load lesson pages (/lesson/[slug])
|
||||
- Load progress page (/progress)
|
||||
- Load static assets (CSS, JS, manifest)
|
||||
|
||||
**Weight 5** = Testing beban pada SvelteKit frontend
|
||||
|
||||
### 2. StudentUser (Weight: 8)
|
||||
Mensimulasikan siswa yang berinteraksi dengan API:
|
||||
- Login dengan token
|
||||
- Melihat daftar pelajaran (API)
|
||||
- Membuka konten lesson (API)
|
||||
- Compile kode C
|
||||
- Validate token
|
||||
- Logout
|
||||
|
||||
**Weight 8** = 40% traffic adalah siswa (API)
|
||||
|
||||
### 3. TeacherUser (Weight: 2)
|
||||
Mensimulasikan guru yang berinteraksi dengan API:
|
||||
- Login dengan token guru
|
||||
- Melihat progress report (API)
|
||||
- Export CSV progress
|
||||
- Membuka lesson untuk review
|
||||
- Validate token
|
||||
|
||||
**Weight 2** = 10% traffic adalah guru (API)
|
||||
|
||||
### 4. APIStressTest (Weight: 1)
|
||||
Stress test endpoint API spesifik:
|
||||
- `/api/compile` - Code compilation
|
||||
- `/api/lesson/[slug].json` - Lesson content
|
||||
- `/api/validate-token` - Token validation
|
||||
- `/api/lessons` - Lessons list
|
||||
|
||||
**Weight 1** = 5% traffic adalah stress test API
|
||||
|
||||
## Endpoint yang Diuji
|
||||
|
||||
### Frontend (SvelteKit :3000)
|
||||
|
||||
| Endpoint | Method | Fungsi | User Class |
|
||||
|----------|--------|--------|------------|
|
||||
| `/` | GET | Homepage (lesson grid) | FrontendUser |
|
||||
| `/lesson/[slug]` | GET | Lesson page | FrontendUser |
|
||||
| `/progress` | GET | Progress report page | FrontendUser |
|
||||
| `/_app/immutable/*` | GET | Static JS/CSS assets | FrontendUser |
|
||||
| `/manifest.json` | GET | PWA manifest | FrontendUser |
|
||||
| `/circuitjs1/circuitjs.html` | GET | Circuit simulator | FrontendUser |
|
||||
|
||||
### Backend API (Flask :5000)
|
||||
|
||||
| Endpoint | Method | Fungsi | User Class |
|
||||
|----------|--------|--------|------------|
|
||||
| `/api/login` | POST | Login dengan token | Student, Teacher |
|
||||
| `/api/logout` | POST | Logout | Student |
|
||||
| `/api/validate-token` | POST | Validasi token | Student, Teacher, Stress |
|
||||
| `/api/lessons` | GET | Daftar pelajaran | Student, Teacher, Stress |
|
||||
| `/api/lesson/<slug>.json` | GET | Konten lesson | Student, Teacher, Stress |
|
||||
| `/api/compile` | POST | Compile & run kode | Student, Stress |
|
||||
| `/api/progress-report.json` | GET | Progress report | Teacher |
|
||||
| `/api/progress-report/export-csv` | GET | Export CSV | Teacher |
|
||||
|
||||
## Contoh Skenario Testing
|
||||
|
||||
### 1. Load Test Ringan (Development)
|
||||
```bash
|
||||
# API only
|
||||
./run.sh headless -u 20 -r 2 -t 60s
|
||||
|
||||
# Frontend
|
||||
./run.sh headless frontend -u 20 -r 2 -t 60s
|
||||
```
|
||||
|
||||
### 2. Load Test Sedang (Staging)
|
||||
```bash
|
||||
# API only
|
||||
./run.sh headless -u 100 -r 10 -t 300s
|
||||
|
||||
# Full Stack (Frontend + API)
|
||||
./run.sh headless fullstack -u 100 -r 10 -t 300s
|
||||
```
|
||||
|
||||
### 3. Load Test Berat (Production Capacity Test)
|
||||
```bash
|
||||
# API only
|
||||
./run.sh headless -u 500 -r 50 -t 600s
|
||||
|
||||
# Frontend
|
||||
./run.sh headless frontend -u 500 -r 50 -t 600s
|
||||
```
|
||||
|
||||
### 4. Stress Test (Find Breaking Point)
|
||||
```bash
|
||||
./run.sh headless fullstack -u 1000 -r 100 -t 300s
|
||||
```
|
||||
|
||||
### 5. Endurance Test (Stability Test)
|
||||
```bash
|
||||
./run.sh headless fullstack -u 50 -r 5 -t 2h
|
||||
```
|
||||
|
||||
### 6. Frontend Page Load Test
|
||||
```bash
|
||||
# Test HTML page rendering + static assets
|
||||
./run.sh headless frontend -u 100 -r 10 -t 5m
|
||||
```
|
||||
|
||||
## Metrik yang Dipantau
|
||||
|
||||
### Di Web UI
|
||||
- **RPS**: Requests per second
|
||||
- **Average Response Time**: Rata-rata waktu response
|
||||
- **Median Response Time**: Response time median
|
||||
- **95th Percentile**: 95% request lebih cepat dari nilai ini
|
||||
- **Failures**: Jumlah request gagal
|
||||
|
||||
### Log Console
|
||||
- Slow requests (>1 detik)
|
||||
- Total requests
|
||||
- Failure rate (%)
|
||||
- Average response time
|
||||
|
||||
## Threshold Performance
|
||||
|
||||
Berikut threshold yang direkomendasikan untuk LMS:
|
||||
|
||||
| Metrik | Target | Warning | Critical |
|
||||
|--------|--------|---------|----------|
|
||||
| Avg Response Time | <200ms | 200-500ms | >500ms |
|
||||
| 95th Percentile | <500ms | 500-1000ms | >1000ms |
|
||||
| Failure Rate | <0.1% | 0.1-1% | >1% |
|
||||
| RPS Capacity | >100 | 50-100 | <50 |
|
||||
|
||||
## Export Hasil
|
||||
|
||||
### HTML Report
|
||||
```bash
|
||||
locust -f locustfile.py --host=http://localhost:5000 --headless -u 100 -r 10 -t 300s --html=report.html
|
||||
```
|
||||
|
||||
### CSV Statistics
|
||||
```bash
|
||||
locust -f locustfile.py --host=http://localhost:5000 --headless -u 100 -r 10 -t 300s --csv=results
|
||||
```
|
||||
|
||||
Output:
|
||||
- `results_requests.csv`
|
||||
- `results_failures.csv`
|
||||
- `results_stats.csv`
|
||||
- `results_history.csv`
|
||||
|
||||
### JSON (via events)
|
||||
Tambahkan ke `locustfile.py`:
|
||||
```python
|
||||
@events.test_stop.add_listener
|
||||
def on_test_stop(environment, **kwargs):
|
||||
import json
|
||||
stats = {
|
||||
'total_requests': environment.stats.total.num_requests,
|
||||
'failures': environment.stats.total.num_failures,
|
||||
'avg_response_time': environment.stats.total.avg_response_time,
|
||||
}
|
||||
with open('results.json', 'w') as f:
|
||||
json.dump(stats, f)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### User Count Tidak Sesuai (Misal: Set 50, Yang Muncul 19)
|
||||
|
||||
**Penyebab:**
|
||||
1. **User class gagal start** - Task error atau on_start error
|
||||
2. **Host tidak reachable** - Server target belum running
|
||||
3. **Weight distribution** - Locust mendistribusikan user berdasarkan weight
|
||||
|
||||
**Solusi:**
|
||||
|
||||
1. **Periksa log console** untuk error messages:
|
||||
```bash
|
||||
./run.sh 2>&1 | grep -i error
|
||||
```
|
||||
|
||||
2. **Pastikan server target running**:
|
||||
```bash
|
||||
# Untuk API testing (:5000)
|
||||
curl http://localhost:5000/api/lessons
|
||||
|
||||
# Untuk Frontend testing (:3000)
|
||||
curl http://localhost:3000/
|
||||
```
|
||||
|
||||
3. **Gunakan mode yang sesuai**:
|
||||
```bash
|
||||
# API only - jangan gunakan frontend user
|
||||
./run.sh headless -u 50 -r 5
|
||||
|
||||
# Frontend only
|
||||
./run.sh headless frontend -u 50 -r 5
|
||||
|
||||
# Full stack (recommended)
|
||||
./run.sh headless fullstack -u 50 -r 5
|
||||
```
|
||||
|
||||
4. **Cek user count di log**:
|
||||
```
|
||||
✅ Spawning complete: 50 users active
|
||||
```
|
||||
|
||||
5. **Disable user class yang tidak perlu** dengan edit weight di `locustfile.py`:
|
||||
```python
|
||||
class FrontendUser(HttpUser):
|
||||
weight = 0 # Disable
|
||||
```
|
||||
|
||||
### Error: "Connection refused"
|
||||
|
||||
Pastikan backend/frontend berjalan:
|
||||
```bash
|
||||
# API (Flask)
|
||||
cd elemes
|
||||
python app.py
|
||||
|
||||
# Frontend (SvelteKit)
|
||||
cd elemes/frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Response time sangat tinggi
|
||||
|
||||
Kemungkinan penyebab:
|
||||
- Backend overload (kurangi user count)
|
||||
- Network latency (jalankan load test di network yang sama)
|
||||
- Database/file I/O bottleneck (periksa logs Flask)
|
||||
|
||||
### High failure rate
|
||||
|
||||
Periksa:
|
||||
- Token guru valid di `tokens_siswa.csv`
|
||||
- Content folder ada dan berisi file .md
|
||||
- Flask logs untuk error detail
|
||||
|
||||
### FrontendUser tidak muncul di panel
|
||||
|
||||
Jika testing di `:5000` (API), FrontendUser otomatis disabled karena halaman web tidak ada di Flask backend. Gunakan `:3000` untuk frontend testing.
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Mulai kecil**: Mulai dengan 10-20 user, naikkan bertahap
|
||||
2. **Monitor resources**: Gunakan `htop`, `docker stats` saat testing
|
||||
3. **Test di staging**: Jangan test load tinggi langsung di production
|
||||
4. **Baseline first**: Jalankan test sekali untuk dapat baseline metrics
|
||||
5. **Compare runs**: Simpan hasil untuk perbandingan setelah perubahan code
|
||||
|
||||
## Integrasi CI/CD
|
||||
|
||||
### GitHub Actions Example
|
||||
```yaml
|
||||
name: Load Test
|
||||
on: [push]
|
||||
jobs:
|
||||
load-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd test-load
|
||||
pip install -r requirements.txt
|
||||
- name: Run load test
|
||||
run: |
|
||||
cd test-load
|
||||
locust -f locustfile.py --host=http://localhost:5000 \
|
||||
--headless -u 50 -r 5 -t 60s --fail-on-ratio=0.01
|
||||
```
|
||||
|
||||
## Referensi
|
||||
|
||||
- [Locust Documentation](https://docs.locust.io/)
|
||||
- [Elemes Documentation](../elemes/documentation.md)
|
||||
- [Elemes README](../elemes/README.md)
|
||||
|
|
@ -0,0 +1,720 @@
|
|||
"""
|
||||
Locust Load Tests for Elemes LMS (Sinau-C)
|
||||
|
||||
This file contains load test scenarios for the Elemes Learning Management System.
|
||||
It simulates student and teacher behaviors including:
|
||||
- Student: login, view lessons, compile code, submit progress
|
||||
- Teacher: login, view progress reports, export CSV
|
||||
- Frontend: load HTML pages (home, lesson pages, progress page)
|
||||
|
||||
IMPORTANT: This test uses ONLY the teacher token to avoid modifying student data.
|
||||
|
||||
Usage:
|
||||
# API only (Flask backend :5000)
|
||||
locust -f locustfile.py --host=http://localhost:5000
|
||||
|
||||
# Frontend only (SvelteKit :3000)
|
||||
locust -f locustfile.py --host=http://localhost:3000
|
||||
|
||||
# Full stack test (use frontend host, it proxies /api to Flask)
|
||||
locust -f locustfile.py --host=http://localhost:3000
|
||||
|
||||
Or for headless mode:
|
||||
locust -f locustfile.py --host=http://localhost:3000 --headless -u 100 -r 10 -t 300s
|
||||
|
||||
Parameters:
|
||||
-u: Number of users to simulate
|
||||
-r: Spawn rate (users per second)
|
||||
-t: Test duration (e.g., 300s, 5m, 1h)
|
||||
"""
|
||||
|
||||
import random
|
||||
import time
|
||||
import logging
|
||||
from locust import HttpUser, task, between, events
|
||||
from faker import Faker
|
||||
|
||||
fake = Faker()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FrontendUser(HttpUser):
|
||||
"""
|
||||
Simulates a real user browsing the SvelteKit frontend.
|
||||
|
||||
This class tests the actual HTML pages that users see, including:
|
||||
- Home page (lesson grid)
|
||||
- Individual lesson pages
|
||||
- Progress report page
|
||||
- Static assets (CSS, JS)
|
||||
|
||||
NOTE: Set host to http://localhost:3000 for frontend testing
|
||||
This class will be disabled if host is :5000 (API only)
|
||||
"""
|
||||
|
||||
wait_time = between(2, 5) # Real users browse slower
|
||||
weight = 5 # Significant portion of traffic
|
||||
|
||||
teacher_token = "141214"
|
||||
|
||||
lessons = [
|
||||
"hello_world",
|
||||
"variables",
|
||||
"data_types",
|
||||
"operators",
|
||||
"conditions",
|
||||
"for_loops",
|
||||
"while_loops",
|
||||
"arrays",
|
||||
"strings",
|
||||
"functions",
|
||||
]
|
||||
|
||||
def on_start(self):
|
||||
"""Check if frontend is available"""
|
||||
try:
|
||||
# Quick check if we're testing frontend (port 3000) or API (port 5000)
|
||||
if self.host and ":5000" in self.host:
|
||||
logger.warning("FrontendUser disabled: Host is API server (:5000)")
|
||||
self.weight = 0 # Disable this user class
|
||||
else:
|
||||
logger.info("FrontendUser started successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"FrontendUser on_start error: {e}")
|
||||
|
||||
@task(5)
|
||||
def load_homepage(self):
|
||||
"""Load the main homepage (lesson grid)"""
|
||||
try:
|
||||
with self.client.get(
|
||||
"/",
|
||||
name="/ [Homepage]",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
# Check if page contains expected content
|
||||
if "Selamat Datang" in response.text or "lesson" in response.text.lower():
|
||||
response.success()
|
||||
else:
|
||||
response.success() # Still success even if content check fails
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"load_homepage error: {e}")
|
||||
|
||||
@task(8)
|
||||
def load_lesson_page(self):
|
||||
"""Load an individual lesson page"""
|
||||
try:
|
||||
lesson = random.choice(self.lessons)
|
||||
|
||||
with self.client.get(
|
||||
f"/lesson/{lesson}",
|
||||
name="/lesson/[slug]",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"load_lesson_page error: {e}")
|
||||
|
||||
@task(3)
|
||||
def load_progress_page(self):
|
||||
"""Load the progress report page (teacher view)"""
|
||||
try:
|
||||
with self.client.get(
|
||||
"/progress",
|
||||
name="/progress",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"load_progress_page error: {e}")
|
||||
|
||||
@task(2)
|
||||
def load_static_assets(self):
|
||||
"""Load static assets (CSS, JS, manifest)"""
|
||||
try:
|
||||
assets = [
|
||||
"/manifest.json",
|
||||
"/circuitjs1/circuitjs.html",
|
||||
]
|
||||
|
||||
for asset in random.sample(assets, min(2, len(assets))):
|
||||
self.client.get(
|
||||
asset,
|
||||
name="/static/[asset]",
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"load_static_assets error: {e}")
|
||||
|
||||
|
||||
class StudentUser(HttpUser):
|
||||
"""
|
||||
Simulates a student user interacting with the LMS.
|
||||
|
||||
Behavior flow:
|
||||
1. Login with student token (test accounts)
|
||||
2. View lesson list (API)
|
||||
3. View individual lessons (API + Frontend page)
|
||||
4. Compile code
|
||||
5. Validate token (read-only)
|
||||
|
||||
NOTE: Uses student test tokens (rows 40-72 in tokens_siswa.csv)
|
||||
These are test accounts that won't affect real student data
|
||||
|
||||
For full student simulation with frontend, use FrontendUser class.
|
||||
This class focuses on API interactions.
|
||||
"""
|
||||
|
||||
wait_time = between(1, 3) # Wait 1-3 seconds between tasks
|
||||
weight = 8 # 80% of traffic will be students
|
||||
|
||||
# Test student tokens from tokens_siswa.csv (rows 40-72)
|
||||
# These are accounts for testing purposes
|
||||
student_tokens = [
|
||||
"CC90965E", # ACHMAD RAFFY IRSYAD RAMADHAN
|
||||
"F84E3336", # AHMAD GHAUZAN AZRIL IRAWAN
|
||||
"D78F6DBD", # AINI BINTANG PRAMESWARI
|
||||
"0D7B2566", # ALVINO MIRZA SAPUTRA
|
||||
"3ED63295", # ANDREAS DIMAS PRASETIYO
|
||||
"90526D3E", # ARDEN HABIBIE PRADANA
|
||||
"298933B4", # ASHIF ZAFRAN ANABIL PUTRA ANWAR
|
||||
"26B7E4F7", # DANIA AURELY RAHMADANI PUTRI MAWANTI
|
||||
"D1B9E890", # DANISH AZKA HAFIZ
|
||||
"CCA98B61", # FADHIL IRSA ARYA MAHITA
|
||||
"894B6691", # FAREL MARCELINO RAMADHANI
|
||||
"8EC73A61", # FAVIAN ALLAND YUDHANTO
|
||||
"88DD18AF", # FITRA KURNIA RAMADHANI
|
||||
"DFDAE0BD", # GEORGE AXL SISWANDOYO
|
||||
"516D84E4", # HAIKAL AKBAR MAULANA
|
||||
"C1BCC269", # JHOFAN RADITYA RAFARDAN
|
||||
"0B59D8F3", # KEENAN ADHIYAKSA RIDHAA RAMADHAN
|
||||
"44888E91", # MOCHAMMAD FATHUR ROCHMAN ATTOILAH
|
||||
"1547C54D", # MUHAMAD FAHMI MAULIDI
|
||||
"348C765F", # MUHAMMAD AFIF FAITH RAMADHAN
|
||||
"E3183577", # MUHAMMAD FAIRUZ RAFI IRAWAN
|
||||
"1B4ACCD0", # MUHAMMAD FAURIL ALFIANSYAH
|
||||
"BB38203E", # MUHAMMAD KAMIL Hanif RAFASYAH
|
||||
"B4CD262B", # MUHAMMAD SAIFURRAHMAN AL 'AZIZI
|
||||
"63BC4A4F", # NADIVA
|
||||
"567050B0", # PANDU BUDI SATRIO
|
||||
"531D6090", # RADITYA PASHA
|
||||
"54933D60", # RAFAEL EZAR ALFATHUR
|
||||
"FD8D92D0", # RAHMITHA DWI RAHAYU
|
||||
"9023A552", # RATU AURELIA BEATRICE PRAYOGA
|
||||
"114B2384", # REVAN RAZIQ AKMAL
|
||||
"5E878384", # RIZKY NANDI SAPUTRA
|
||||
"BE0DEBFE", # SATRIYO WICAKSONO
|
||||
]
|
||||
|
||||
# Sample lesson filenames from content/
|
||||
lessons = [
|
||||
"hello_world",
|
||||
"variables",
|
||||
"data_types",
|
||||
"operators",
|
||||
"conditions",
|
||||
"for_loops",
|
||||
"while_loops",
|
||||
"arrays",
|
||||
"strings",
|
||||
"functions",
|
||||
]
|
||||
|
||||
# Sample C code for compilation tests
|
||||
code_samples = [
|
||||
{
|
||||
"name": "hello_world",
|
||||
"code": """#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
printf("Hello, World!\\n");
|
||||
return 0;
|
||||
}"""
|
||||
},
|
||||
{
|
||||
"name": "variables",
|
||||
"code": """#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
int age = 20;
|
||||
float height = 175.5;
|
||||
char grade = 'A';
|
||||
|
||||
printf("Age: %d\\n", age);
|
||||
printf("Height: %.1f\\n", height);
|
||||
printf("Grade: %c\\n", grade);
|
||||
|
||||
return 0;
|
||||
}"""
|
||||
},
|
||||
{
|
||||
"name": "conditions",
|
||||
"code": """#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
int score = 85;
|
||||
|
||||
if (score >= 90) {
|
||||
printf("Grade: A\\n");
|
||||
} else if (score >= 80) {
|
||||
printf("Grade: B\\n");
|
||||
} else if (score >= 70) {
|
||||
printf("Grade: C\\n");
|
||||
} else {
|
||||
printf("Grade: D\\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}"""
|
||||
},
|
||||
{
|
||||
"name": "for_loop",
|
||||
"code": """#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
printf("%d ", i);
|
||||
}
|
||||
printf("\\n");
|
||||
return 0;
|
||||
}"""
|
||||
},
|
||||
{
|
||||
"name": "array_sum",
|
||||
"code": """#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
int numbers[5] = {1, 2, 3, 4, 5};
|
||||
int sum = 0;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
sum += numbers[i];
|
||||
}
|
||||
|
||||
printf("Sum: %d\\n", sum);
|
||||
return 0;
|
||||
}"""
|
||||
},
|
||||
]
|
||||
|
||||
def on_start(self):
|
||||
"""Called when a simulated user starts"""
|
||||
try:
|
||||
self.token = random.choice(self.student_tokens)
|
||||
logger.info(f"StudentUser started with token: {self.token}")
|
||||
except Exception as e:
|
||||
logger.error(f"StudentUser on_start error: {e}")
|
||||
|
||||
@task(3)
|
||||
def view_lessons_list(self):
|
||||
"""View the list of available lessons"""
|
||||
try:
|
||||
with self.client.get(
|
||||
"/api/lessons",
|
||||
params={"token": self.token},
|
||||
name="/api/lessons",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if "lessons" in data:
|
||||
response.success()
|
||||
else:
|
||||
response.success() # Don't fail, just log
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"view_lessons_list error: {e}")
|
||||
|
||||
@task(5)
|
||||
def view_single_lesson(self):
|
||||
"""View a single lesson content"""
|
||||
try:
|
||||
lesson = random.choice(self.lessons)
|
||||
with self.client.get(
|
||||
f"/api/lesson/{lesson}.json",
|
||||
params={"token": self.token},
|
||||
name="/api/lesson/[slug].json",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"view_single_lesson error: {e}")
|
||||
|
||||
@task(4)
|
||||
def compile_code(self):
|
||||
"""Compile and run C code"""
|
||||
try:
|
||||
code_sample = random.choice(self.code_samples)
|
||||
|
||||
with self.client.post(
|
||||
"/api/compile",
|
||||
json={
|
||||
"code": code_sample["code"],
|
||||
"language": "c",
|
||||
},
|
||||
name="/api/compile",
|
||||
catch_response=True,
|
||||
timeout=30, # Compilation can take longer
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"compile_code error: {e}")
|
||||
|
||||
@task(1)
|
||||
def validate_token(self):
|
||||
"""Validate user token (read-only operation)"""
|
||||
try:
|
||||
with self.client.post(
|
||||
"/api/validate-token",
|
||||
json={"token": self.token},
|
||||
name="/api/validate-token",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"validate_token error: {e}")
|
||||
|
||||
@task(1)
|
||||
def logout(self):
|
||||
"""Logout from the system"""
|
||||
try:
|
||||
with self.client.post(
|
||||
"/api/logout",
|
||||
name="/api/logout",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"logout error: {e}")
|
||||
|
||||
|
||||
class TeacherUser(HttpUser):
|
||||
"""
|
||||
Simulates a teacher user interacting with the LMS.
|
||||
|
||||
Behavior flow:
|
||||
1. Login with teacher token
|
||||
2. View progress reports
|
||||
3. Export CSV
|
||||
4. View individual student progress
|
||||
5. Track progress (testing with test student tokens)
|
||||
"""
|
||||
|
||||
wait_time = between(2, 5) # Teachers browse slower
|
||||
weight = 2 # 20% of traffic will be teachers
|
||||
|
||||
# Teacher token (first row in tokens_siswa.csv)
|
||||
teacher_token = "141214" # Anggoro Dwi
|
||||
|
||||
# Test student tokens for progress tracking tests
|
||||
student_tokens = [
|
||||
"CC90965E", "F84E3336", "D78F6DBD", "0D7B2566", "3ED63295",
|
||||
"90526D3E", "298933B4", "26B7E4F7", "D1B9E890", "CCA98B61",
|
||||
]
|
||||
|
||||
lessons = [
|
||||
"hello_world",
|
||||
"variables",
|
||||
"data_types",
|
||||
"operators",
|
||||
"conditions",
|
||||
"for_loops",
|
||||
"while_loops",
|
||||
"arrays",
|
||||
"strings",
|
||||
"functions",
|
||||
]
|
||||
|
||||
def on_start(self):
|
||||
"""Called when a simulated user starts"""
|
||||
try:
|
||||
self.token = self.teacher_token
|
||||
logger.info(f"TeacherUser started with token: {self.token}")
|
||||
except Exception as e:
|
||||
logger.error(f"TeacherUser on_start error: {e}")
|
||||
|
||||
@task(4)
|
||||
def view_progress_report(self):
|
||||
"""View student progress report"""
|
||||
try:
|
||||
with self.client.get(
|
||||
"/api/progress-report.json",
|
||||
name="/api/progress-report.json",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"view_progress_report error: {e}")
|
||||
|
||||
@task(2)
|
||||
def export_progress_csv(self):
|
||||
"""Export progress report as CSV"""
|
||||
try:
|
||||
with self.client.get(
|
||||
"/api/progress-report/export-csv",
|
||||
name="/api/progress-report/export-csv",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"export_progress_csv error: {e}")
|
||||
|
||||
@task(3)
|
||||
def view_lessons_list(self):
|
||||
"""View the list of lessons (for grading reference)"""
|
||||
try:
|
||||
with self.client.get(
|
||||
"/api/lessons",
|
||||
params={"token": self.token},
|
||||
name="/api/lessons",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"view_lessons_list error: {e}")
|
||||
|
||||
@task(2)
|
||||
def view_single_lesson(self):
|
||||
"""View a single lesson content"""
|
||||
try:
|
||||
lesson = random.choice(self.lessons)
|
||||
with self.client.get(
|
||||
f"/api/lesson/{lesson}.json",
|
||||
params={"token": self.token},
|
||||
name="/api/lesson/[slug].json",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"view_single_lesson error: {e}")
|
||||
|
||||
@task(1)
|
||||
def validate_token(self):
|
||||
"""Validate teacher token"""
|
||||
try:
|
||||
with self.client.post(
|
||||
"/api/validate-token",
|
||||
json={"token": self.token},
|
||||
name="/api/validate-token",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"validate_token error: {e}")
|
||||
|
||||
@task(1)
|
||||
def track_student_progress(self):
|
||||
"""Track progress for test student accounts (safe for testing)"""
|
||||
try:
|
||||
token = random.choice(self.student_tokens)
|
||||
lesson = random.choice(self.lessons)
|
||||
|
||||
with self.client.post(
|
||||
"/api/track-progress",
|
||||
json={
|
||||
"token": token,
|
||||
"lesson_name": lesson,
|
||||
"status": "completed",
|
||||
},
|
||||
name="/api/track-progress",
|
||||
catch_response=True,
|
||||
timeout=10,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
response.success()
|
||||
else:
|
||||
response.failure(f"Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"track_student_progress error: {e}")
|
||||
|
||||
|
||||
class APIStressTest(HttpUser):
|
||||
"""
|
||||
Stress test specific API endpoints.
|
||||
This user class focuses on hammering specific endpoints to find bottlenecks.
|
||||
|
||||
NOTE: Uses test student tokens - safe for production testing
|
||||
These tokens are designated for testing purposes only
|
||||
"""
|
||||
|
||||
wait_time = between(0.1, 0.5) # Very fast requests
|
||||
weight = 1 # Lower weight for stress tests
|
||||
|
||||
# Test student tokens for stress testing
|
||||
student_tokens = [
|
||||
"CC90965E", "F84E3336", "D78F6DBD", "0D7B2566", "3ED63295",
|
||||
]
|
||||
|
||||
lessons = ["hello_world", "variables", "conditions"]
|
||||
|
||||
@task(3)
|
||||
def stress_compile_endpoint(self):
|
||||
"""Stress test the compile endpoint"""
|
||||
try:
|
||||
code = """#include <stdio.h>
|
||||
int main() { printf("Stress test\\n"); return 0; }"""
|
||||
|
||||
self.client.post(
|
||||
"/api/compile",
|
||||
json={"code": code, "language": "c"},
|
||||
name="/api/compile [stress]",
|
||||
timeout=30,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"stress_compile_endpoint error: {e}")
|
||||
|
||||
@task(2)
|
||||
def stress_lesson_endpoint(self):
|
||||
"""Stress test the lesson endpoint"""
|
||||
try:
|
||||
token = random.choice(self.student_tokens)
|
||||
self.client.get(
|
||||
"/api/lesson/hello_world.json",
|
||||
params={"token": token},
|
||||
name="/api/lesson/[slug].json [stress]",
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"stress_lesson_endpoint error: {e}")
|
||||
|
||||
@task(1)
|
||||
def stress_validate_token(self):
|
||||
"""Stress test the validate token endpoint"""
|
||||
try:
|
||||
token = random.choice(self.student_tokens)
|
||||
self.client.post(
|
||||
"/api/validate-token",
|
||||
json={"token": token},
|
||||
name="/api/validate-token [stress]",
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"stress_validate_token error: {e}")
|
||||
|
||||
@task(1)
|
||||
def stress_lessons_list(self):
|
||||
"""Stress test the lessons list endpoint"""
|
||||
try:
|
||||
token = random.choice(self.student_tokens)
|
||||
self.client.get(
|
||||
"/api/lessons",
|
||||
params={"token": token},
|
||||
name="/api/lessons [stress]",
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"stress_lessons_list error: {e}")
|
||||
|
||||
@task(1)
|
||||
def stress_track_progress(self):
|
||||
"""Stress test the progress tracking endpoint"""
|
||||
try:
|
||||
token = random.choice(self.student_tokens)
|
||||
lesson = random.choice(self.lessons)
|
||||
|
||||
self.client.post(
|
||||
"/api/track-progress",
|
||||
json={
|
||||
"token": token,
|
||||
"lesson_name": lesson,
|
||||
"status": "completed",
|
||||
},
|
||||
name="/api/track-progress [stress]",
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"stress_track_progress error: {e}")
|
||||
|
||||
|
||||
# Event hooks for custom logging
|
||||
@events.request.add_listener
|
||||
def on_request(request_type, name, response_time, response_length, response, context, exception, **kwargs):
|
||||
"""Log slow requests"""
|
||||
if response_time > 1000: # More than 1 second
|
||||
logger.warning(f"⚠️ SLOW REQUEST: {name} took {response_time}ms")
|
||||
|
||||
|
||||
@events.test_start.add_listener
|
||||
def on_test_start(environment, **kwargs):
|
||||
"""Called when load test starts"""
|
||||
logger.info("🚀 Starting load test for Elemes LMS")
|
||||
logger.info(f"📊 Target host: {environment.host}")
|
||||
logger.info("🔒 Using teacher token only (safe for production testing)")
|
||||
|
||||
# Log user class configuration
|
||||
user_classes = environment.user_classes
|
||||
total_weight = sum(uc.weight for uc in user_classes if hasattr(uc, 'weight'))
|
||||
logger.info(f"👥 User classes loaded: {[uc.__name__ for uc in user_classes]}")
|
||||
logger.info(f"⚖️ Total weight: {total_weight}")
|
||||
|
||||
for uc in user_classes:
|
||||
if hasattr(uc, 'weight'):
|
||||
logger.info(f" - {uc.__name__}: weight={uc.weight}")
|
||||
|
||||
|
||||
@events.spawning_complete.add_listener
|
||||
def on_spawning_complete(environment, user_count, **kwargs):
|
||||
"""Called when spawning is complete"""
|
||||
logger.info(f"✅ Spawning complete: {user_count} users active")
|
||||
|
||||
|
||||
@events.test_stop.add_listener
|
||||
def on_test_stop(environment, **kwargs):
|
||||
"""Called when load test stops"""
|
||||
logger.info("✅ Load test completed")
|
||||
if environment.stats.total.num_requests > 0:
|
||||
fail_rate = (environment.stats.total.num_failures / environment.stats.total.num_requests) * 100
|
||||
logger.info(f"📈 Total requests: {environment.stats.total.num_requests}")
|
||||
logger.info(f"❌ Failures: {environment.stats.total.num_failures} ({fail_rate:.2f}%)")
|
||||
logger.info(f"⏱️ Average response time: {environment.stats.total.avg_response_time:.2f}ms")
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Locust Load Testing Dependencies for Elemes LMS
|
||||
# Install: pip install -r requirements.txt
|
||||
|
||||
locust>=2.20.0
|
||||
faker>=21.0.0
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
#!/bin/bash
|
||||
# Script untuk menjalankan Locust Load Test dengan virtual environment
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VENV_DIR="$SCRIPT_DIR/venv"
|
||||
PYTHON="$VENV_DIR/bin/python"
|
||||
PIP="$VENV_DIR/bin/pip"
|
||||
LOCUST="$VENV_DIR/bin/locust"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo "🚀 Elemes LMS Load Test Setup"
|
||||
echo "=============================="
|
||||
|
||||
# Check if virtual environment exists
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo -e "${YELLOW}📦 Creating virtual environment...${NC}"
|
||||
python3 -m venv "$VENV_DIR"
|
||||
echo -e "${GREEN}✅ Virtual environment created${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ Virtual environment already exists${NC}"
|
||||
fi
|
||||
|
||||
# Activate virtual environment and install dependencies
|
||||
echo -e "${YELLOW}📦 Installing dependencies...${NC}"
|
||||
"$PIP" install --upgrade pip -q
|
||||
"$PIP" install -r "$SCRIPT_DIR/requirements.txt" -q
|
||||
echo -e "${GREEN}✅ Dependencies installed${NC}"
|
||||
|
||||
echo ""
|
||||
echo "=============================="
|
||||
echo "Usage:"
|
||||
echo " ./run.sh - Start Web UI (default: API on :5000)"
|
||||
echo " ./run.sh frontend - Start Web UI for Frontend testing (:3000)"
|
||||
echo " ./run.sh fullstack - Start Web UI testing full stack (:3000)"
|
||||
echo " ./run.sh headless - Run in headless mode (default: 50 users, 5 spawn rate, 60s)"
|
||||
echo " ./run.sh clean - Remove virtual environment"
|
||||
echo ""
|
||||
echo "Headless mode options:"
|
||||
echo " ./run.sh headless -u 100 -r 10 -t 300s"
|
||||
echo " ./run.sh headless frontend -u 100 -r 10 -t 5m"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " # Test API only (Flask backend)"
|
||||
echo " ./run.sh --host=http://localhost:5000"
|
||||
echo ""
|
||||
echo " # Test Frontend only (SvelteKit)"
|
||||
echo " ./run.sh frontend --host=http://localhost:3000"
|
||||
echo ""
|
||||
echo " # Test full stack (Frontend + API via proxy)"
|
||||
echo " ./run.sh fullstack -u 100 -r 10 -t 5m"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
|
||||
# Parse arguments
|
||||
MODE="api"
|
||||
HEADLESS=false
|
||||
|
||||
if [ "$1" == "clean" ]; then
|
||||
echo -e "${YELLOW}🗑️ Removing virtual environment...${NC}"
|
||||
rm -rf "$VENV_DIR"
|
||||
echo -e "${GREEN}✅ Virtual environment removed${NC}"
|
||||
exit 0
|
||||
elif [ "$1" == "frontend" ]; then
|
||||
MODE="frontend"
|
||||
shift
|
||||
elif [ "$1" == "fullstack" ]; then
|
||||
MODE="fullstack"
|
||||
shift
|
||||
elif [ "$1" == "headless" ]; then
|
||||
HEADLESS=true
|
||||
shift
|
||||
# Check if next arg is frontend/fullstack
|
||||
if [ "$1" == "frontend" ]; then
|
||||
MODE="frontend"
|
||||
shift
|
||||
elif [ "$1" == "fullstack" ]; then
|
||||
MODE="fullstack"
|
||||
shift
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set default host based on mode
|
||||
if [ "$MODE" == "frontend" ]; then
|
||||
HOST="${HOST:-http://localhost:3000}"
|
||||
echo -e "${YELLOW}🎨 Frontend testing mode${NC}"
|
||||
elif [ "$MODE" == "fullstack" ]; then
|
||||
HOST="${HOST:-http://localhost:3000}"
|
||||
echo -e "${YELLOW}🔄 Fullstack testing mode${NC}"
|
||||
else
|
||||
HOST="${HOST:-http://localhost:5000}"
|
||||
echo -e "${YELLOW}🔧 API testing mode${NC}"
|
||||
fi
|
||||
|
||||
if [ "$HEADLESS" == "true" ]; then
|
||||
# Default values (HOST already set above based on mode)
|
||||
USERS="${USERS:-50}"
|
||||
SPAWN="${SPAWN:-5}"
|
||||
TIME="${TIME:-60s}"
|
||||
|
||||
# Parse additional arguments
|
||||
shift
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-u|--users)
|
||||
USERS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-r|--spawn)
|
||||
SPAWN="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--time)
|
||||
TIME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--host)
|
||||
HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $1${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo -e "${YELLOW}🏃 Running Locust in headless mode...${NC}"
|
||||
echo " Host: $HOST"
|
||||
echo " Users: $USERS"
|
||||
echo " Spawn rate: $SPAWN/s"
|
||||
echo " Duration: $TIME"
|
||||
echo ""
|
||||
|
||||
"$LOCUST" -f "$SCRIPT_DIR/locustfile.py" \
|
||||
--host="$HOST" \
|
||||
--headless \
|
||||
-u "$USERS" \
|
||||
-r "$SPAWN" \
|
||||
-t "$TIME"
|
||||
else
|
||||
# Default: Web UI mode (HOST already set above based on mode)
|
||||
|
||||
echo -e "${YELLOW}🌐 Starting Locust Web UI...${NC}"
|
||||
echo " Target: $HOST"
|
||||
echo " Mode: $MODE"
|
||||
echo " Open http://localhost:8089 in your browser"
|
||||
echo ""
|
||||
echo -e "${GREEN}Press Ctrl+C to stop${NC}"
|
||||
echo ""
|
||||
|
||||
"$LOCUST" -f "$SCRIPT_DIR/locustfile.py" --host="$HOST"
|
||||
fi
|
||||
Loading…
Reference in New Issue