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:
a2nr 2026-04-02 10:57:14 +07:00
parent 5d91087426
commit 885980f276
5 changed files with 1357 additions and 0 deletions

19
test-load/.gitignore vendored Normal file
View File

@ -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

454
test-load/README.md Normal file
View File

@ -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)

720
test-load/locustfile.py Normal file
View File

@ -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")

View File

@ -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

159
test-load/run.sh Executable file
View File

@ -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