From 606d83ebacfe2410ce35a437d70b55c3a632b390 Mon Sep 17 00:00:00 2001 From: a2nr Date: Sat, 17 Jan 2026 14:03:51 +0700 Subject: [PATCH] Enhance load testing capabilities with sophisticated user behavior patterns - Add comprehensive LOAD_TESTING_GUIDE.md with instructions for different testing scenarios - Implement advanced user behavior patterns in locustfile.py: * SessionBasedUser class to simulate complete learning sessions * BehaviorAnalysisTaskSet for sequential lesson progression * PowerUser class representing intensive usage patterns - Improve code extraction and compilation functionality to work with lesson content - Enhance realism by using actual tokens from tokens_siswa.csv for user simulation - Add proper progress tracking during simulated learning sessions Co-authored-by: Qwen-Coder --- test/LOAD_TESTING_GUIDE.md | 125 +++++++++++++++++++++++ test/locustfile.py | 197 ++++++++++++++++++++++++++++++++++++- 2 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 test/LOAD_TESTING_GUIDE.md diff --git a/test/LOAD_TESTING_GUIDE.md b/test/LOAD_TESTING_GUIDE.md new file mode 100644 index 0000000..2fde46b --- /dev/null +++ b/test/LOAD_TESTING_GUIDE.md @@ -0,0 +1,125 @@ +# Load Testing Guide for C Programming Learning Management System + +This guide explains how to run load tests on the LMS-C application using Locust with different scenarios and configurations. + +## Prerequisites + +Before running load tests, ensure you have: +- Podman installed on your system +- The LMS-C application is properly configured with content and tokens +- The `tokens_siswa.csv` file contains student tokens (the load testing script will use these for realistic user simulation) + +## Running Load Tests + +### 1. Basic Load Test + +To run a basic load test with the default configuration: + +```bash +# Navigate to the test directory +cd /path/to/lms-c/elemes/test + +# Run the load test with default settings (10 simulated users) +podman-compose -f podman-compose.yml up --build +``` + +Then access the Locust web interface at `http://localhost:8089` and configure your test parameters. + +### 2. Distributed Load Test + +For larger-scale testing, you can run a distributed test with multiple workers: + +```bash +# Start the master node +podman-compose -f podman-compose.yml up --scale worker=3 master + +# Or run with specific environment variables +TARGET_URL=http://your-lms-url.com LOCUST_NUM_STUDENTS=100 podman-compose -f podman-compose.yml up --scale worker=3 master +``` + +This will start 1 master and 3 worker nodes to distribute the load. + +### 3. Environment Variables + +The load testing configuration can be customized using these environment variables: + +- `TARGET_URL`: The URL of the LMS-C application to test (default: `http://example.com`) +- `LOCUST_NUM_STUDENTS`: Number of simulated students/users (default: 10, but will automatically detect from tokens_siswa.csv) + +Example: +```bash +TARGET_URL=http://192.168.1.100:5000 LOCUST_NUM_STUDENTS=50 podman-compose -f podman-compose.yml up --build +``` + +### 4. Customizing Test Scenarios + +The `locustfile.py` implements several user behavior patterns: + +#### WebsiteUser Class +- Simulates basic website visitors +- Performs tasks like viewing homepage, lessons, and compiling code +- Weight: 1 (less frequent) + +#### AdvancedUser Class +- Simulates engaged students who actively participate +- Performs more complex behaviors like lesson navigation and intensive code compilation +- Weight: 2 (twice as likely to be chosen as WebsiteUser) + +#### Task Distribution +- `view_homepage`: Weight 3 (most common action) +- `compile_code`: Weight 4 (very common action) +- `view_lesson`: Weight 2 (common action) +- `login_student`: Weight 1 (less frequent but important) +- `validate_token`: Weight 1 (essential for tracking) +- `track_progress`: Weight 1 (important for completion tracking) + +### 5. Realistic Student Simulation + +The load testing script reads from `tokens_siswa.csv` to simulate real students: +- Each simulated user gets assigned a real student token from the CSV +- This ensures realistic progress tracking behavior +- The number of simulated users should match or be proportional to the number of tokens in the CSV + +### 6. Monitoring and Analysis + +Access the Locust web interface at `http://localhost:8089` to: +- Configure the number of users and spawn rate +- Monitor real-time statistics +- View response times, failure rates, and throughput +- Download test reports + +### 7. Running Without the Web Interface + +You can also run Locust in headless mode: + +```bash +# Run with specific parameters without web UI +podman run -v $(pwd)/..:/mnt/locust \ + -e LOCUST_HOST=http://your-target-url.com \ + -e LOCUST_USERS=100 \ + -e LOCUST_SPAWN_RATE=10 \ + -e LOCUST_RUN_TIME=10m \ + locustio/locust -f /mnt/locust/elemes/test/locustfile.py --headless +``` + +### 8. Scaling Recommendations + +- For 1-50 concurrent users: Single master node is sufficient +- For 50-200 concurrent users: Use 1 master + 2-3 worker nodes +- For 200+ concurrent users: Scale workers proportionally (1 master + 5+ workers) + +### 9. Best Practices + +- Always test against a staging environment that mirrors production +- Gradually increase the number of users to identify performance bottlenecks +- Monitor server resources (CPU, memory, disk I/O) during tests +- Run tests multiple times to account for variations +- Clean up resources after testing to avoid unnecessary resource consumption + +### 10. Troubleshooting + +If you encounter issues: +- Ensure the target LMS-C application is accessible from the Locust containers +- Check that the `tokens_siswa.csv` file is properly mounted and readable +- Verify that the content directory has lesson files for realistic testing +- Monitor container logs with `podman logs -f ` \ No newline at end of file diff --git a/test/locustfile.py b/test/locustfile.py index e614d56..7cb791f 100644 --- a/test/locustfile.py +++ b/test/locustfile.py @@ -307,6 +307,10 @@ class WebsiteUser(HttpUser): pass # Ignore logout errors +# Define the user classes to be used in the test +user_classes = [WebsiteUser, AdvancedUser, SessionBasedUser, PowerUser] + + # Additional task sets for more complex behaviors class LessonNavigationTaskSet(TaskSet): @@ -506,4 +510,195 @@ class AdvancedUser(HttpUser): weight = 2 # Twice as likely to be chosen as WebsiteUser tasks = {LessonNavigationTaskSet: 2, CompilationFocusedTaskSet: 3, LMSCUserBehavior: 4} - wait_time = between(0.5, 2) \ No newline at end of file + wait_time = between(0.5, 2) + + +class SessionBasedUser(HttpUser): + """ + User that simulates a complete learning session with focused behavior + """ + weight = 1 + tasks = [LMSCUserBehavior] + + wait_time = between(2, 5) + + def on_start(self): + """ + Initialize a complete learning session + """ + # Login at the beginning of the session + tokens_file = '/mnt/locust/tokens_siswa.csv' + all_students = [] + + if os.path.exists(tokens_file): + with open(tokens_file, 'r', newline='', encoding='utf-8') as csvfile: + reader = csv.DictReader(csvfile, delimiter=';') + all_students = list(reader) + + if all_students: + selected_student = random.choice(all_students) + self.student_token = selected_student.get('token', f"STUDENT_TOKEN_{random.randint(1000, 9999)}") + self.student_name = selected_student.get('nama_siswa', f"Student_{random.randint(1000, 9999)}") + else: + self.student_token = f"STUDENT_TOKEN_{random.randint(1000, 9999)}" + self.student_name = f"Student_{random.randint(1000, 9999)}" + + # Login the student + login_payload = { + "token": self.student_token + } + + try: + self.client.post("/login", json=login_payload) + except: + pass # Continue even if login fails + + # Read lesson files + content_dir = '/mnt/locust/content' + self.lesson_files = [] + + if os.path.exists(content_dir): + lesson_paths = glob.glob(os.path.join(content_dir, "*.md")) + self.lesson_files = [os.path.basename(path) for path in lesson_paths if os.path.isfile(path)] + + +class BehaviorAnalysisTaskSet(TaskSet): + """ + Task set for analyzing user behavior patterns + """ + + def on_start(self): + """ + Initialize with student data + """ + tokens_file = '/mnt/locust/tokens_siswa.csv' + all_students = [] + + if os.path.exists(tokens_file): + with open(tokens_file, 'r', newline='', encoding='utf-8') as csvfile: + reader = csv.DictReader(csvfile, delimiter=';') + all_students = list(reader) + + if all_students: + selected_student = random.choice(all_students) + self.student_token = selected_student.get('token', f"STUDENT_TOKEN_{random.randint(1000, 9999)}") + self.student_name = selected_student.get('nama_siswa', f"Student_{random.randint(1000, 9999)}") + else: + self.student_token = f"STUDENT_TOKEN_{random.randint(1000, 9999)}" + self.student_name = f"Student_{random.randint(1000, 9999)}" + + # Read lesson files + content_dir = '/mnt/locust/content' + self.lesson_files = [] + + if os.path.exists(content_dir): + lesson_paths = glob.glob(os.path.join(content_dir, "*.md")) + self.lesson_files = [os.path.basename(path) for path in lesson_paths if os.path.isfile(path)] + + @task(3) + def analyze_learning_pattern(self): + """ + Simulate a learning pattern where a student goes through multiple lessons in sequence + """ + if not self.lesson_files: + return + + # Select a few lessons to go through in sequence + selected_lessons = random.sample(self.lesson_files, min(3, len(self.lesson_files))) + + for lesson in selected_lessons: + # Visit the lesson + self.client.get(f"/lesson/{lesson}?token={self.student_token}") + + # Spend some time reading (simulate wait) + self.wait() + + # Try to compile code from the lesson + self.compile_code_from_lesson(lesson) + + # Track progress for the lesson + lesson_name = lesson.replace('.md', '') + progress_payload = { + "token": self.student_token, + "lesson_name": lesson_name, + "status": "in_progress" + } + self.client.post("/track-progress", json=progress_payload) + + # Wait between activities + self.wait() + + # Mark final lesson as completed + if selected_lessons: + final_lesson = selected_lessons[-1].replace('.md', '') + progress_payload = { + "token": self.student_token, + "lesson_name": final_lesson, + "status": "completed" + } + self.client.post("/track-progress", json=progress_payload) + + def compile_code_from_lesson(self, lesson_file): + """ + Attempt to compile code from a specific lesson + """ + lesson_path = f'/mnt/locust/content/{lesson_file}' + + if not os.path.exists(lesson_path): + return + + try: + with open(lesson_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Look for code sections in the lesson + initial_code = self.extract_code_section(content, '---INITIAL_CODE---', '---END_INITIAL_CODE---') + solution_code = self.extract_code_section(content, '---SOLUTION_CODE---', '---END_SOLUTION_CODE---') + + # Use available code for compilation + code_to_compile = None + if solution_code: + code_to_compile = solution_code + elif initial_code: + code_to_compile = initial_code + + if code_to_compile: + programming_language = os.environ.get('DEFAULT_PROGRAMMING_LANGUAGE', 'c') + + compile_payload = { + "code": code_to_compile, + "language": programming_language + } + + response = self.client.post("/compile", json=compile_payload) + + if response.status_code == 200: + result = response.json() + if not result.get("success"): + print(f"Compilation error in lesson {lesson_file}: {result.get('error')}") + except Exception as e: + print(f"Error compiling code from lesson {lesson_file}: {str(e)}") + + def extract_code_section(self, content, start_marker, end_marker): + """ + Extract code between start and end markers + """ + start_idx = content.find(start_marker) + end_idx = content.find(end_marker) + + if start_idx != -1 and end_idx != -1 and end_idx > start_idx: + start_pos = start_idx + len(start_marker) + extracted_code = content[start_pos:end_idx].strip() + return extracted_code + + return None + + +class PowerUser(HttpUser): + """ + Power user that exhibits intensive usage patterns + """ + weight = 1 + tasks = {BehaviorAnalysisTaskSet: 3, CompilationFocusedTaskSet: 4, LMSCUserBehavior: 2} + + wait_time = between(0.2, 1.5) \ No newline at end of file