diff --git a/.gitignore b/.gitignore index 945f189..76b2cc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ state/ .env .qwen +tokens_siswa.csv diff --git a/test-load/.gitignore b/test-load/.gitignore deleted file mode 100644 index 5b3c156..0000000 --- a/test-load/.gitignore +++ /dev/null @@ -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 diff --git a/test-load/README.md b/test-load/README.md deleted file mode 100644 index 957e013..0000000 --- a/test-load/README.md +++ /dev/null @@ -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/.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) diff --git a/test-load/locustfile.py b/test-load/locustfile.py deleted file mode 100644 index f35c503..0000000 --- a/test-load/locustfile.py +++ /dev/null @@ -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 - -int main() { - printf("Hello, World!\\n"); - return 0; -}""" - }, - { - "name": "variables", - "code": """#include - -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 - -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 - -int main() { - for (int i = 1; i <= 10; i++) { - printf("%d ", i); - } - printf("\\n"); - return 0; -}""" - }, - { - "name": "array_sum", - "code": """#include - -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 -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") diff --git a/test-load/requirements.txt b/test-load/requirements.txt deleted file mode 100644 index dd6ca41..0000000 --- a/test-load/requirements.txt +++ /dev/null @@ -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 diff --git a/test-load/run.sh b/test-load/run.sh deleted file mode 100755 index dc2998f..0000000 --- a/test-load/run.sh +++ /dev/null @@ -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