elemes/services/tests/test_lesson_list_cache.py

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"
)