fix: improve JSON parsing robustness in auth routes and update Locust load test scripts with worker management utilities.

master v2.5
a2nr 2026-04-17 10:50:46 +07:00
parent 7c069660f6
commit 89f0967c3e
5 changed files with 64 additions and 30 deletions

View File

@ -308,7 +308,7 @@ class ElemesStudent(HttpUser):
def compile_arduino_lesson(self): def compile_arduino_lesson(self):
""" """
Compile an Arduino lesson's code via Velxio backend. 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. Validates: compile success + hex_content returned.
Note: serial output can only be validated in-browser (avr8js sim). Note: serial output can only be validated in-browser (avr8js sim).
""" """
@ -321,8 +321,11 @@ class ElemesStudent(HttpUser):
return return
with self.client.post( with self.client.post(
'/velxio/api/compile', '/velxio/api/compile/',
json={'code': code, 'board_fqbn': 'arduino:avr:uno'}, json={
'files': [{'name': 'sketch.ino', 'content': code}],
'board_fqbn': 'arduino:avr:uno'
},
name='/velxio/api/compile [Arduino]', name='/velxio/api/compile [Arduino]',
timeout=30, timeout=30,
catch_response=True catch_response=True
@ -331,10 +334,14 @@ class ElemesStudent(HttpUser):
if not data.get('success'): if not data.get('success'):
# Report as Locust failure for stats tracking # Report as Locust failure for stats tracking
resp.failure( # Fallback to checking 'detail' for HTTP 422 validations
f"{lesson['slug']}: compile failed — " err_msg = data.get('error', data.get('stderr'))
f"{data.get('error', data.get('stderr', 'unknown'))}" 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 return
hex_content = data.get('hex_content', '') hex_content = data.get('hex_content', '')
@ -383,9 +390,11 @@ class ElemesStudent(HttpUser):
elif lesson['type'] == 'arduino' and lesson.get('initial_code_arduino'): elif lesson['type'] == 'arduino' and lesson.get('initial_code_arduino'):
self.client.post( self.client.post(
'/velxio/api/compile', '/velxio/api/compile/',
json={'code': lesson['initial_code_arduino'], json={
'board_fqbn': 'arduino:avr:uno'}, 'files': [{'name': 'sketch.ino', 'content': lesson['initial_code_arduino']}],
'board_fqbn': 'arduino:avr:uno'
},
name='/velxio/api/compile (flow)', name='/velxio/api/compile (flow)',
timeout=30 timeout=30
) )

View File

@ -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'."

4
load-test/stop_locust.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
echo "Mematikan server dan seluruh worker Locust..."
pkill -f "locust"
echo "Locust berhasil dihentikan."

View File

@ -30,17 +30,14 @@ Berdasarkan log penyelesaian integrasi Velxio dan dokumen sebelumnya, berikut ad
- URL backend dikonfigurasi user melalui Locust web UI - URL backend dikonfigurasi user melalui Locust web UI
## 🔴 Prioritas Tinggi ## 🔴 Prioritas Tinggi
- [ ] **Testing End-to-End (E2E)** - [x] **Testing End-to-End (E2E)**
- Script Locust sudah siap, tinggal jalankan: - 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.
cd elemes/load-test && python content_parser.py && locust -f locustfile.py - [x] **Push ke Remote**
```
- 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**
- `git push` untuk repo elemes dan velxio (keduanya ahead of origin). - `git push` untuk repo elemes dan velxio (keduanya ahead of origin).
- [ ] **Hapus file test lama** di root `elemes/`: - [x] **Hapus file test lama** di root `elemes/`:
- `rm elemes/content_parser.py elemes/locustfile.py elemes/requirements-test.txt` - `rm elemes/content_parser.py elemes/locustfile.py elemes/requirements-test.txt` (sudah dibersihkan sebelumnya, sekarang rapi di `load-test`).
## 🟡 Prioritas Sedang ## 🟡 Prioritas Sedang
- [ ] **Tuning Crosshair/UX Mobile** - [ ] **Tuning Crosshair/UX Mobile**
@ -56,4 +53,4 @@ Berdasarkan log penyelesaian integrasi Velxio dan dokumen sebelumnya, berikut ad
## ⚪ Opsional ## ⚪ Opsional
- [x] ~~**Locust Load Testing Plan**~~ → Sudah diimplementasi di `load-test/` - [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.

View File

@ -13,8 +13,8 @@ auth_bp = Blueprint('auth', __name__)
def login(): def login():
"""Handle student login with token.""" """Handle student login with token."""
try: try:
data = request.get_json() data = request.get_json(silent=True, force=True) or {}
token = data.get('token', '').strip() token = (data.get('token') or '').strip()
if not token: if not token:
return jsonify({'success': False, 'message': 'Token is required'}) return jsonify({'success': False, 'message': 'Token is required'})
@ -36,7 +36,7 @@ def login():
return jsonify({'success': False, 'message': 'Invalid token'}) return jsonify({'success': False, 'message': 'Invalid token'})
except Exception as e: 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']) @auth_bp.route('/logout', methods=['POST'])
@ -47,18 +47,18 @@ def logout():
response.set_cookie('student_token', '', expires=0) response.set_cookie('student_token', '', expires=0)
return response return response
except Exception as e: 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']) @auth_bp.route('/validate-token', methods=['POST'])
def validate_token_route(): def validate_token_route():
"""Validate a token without logging in.""" """Validate a token without logging in."""
try: try:
data = request.get_json() data = request.get_json(silent=True, force=True) or {}
token = data.get('token', '').strip() token = (data.get('token') or '').strip()
if not token: if not token:
token = request.cookies.get('student_token', '').strip() token = (request.cookies.get('student_token') or '').strip()
if not token: if not token:
return jsonify({'success': False, 'message': 'Token is required'}) return jsonify({'success': False, 'message': 'Token is required'})
@ -74,4 +74,4 @@ def validate_token_route():
return jsonify({'success': False, 'message': 'Invalid token'}) return jsonify({'success': False, 'message': 'Invalid token'})
except Exception as e: 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)}'})