From 89f0967c3eb9876c8cf6e3e8e6d3e80ab656bbab Mon Sep 17 00:00:00 2001 From: a2nr Date: Fri, 17 Apr 2026 10:50:46 +0700 Subject: [PATCH] fix: improve JSON parsing robustness in auth routes and update Locust load test scripts with worker management utilities. --- load-test/locustfile.py | 31 ++++++++++++++++++++----------- load-test/start_locust_workers.sh | 24 ++++++++++++++++++++++++ load-test/stop_locust.sh | 4 ++++ proposal.md | 19 ++++++++----------- routes/auth.py | 16 ++++++++-------- 5 files changed, 64 insertions(+), 30 deletions(-) create mode 100755 load-test/start_locust_workers.sh create mode 100755 load-test/stop_locust.sh diff --git a/load-test/locustfile.py b/load-test/locustfile.py index 4ab0183..0db3e0d 100644 --- a/load-test/locustfile.py +++ b/load-test/locustfile.py @@ -308,7 +308,7 @@ class ElemesStudent(HttpUser): def compile_arduino_lesson(self): """ Compile an Arduino lesson's code via Velxio backend. - Hits Velxio's POST /velxio/api/compile endpoint. + Hits Velxio's POST /velxio/api/compile/ endpoint. Validates: compile success + hex_content returned. Note: serial output can only be validated in-browser (avr8js sim). """ @@ -321,8 +321,11 @@ class ElemesStudent(HttpUser): return with self.client.post( - '/velxio/api/compile', - json={'code': code, 'board_fqbn': 'arduino:avr:uno'}, + '/velxio/api/compile/', + json={ + 'files': [{'name': 'sketch.ino', 'content': code}], + 'board_fqbn': 'arduino:avr:uno' + }, name='/velxio/api/compile [Arduino]', timeout=30, catch_response=True @@ -331,10 +334,14 @@ class ElemesStudent(HttpUser): if not data.get('success'): # Report as Locust failure for stats tracking - resp.failure( - f"{lesson['slug']}: compile failed — " - f"{data.get('error', data.get('stderr', 'unknown'))}" - ) + # Fallback to checking 'detail' for HTTP 422 validations + err_msg = data.get('error', data.get('stderr')) + if not err_msg and data.get('detail'): + err_msg = str(data.get('detail')) + elif not err_msg: + err_msg = f"HTTP {resp.status_code} - unknown" + + resp.failure(f"{lesson['slug']}: compile failed — {err_msg}") return hex_content = data.get('hex_content', '') @@ -383,12 +390,14 @@ class ElemesStudent(HttpUser): elif lesson['type'] == 'arduino' and lesson.get('initial_code_arduino'): self.client.post( - '/velxio/api/compile', - json={'code': lesson['initial_code_arduino'], - 'board_fqbn': 'arduino:avr:uno'}, + '/velxio/api/compile/', + json={ + 'files': [{'name': 'sketch.ino', 'content': lesson['initial_code_arduino']}], + 'board_fqbn': 'arduino:avr:uno' + }, name='/velxio/api/compile (flow)', timeout=30 - ) + ) time.sleep(0.5) # 3. Track progress diff --git a/load-test/start_locust_workers.sh b/load-test/start_locust_workers.sh new file mode 100755 index 0000000..b6f63b3 --- /dev/null +++ b/load-test/start_locust_workers.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Pastikan berada di dalam folder load-test +cd "$(dirname "$0")" + +echo "Menutup proses locust yang mungkin sedang berjalan..." +pkill -f "locust -f" 2>/dev/null +sleep 1 + +echo "Memulai Locust Master pada http://localhost:8089 ..." +locust -f locustfile.py --master > master.log 2>&1 & + +sleep 2 + +echo "Memulai 5 Locust Workers..." +for i in {1..5}; do + locust -f locustfile.py --worker > worker_$i.log 2>&1 & + echo "Worker $i berjalan..." +done + +echo "" +echo "Selesai! 1 Master dan 5 Worker sudah berjalan di latar belakang." +echo "Silakan buka kembali http://localhost:8089 di Chrome Anda." +echo "Catatan: Untuk mematikan semuanya nanti, gunakan perintah: ./stop_locust.sh atau ketik 'pkill -f locust'." diff --git a/load-test/stop_locust.sh b/load-test/stop_locust.sh new file mode 100755 index 0000000..dcf47ff --- /dev/null +++ b/load-test/stop_locust.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo "Mematikan server dan seluruh worker Locust..." +pkill -f "locust" +echo "Locust berhasil dihentikan." diff --git a/proposal.md b/proposal.md index 4dcbc94..26ab8b3 100644 --- a/proposal.md +++ b/proposal.md @@ -30,17 +30,14 @@ Berdasarkan log penyelesaian integrasi Velxio dan dokumen sebelumnya, berikut ad - URL backend dikonfigurasi user melalui Locust web UI ## 🔴 Prioritas Tinggi -- [ ] **Testing End-to-End (E2E)** - - Script Locust sudah siap, tinggal jalankan: - ``` - cd elemes/load-test && python content_parser.py && locust -f locustfile.py - ``` - - Build ulang container dan test via Locust web UI - - Tes khusus: lesson tanpa wiring (`hello_serial_arduino.md`) harus bisa pass hanya dengan kode + serial. -- [ ] **Push ke Remote** +- [x] **Testing End-to-End (E2E)** + - Script Locust sudah disiapkan (di dalam `load-test/`). + - Bug pada proxy JSON parsing dari frontend ke backend sudah diperbaiki dengan `force=True` dan `silent=True` di `auth.py`, sehingga Login via test suite berhasil. +- [x] **Push ke Remote** - `git push` untuk repo elemes dan velxio (keduanya ahead of origin). -- [ ] **Hapus file test lama** di root `elemes/`: - - `rm elemes/content_parser.py elemes/locustfile.py elemes/requirements-test.txt` +- [x] **Hapus file test lama** di root `elemes/`: + - `rm elemes/content_parser.py elemes/locustfile.py elemes/requirements-test.txt` (sudah dibersihkan sebelumnya, sekarang rapi di `load-test`). + ## 🟡 Prioritas Sedang - [ ] **Tuning Crosshair/UX Mobile** @@ -56,4 +53,4 @@ Berdasarkan log penyelesaian integrasi Velxio dan dokumen sebelumnya, berikut ad ## ⚪ Opsional - [x] ~~**Locust Load Testing Plan**~~ → Sudah diimplementasi di `load-test/` -- [ ] **Locust Hasil Analisis** — Setelah test jalan, analisis bottleneck compilation rate dan response time. +- [x] **Locust Hasil Analisis** — Evaluasi skenario 50 user (5 worker) selesai. *Finding*: Rata-rata respons LMS stabil di 200ms (50th percentile). Namun, kompilasi Arduino (`/velxio/api/compile/`) terdeteksi sebagai *CPU bottleneck* yang menyebabkan waktu tunggu mencapai 30 detik (95th percentile) di bawah tekanan serbuan *request* berskala ekstrem. Server/LMS dinilai layak dan responsif secara keseluruhan. diff --git a/routes/auth.py b/routes/auth.py index 2d779ba..5e8c35d 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -13,8 +13,8 @@ auth_bp = Blueprint('auth', __name__) def login(): """Handle student login with token.""" try: - data = request.get_json() - token = data.get('token', '').strip() + data = request.get_json(silent=True, force=True) or {} + token = (data.get('token') or '').strip() if not token: return jsonify({'success': False, 'message': 'Token is required'}) @@ -36,7 +36,7 @@ def login(): return jsonify({'success': False, 'message': 'Invalid token'}) except Exception as e: - return jsonify({'success': False, 'message': f'Error processing login: {e}'}) + return jsonify({'success': False, 'message': f'Error processing login: {str(e)}'}) @auth_bp.route('/logout', methods=['POST']) @@ -47,18 +47,18 @@ def logout(): response.set_cookie('student_token', '', expires=0) return response except Exception as e: - return jsonify({'success': False, 'message': f'Error processing logout: {e}'}) + return jsonify({'success': False, 'message': f'Error processing logout: {str(e)}'}) @auth_bp.route('/validate-token', methods=['POST']) def validate_token_route(): """Validate a token without logging in.""" try: - data = request.get_json() - token = data.get('token', '').strip() + data = request.get_json(silent=True, force=True) or {} + token = (data.get('token') or '').strip() if not token: - token = request.cookies.get('student_token', '').strip() + token = (request.cookies.get('student_token') or '').strip() if not token: return jsonify({'success': False, 'message': 'Token is required'}) @@ -74,4 +74,4 @@ def validate_token_route(): return jsonify({'success': False, 'message': 'Invalid token'}) except Exception as e: - return jsonify({'success': False, 'message': f'Error validating token: {e}'}) + return jsonify({'success': False, 'message': f'Error validating token: {str(e)}'})