2026-08-08 11:57:08 +07:00
|
|
|
"""
|
|
|
|
|
Locust scenario khusus migrasi database (CSV → PostgreSQL).
|
|
|
|
|
|
|
|
|
|
Menargetkan endpoint yang persistence-nya berubah:
|
|
|
|
|
- /api/login + /api/validate-token (identitas: CSV → PG users/access_tokens)
|
|
|
|
|
- /api/track-progress (write progress: CSV → PG student_progress)
|
|
|
|
|
- /api/progress-report.json (report guru — baca semua siswa)
|
|
|
|
|
|
|
|
|
|
Data token diambil dari test_data.json (hasilkan dulu:
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
python content_parser.py --content-dir ../../content --num-tokens 50
|
|
|
|
|
→ token sintetis LOCUST_TEST_* di-seed ke PostgreSQL bila DATABASE_URL ter-set)
|
2026-08-08 11:57:08 +07:00
|
|
|
|
|
|
|
|
Usage (dari elemes/load-test/):
|
|
|
|
|
locust -f locustfile_db.py
|
|
|
|
|
→ set host ke URL Elemes (mis. https://sinau-c-dev.manakin-gentoo.ts.net)
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import random
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from locust import HttpUser, between, events, task
|
|
|
|
|
|
|
|
|
|
TEST_DATA_FILE = Path(__file__).parent / 'test_data.json'
|
|
|
|
|
API = os.environ.get('API_PREFIX', '/api')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_test_data() -> dict:
|
|
|
|
|
if not TEST_DATA_FILE.exists():
|
|
|
|
|
raise FileNotFoundError(
|
|
|
|
|
f'{TEST_DATA_FILE} tidak ada. Jalankan: python content_parser.py'
|
|
|
|
|
)
|
|
|
|
|
with open(TEST_DATA_FILE, 'r', encoding='utf-8') as f:
|
|
|
|
|
return json.load(f)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
TEST_DATA = load_test_data()
|
|
|
|
|
except FileNotFoundError as e:
|
|
|
|
|
logging.error(str(e))
|
|
|
|
|
TEST_DATA = {'tokens': [], 'teacher_token': '', 'lessons': []}
|
|
|
|
|
|
|
|
|
|
STUDENT_TOKENS = [t for t in TEST_DATA.get('tokens', []) if t != TEST_DATA.get('teacher_token')]
|
|
|
|
|
TEACHER_TOKEN = TEST_DATA.get('teacher_token', '')
|
|
|
|
|
LESSON_SLUGS = [l['filename'].replace('.md', '') for l in TEST_DATA.get('lessons', [])]
|
|
|
|
|
|
|
|
|
|
if not STUDENT_TOKENS:
|
|
|
|
|
logging.warning('test_data.json tidak punya token siswa — hanya teacher')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StudentUser(HttpUser):
|
|
|
|
|
"""Siswa: login → baca lesson → track progress (random status)."""
|
|
|
|
|
|
|
|
|
|
wait_time = between(1, 4)
|
|
|
|
|
|
|
|
|
|
def on_start(self):
|
|
|
|
|
self.token = random.choice(STUDENT_TOKENS) if STUDENT_TOKENS else ''
|
|
|
|
|
if not self.token:
|
|
|
|
|
return
|
|
|
|
|
self.client.post(f'{API}/login', json={'token': self.token})
|
|
|
|
|
|
|
|
|
|
@task(2)
|
|
|
|
|
def validate_token(self):
|
|
|
|
|
self.client.post(f'{API}/validate-token', json={'token': self.token})
|
|
|
|
|
|
|
|
|
|
@task(3)
|
|
|
|
|
def read_lesson(self):
|
|
|
|
|
if not LESSON_SLUGS:
|
|
|
|
|
return
|
|
|
|
|
slug = random.choice(LESSON_SLUGS)
|
|
|
|
|
self.client.get(f'/bab/dasar-pemrograman/{slug}', name='/bab/.../{slug}')
|
|
|
|
|
|
|
|
|
|
@task(3)
|
|
|
|
|
def track_progress(self):
|
|
|
|
|
if not LESSON_SLUGS:
|
|
|
|
|
return
|
|
|
|
|
slug = random.choice(LESSON_SLUGS)
|
|
|
|
|
status = random.choice(['completed', 'not_started', '3/4', '2/5'])
|
|
|
|
|
self.client.post(
|
|
|
|
|
f'{API}/track-progress',
|
|
|
|
|
json={'token': self.token, 'lesson_name': slug, 'status': status},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TeacherUser(HttpUser):
|
|
|
|
|
"""Guru: login → report keseluruhan (beban baca berat di PG)."""
|
|
|
|
|
|
|
|
|
|
wait_time = between(3, 8)
|
|
|
|
|
|
|
|
|
|
def on_start(self):
|
|
|
|
|
if TEACHER_TOKEN:
|
|
|
|
|
self.client.post(f'{API}/login', json={'token': TEACHER_TOKEN})
|
|
|
|
|
|
|
|
|
|
@task(1)
|
|
|
|
|
def progress_report(self):
|
|
|
|
|
if TEACHER_TOKEN:
|
|
|
|
|
self.client.get(f'{API}/progress-report.json?token={TEACHER_TOKEN}')
|
|
|
|
|
|
|
|
|
|
@task(1)
|
|
|
|
|
def export_csv(self):
|
|
|
|
|
if TEACHER_TOKEN:
|
|
|
|
|
self.client.get(f'{API}/progress-report/export-csv?token={TEACHER_TOKEN}')
|