100 lines
3.5 KiB
Python
100 lines
3.5 KiB
Python
"""Unit: TTL cache daftar lesson — menyegarkan diri tanpa restart.
|
|
|
|
Produksi gunicorn multi-worker: `invalidate_lesson_caches()` hanya membersihkan
|
|
cache di worker yang menangani publish. Worker lain harus menyegarkan diri
|
|
sendiri setelah TTL, agar perubahan home.md/sub-home.md tidak "nyangkut"
|
|
sampai proses di-restart.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from services import lesson_service
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
def _write_home(tmp_path, slugs, bump=0.0):
|
|
"""Tulis home.md + file lesson untuk daftar slug. `bump` menggeser mtime
|
|
agar perubahan file selalu terdeteksi (mtime presisi sub-detik)."""
|
|
lines = ["# Home", "", "---Available_Lessons---"]
|
|
for slug in slugs:
|
|
(tmp_path / f"{slug}.md").write_text(f"# {slug}\nisi", encoding="utf-8")
|
|
lines.append(f"- [{slug}]({slug}.md)")
|
|
home = tmp_path / "home.md"
|
|
home.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
if bump:
|
|
stamp = time.time() + bump
|
|
os.utime(home, (stamp, stamp))
|
|
|
|
|
|
def _filenames():
|
|
return [lesson["filename"] for lesson in lesson_service.get_lessons()]
|
|
|
|
|
|
def test_list_cache_refreshes_after_ttl(tmp_path, monkeypatch):
|
|
_write_home(tmp_path, ["alpha"])
|
|
monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path))
|
|
lesson_service.invalidate_lesson_caches()
|
|
|
|
assert _filenames() == ["alpha.md"]
|
|
|
|
# home.md berubah, tetapi masih di dalam TTL → cache lama tetap dipakai.
|
|
_write_home(tmp_path, ["alpha", "beta"], bump=1)
|
|
assert _filenames() == ["alpha.md"]
|
|
|
|
# TTL habis → worker mem-parse ulang tanpa invalidasi eksplisit.
|
|
monkeypatch.setattr("services.lesson_service._LESSON_LIST_CACHE_TTL_SECONDS", 0.0)
|
|
assert _filenames() == ["alpha.md", "beta.md"]
|
|
|
|
|
|
def test_explicit_cache_clear_still_works(tmp_path, monkeypatch):
|
|
_write_home(tmp_path, ["alpha"])
|
|
monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path))
|
|
lesson_service.invalidate_lesson_caches()
|
|
|
|
assert _filenames() == ["alpha.md"]
|
|
|
|
# In-process publish boleh membersihkan cache lebih awal dari TTL.
|
|
_write_home(tmp_path, ["alpha", "beta"], bump=1)
|
|
lesson_service.invalidate_lesson_caches()
|
|
assert _filenames() == ["alpha.md", "beta.md"]
|
|
|
|
|
|
# ── Signature evaluasi (deteksi perubahan saat publish) ─────────────
|
|
|
|
_EXERCISE = "# L\n\n---INITIAL_CODE---\n```c\nint main(){}\n```\n"
|
|
_MCQ = "\n---QUIZ_MCQ---\n### Q?\n- [x] A\n- [] B\n---END_QUIZ_MCQ---\n"
|
|
_WEIGHTED = "\n---QUIZ_WEIGHTED---\n### Q?\n- [10] A\n- [ ] B\n---END_QUIZ_WEIGHTED---\n"
|
|
|
|
|
|
def test_evaluation_signature_detects_quiz_type_change():
|
|
base = lesson_service.get_evaluation_signature(_EXERCISE)
|
|
assert base == {
|
|
"has_exercise": True,
|
|
"has_quiz": False,
|
|
"quiz_kinds": [],
|
|
"quiz_total": 0,
|
|
}
|
|
|
|
with_mcq = lesson_service.get_evaluation_signature(_EXERCISE + _MCQ)
|
|
assert with_mcq["has_quiz"] is True
|
|
assert with_mcq["quiz_kinds"] == ["mcq"]
|
|
assert with_mcq["quiz_total"] == 1
|
|
|
|
with_weighted = lesson_service.get_evaluation_signature(_EXERCISE + _WEIGHTED)
|
|
assert with_weighted["quiz_kinds"] == ["weighted"]
|
|
assert with_weighted["quiz_total"] == 100
|
|
|
|
# MCQ → weighted harus terdeteksi sebagai perubahan.
|
|
assert with_mcq != with_weighted
|
|
|
|
|
|
def test_evaluation_signature_invalid_quiz_raises():
|
|
with pytest.raises(ValueError):
|
|
lesson_service.get_evaluation_signature(
|
|
"# L\n\n---QUIZ_MCQ---\n### Q?\n- [x] A\n- [x] B\n---END_QUIZ_MCQ---\n"
|
|
)
|