remote gitignore and test-load
This commit is contained in:
parent
6088254959
commit
68e33b9e48
|
|
@ -1,3 +1,4 @@
|
||||||
state/
|
state/
|
||||||
.env
|
.env
|
||||||
.qwen
|
.qwen
|
||||||
|
tokens_siswa.csv
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,454 +0,0 @@
|
||||||
# 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)
|
|
||||||
|
|
@ -1,720 +0,0 @@
|
||||||
"""
|
|
||||||
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")
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# 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
159
test-load/run.sh
|
|
@ -1,159 +0,0 @@
|
||||||
#!/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