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