Fitur B — setiap jenis soal kini punya key sendiri sehingga model
penilaiannya eksplisit, tidak ditebak dari isi konten:
- ---QUIZ_MCQ--- pilihan ganda biner (tepat satu [x]; larang [N])
- ---QUIZ_WEIGHTED--- pilihan ganda berbobot ([N] >= 1, minimal satu [ ],
bobot tertinggi unik = kunci; poin parsial)
- ---FLASHCARD--- kartu hafalan tanpa penilaian (larang opsi)
- legacy ---QUIZ_FLASHCARD--- / ---INITIAL_QUIZ--- tetap dibaca tanpa
perubahan perilaku (regex opsi legacy sengaja tidak berubah — baris
'- [10]' di konten lama BUKAN opsi); dihapus dari docs/template
- ::diagnostic tetap kategori di dalam QUIZ_MCQ & tetap masuk skor resmi
Model skor: rata-rata persentase per jenis yang ada, Math.round,
lulus >= 75. statusString: "X/Y" bila hanya MCQ/legacy, "P/100" bila ada
weighted, "completed" bila hanya flashcard (auto-lulus). Satu sesi/
attempt untuk semua jenis; soal flat urut dokumen.
Parser (backend & frontend mirror, ekstraksi multi-blok per key urut
dokumen via loop marker-start paling awal; id q-N deterministik):
- lesson_service.py: regex [ xX]?[0-9]* utk key baru, _iter_option_lines
kind-aware, _validate_quiz_options per kind, _parse_flashcards(kind,
id_offset), _extract_quiz_sections + guard loop maju (marker end
hilang tidak lagi infinite loop), get_lesson_quiz_total -> 100 bila
ada weighted (round-trip CSV "P/100")
- parseFlashcards.ts / index.ts / detectActiveTabs.ts / extractSections.ts:
mirror perilaku; heading soal <h3> kini fence-aware (fix fixture stale)
Payload attempt baru: quiz_type, earned_points, question_max_points —
opsional di validasi backend supaya klien lama tetap diterima; poin
di-recompute dari konten saat resume/review.
UI: QuizTab skor via statusString + alert berbasis percent, badge
weighted (✓ 10 / 🟡 7/10 / ✗ 0) di review, preview guru badge bobot +
"Berbobot (total N)", laporan guru badge MCQ/Bobot + tooltip, CSV
progress 2 kolom baru per lesson (_quiz_mcq, _quiz_weighted), template
editor konten QUIZ_MCQ/Quiz Bobot/Flashcard.
Tooling & docs: scripts/convert_quiz_keys.py (konversi legacy -> key
baru per run jenis, idempoten, verifikasi parse identik; examples/
content dikonversi), scanner mendukung semua key, docs/07 ditulis ulang,
docs/15 direvisi ke desain final, README/documentation/docs 02/04/09
diperbarui.
Tes: backend 444 lulus (+33: validasi ketat per key, urutan dokumen,
multi-blok, zero regression, field selalu di-emit), frontend 249 lulus
(+13 parser key baru, +11 model skor), parity fixture diregenerasi,
svelte-check 0 error, vite build OK.
|
||
|---|---|---|
| .. | ||
| README.md | ||
| content_parser.py | ||
| e2e_session_check.py | ||
| locustfile.py | ||
| locustfile_db.py | ||
| requirements.txt | ||
| smoke_live.py | ||
| start_locust_workers.sh | ||
| stop_locust.sh | ||
README.md
Elemes Load Test
Load test E2E menggunakan Locust yang otomatis di-generate dari folder content/.
Cara Pakai
cd elemes/load-test
# 0. Virtual Environment
python3 -m venv ./env
source ./env/bin/activate
# 1. Install dependency
pip install -r requirements.txt
# 2. Generate test data dari content/ (konten ada di subfolder dasar/, arduino/, circuit/)
# Token sintetis LOCUST_TEST_* otomatis di-seed ke PostgreSQL bila env DATABASE_URL ter-set
export DATABASE_URL=postgresql+psycopg://elemes:<password>@127.0.0.1:5432/elemes
python content_parser.py --content-dir ../../content --num-tokens 50
# 3. Jalankan Locust (opsional: set VELXIO_HOST jika Velxio bukan di localhost:8001)
export VELXIO_HOST=http://localhost:8001
locust -f locustfile.py
Buka http://localhost:8089, masukkan URL backend Elemes (misalnya http://localhost:3000), lalu mulai test.
File
| File | Fungsi |
|---|---|
content_parser.py |
Parse content/**/*.md (rekursif subfolder) → test_data.json + seed token LOCUST_TEST_* ke PostgreSQL |
locustfile.py |
Locust script (task weighted) yang baca test_data.json |
test_data.json |
Auto-generated, jangan di-commit |
requirements.txt |
Dependency (locust) |
e2e_session_check.py |
Smoke-test alur interaktif (PTY session) via Flask proxy — jalan di container |
Test Scenarios (9 Tasks)
| # | Task | Weight | Target | Deskripsi |
|---|---|---|---|---|
| 1 | Browse Lessons | 3 | Elemes | GET /lessons, validasi jumlah lesson |
| 2 | View Detail | 5 | Elemes | GET /lesson/{slug}.json, validasi field per tipe |
| 3 | Compile C | 4 | Elemes | POST /compile, validasi output vs expected |
| 4 | Compile Python | 3 | Elemes | POST /compile, validasi output vs expected |
| 5 | Interactive Session Python | 4 | Elemes | POST /compile/sessions → poll prompt → POST /input → verifikasi output → DELETE |
| 6 | Verify Arduino | 2 | Elemes | GET lesson Arduino, validasi JSON structure |
| 7 | Complete Flow | 2 | Both | fetch → compile → track-progress |
| 8 | Progress Report | 1 | Elemes | Login guru → GET /progress-report.json |
| 9 | Compile Arduino | 3 | Velxio | POST /api/compile, validasi hex_content |
Task 5 (interactive session) menguji alur baru playground: Run → prompt muncul → ketik jawaban → Enter → output. Skrip memakai token (login) agar lolos rate-limit anonymous dan benar-benar mengukur kapasitas compiler worker (max 50 sesi, 2 compile bersamaan, queue timeout 20 s).
Load Test 50 Pengguna (2 Device)
Host hanya 4-core/3.5 GiB — 50 kompilasi berat bersamaan tidak realistis, tapi 50 sesi yang menunggu input aman (proses idle tidak makan CPU). Batas worker: INTERACTIVE_MAX_SESSIONS=50, INTERACTIVE_MAX_COMPILES=2, INTERACTIVE_QUEUE_TIMEOUT_SECONDS=20.
Device A — Locust Master (jalankan UI di sini)
cd elemes/load-test
source ./env/bin/activate
locust -f locustfile.py --master --web-port 8089
Device B (atau beberapa device) — Locust Worker
cd elemes/load-test
source ./env/bin/activate
# Ganti <IP_MASTER> dengan IP device A (di jaringan/Tailscale yang sama)
locust -f locustfile.py --worker --master-host <IP_MASTER>
Mulai Test
- Buka
http://<IP_MASTER>:8089di browser device A. - Host: URL app yang diuji, misal
https://sinau-c-dev.manakin-gentoo.ts.net(Tailscale Funnel) atauhttp://localhost:3000. - Number of users: 50 · Spawn rate: 5–10/s (ramp-up ~5–10 detik agar hampir bersamaan) · Duration: 5–10 menit.
- Amati di tab Charts: RPS per endpoint, response time
/compile/sessions&/input, dan failure rate. - Skenario
/compile/sessions [Python interactive]wajib ≥95% sukses; failure429pada create menandakan anon rate limit (bukan kapasitas) — gunakan token.
Catatan: task compile biasa (
/compile) tetap kena anon rate limit1 per 2 menitbila token tidak ikut — login dion_startmenyimpan cookie, jadi user ber-token exempt.
Re-generate Setelah Tambah Lesson Baru
Setiap kali ada lesson baru di content/, cukup jalankan ulang:
python content_parser.py --content-dir ../../content --num-tokens 50
test_data.json akan di-update otomatis dan Locust langsung test lesson baru.
E2E Smoke Test (container)
Verifikasi cepat alur interaktif tanpa browser — jalan di dalam container backend:
# Nama container mengikuti folder deploy (PROJECT_NAME = basename folder
# parent, mis. lms-dev → lms-dev_elemes_1) — resolve via label project+service,
# jangan hard-code nama container:
ELEMES_CTN=$(podman ps -q \
--filter "label=io.podman.compose.project=<PROJECT_NAME>" \
--filter "label=com.docker.compose.service=elemes")
podman cp elemes/load-test/e2e_session_check.py "$ELEMES_CTN":/tmp/
podman exec -e E2E_TOKEN=<token_siswa> "$ELEMES_CTN" python3 /tmp/e2e_session_check.py
# Contoh token: LOCUST_TEST_xxxx — token sintetis yang di-seed ke PostgreSQL
# oleh content_parser.py (jalankan langkah 2 dengan DATABASE_URL ter-set).
# <PROJECT_NAME> = nama folder deploy (mis. lms-dev / sinau-c).
Tanpa E2E_TOKEN skrip tetap jalan tapi hanya 2 create session (anon rate limit 1/2 menit).