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