225 lines
7.9 KiB
Python
225 lines
7.9 KiB
Python
"""
|
|
Unit test untuk helper sub-home di lesson_service:
|
|
|
|
- `_read_md_cached`: membaca file dengan cache berbasis mtime; hasil harus
|
|
segar bila file diubah (mtime berubah).
|
|
- `find_sub_home_for_lesson`: mendeteksi `sub-home.md` di folder induk satu
|
|
level dari sebuah file lesson (ada / tidak ada).
|
|
- `get_sub_home_data`: mem-parsing sub-home.md (title, intro, daftar lesson)
|
|
dan me-refresh hasilnya bila file berubah.
|
|
- `get_ordered_lessons_with_learning_objectives(source_path=...)`: daftar
|
|
materi diambil dari sub-home.md bila source_path diberikan, fallback home.md.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from services import lesson_service
|
|
from services.lesson_service import (
|
|
_read_md_cached,
|
|
find_sub_home_for_lesson,
|
|
get_ordered_lessons_with_learning_objectives,
|
|
get_sub_home_data,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_caches():
|
|
"""Bersihkan cache lintas test supaya hasil tidak basi antar kasus."""
|
|
yield
|
|
lesson_service._file_cache.clear()
|
|
lesson_service._sub_home_cache.clear()
|
|
lesson_service.find_lesson_file.cache_clear()
|
|
lesson_service.get_lessons.cache_clear()
|
|
lesson_service.get_lesson_names.cache_clear()
|
|
lesson_service.get_lessons_with_learning_objectives.cache_clear()
|
|
|
|
|
|
def _touch_mtime(path):
|
|
"""Paksa mtime berubah (beberapa FS punya resolusi nanodetik)."""
|
|
old = os.path.getmtime(path)
|
|
os.utime(path, (old + 5, old + 5))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _read_md_cached
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_read_md_cached_returns_content(tmp_path):
|
|
p = tmp_path / "sub-home.md"
|
|
p.write_text("# Bab Satu\n\n----Available_Lessons----\n", encoding="utf-8")
|
|
assert _read_md_cached(str(p)) == "# Bab Satu\n\n----Available_Lessons----\n"
|
|
|
|
|
|
def test_read_md_cached_refreshes_on_mtime_change(tmp_path):
|
|
p = tmp_path / "sub-home.md"
|
|
p.write_text("versi 1", encoding="utf-8")
|
|
assert _read_md_cached(str(p)) == "versi 1"
|
|
|
|
p.write_text("versi 2", encoding="utf-8")
|
|
_touch_mtime(p)
|
|
assert _read_md_cached(str(p)) == "versi 2"
|
|
|
|
|
|
def test_read_md_cached_missing_file_returns_empty(tmp_path):
|
|
assert _read_md_cached(str(tmp_path / "tidak-ada.md")) == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# find_sub_home_for_lesson
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_find_sub_home_for_lesson_found(tmp_path):
|
|
bab = tmp_path / "bab1"
|
|
bab.mkdir()
|
|
(bab / "sub-home.md").write_text("# Bab 1", encoding="utf-8")
|
|
lesson = bab / "hello.md"
|
|
lesson.write_text("# Hello", encoding="utf-8")
|
|
|
|
sub_home_path, folder_name = find_sub_home_for_lesson(str(lesson))
|
|
assert folder_name == "bab1"
|
|
assert os.path.basename(sub_home_path) == "sub-home.md"
|
|
|
|
|
|
def test_find_sub_home_for_lesson_missing(tmp_path):
|
|
bab = tmp_path / "bab1"
|
|
bab.mkdir()
|
|
lesson = bab / "hello.md"
|
|
lesson.write_text("# Hello", encoding="utf-8")
|
|
|
|
assert find_sub_home_for_lesson(str(lesson)) == (None, None)
|
|
|
|
|
|
def test_find_sub_home_for_lesson_none_path():
|
|
assert find_sub_home_for_lesson(None) == (None, None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_sub_home_data
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _write_sub_home_fixture(base, folder="bab1"):
|
|
bab = base / folder
|
|
bab.mkdir(parents=True, exist_ok=True)
|
|
(bab / "sub-home.md").write_text(
|
|
"# Bab Satu\n\nIntro bab satu.\n\n"
|
|
"----Available_Lessons----\n"
|
|
"1. [Hello, World!](lesson/hello_world.md)\n"
|
|
"2. [Variabel](lesson/variabel.md)\n"
|
|
"3. [Percabangan](lesson/percabangan.md)\n",
|
|
encoding="utf-8",
|
|
)
|
|
(bab / "hello_world.md").write_text(
|
|
"# Hello, World!\nMateri pertama.\n", encoding="utf-8"
|
|
)
|
|
(bab / "variabel.md").write_text("# Variabel\nMateri kedua.\n", encoding="utf-8")
|
|
(bab / "percabangan.md").write_text("# Percabangan\nMateri ketiga.\n", encoding="utf-8")
|
|
return bab
|
|
|
|
|
|
def test_get_sub_home_data_parses(tmp_path, monkeypatch):
|
|
_write_sub_home_fixture(tmp_path)
|
|
monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path))
|
|
|
|
data = get_sub_home_data("bab1")
|
|
assert data is not None
|
|
assert data["title"] == "Bab Satu"
|
|
assert "Intro bab satu" in data["intro_html"]
|
|
assert data["folder"] == "bab1"
|
|
assert data["url"] == "/bab/bab1"
|
|
filenames = [l["filename"] for l in data["lessons"]]
|
|
assert filenames == ["hello_world.md", "variabel.md", "percabangan.md"]
|
|
|
|
|
|
def test_get_sub_home_data_missing_folder_returns_none(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path))
|
|
assert get_sub_home_data("tidak-ada") is None
|
|
|
|
|
|
def test_get_sub_home_data_missing_file_returns_none(tmp_path, monkeypatch):
|
|
(tmp_path / "bab1").mkdir()
|
|
monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path))
|
|
assert get_sub_home_data("bab1") is None
|
|
|
|
|
|
def test_get_sub_home_data_refreshes_on_change(tmp_path, monkeypatch):
|
|
_write_sub_home_fixture(tmp_path)
|
|
monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path))
|
|
|
|
data = get_sub_home_data("bab1")
|
|
assert [l["filename"] for l in data["lessons"]] == [
|
|
"hello_world.md",
|
|
"variabel.md",
|
|
"percabangan.md",
|
|
]
|
|
|
|
# Ubah sub-home.md → panggilan berikutnya harus memakai data baru
|
|
bab = tmp_path / "bab1"
|
|
(bab / "sub-home.md").write_text(
|
|
"# Bab Satu\n\n----Available_Lessons----\n"
|
|
"1. [Hello, World!](lesson/hello_world.md)\n",
|
|
encoding="utf-8",
|
|
)
|
|
_touch_mtime(bab / "sub-home.md")
|
|
|
|
data2 = get_sub_home_data("bab1")
|
|
assert [l["filename"] for l in data2["lessons"]] == ["hello_world.md"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_ordered_lessons_with_learning_objectives(source_path=...)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ordered_lessons_source_path_uses_sub_home(tmp_path, monkeypatch):
|
|
_write_sub_home_fixture(tmp_path)
|
|
# home.md global: hanya 1 lesson agar beda dengan daftar sub-home
|
|
(tmp_path / "home.md").write_text(
|
|
"# Home\n\n----Available_Lessons----\n"
|
|
"1. [Hello, World!](lesson/hello_world.md)\n",
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path))
|
|
lesson_service.get_lessons.cache_clear()
|
|
|
|
sub_home_path = str(tmp_path / "bab1" / "sub-home.md")
|
|
lessons = get_ordered_lessons_with_learning_objectives(source_path=sub_home_path)
|
|
filenames = [l["filename"] for l in lessons]
|
|
assert filenames == ["hello_world.md", "variabel.md", "percabangan.md"]
|
|
|
|
|
|
def test_ordered_lessons_without_source_path_uses_home(tmp_path, monkeypatch):
|
|
_write_sub_home_fixture(tmp_path)
|
|
(tmp_path / "home.md").write_text(
|
|
"# Home\n\n----Available_Lessons----\n"
|
|
"1. [Hello, World!](lesson/hello_world.md)\n",
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path))
|
|
lesson_service.get_lessons.cache_clear()
|
|
|
|
lessons = get_ordered_lessons_with_learning_objectives()
|
|
filenames = [l["filename"] for l in lessons]
|
|
assert filenames == ["hello_world.md"]
|
|
|
|
|
|
def test_ordered_lessons_with_completion_status(tmp_path, monkeypatch):
|
|
_write_sub_home_fixture(tmp_path)
|
|
(tmp_path / "home.md").write_text(
|
|
"# Home\n\n----Available_Lessons----\n"
|
|
"1. [Hello, World!](lesson/hello_world.md)\n",
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path))
|
|
lesson_service.get_lessons.cache_clear()
|
|
|
|
progress = {"hello_world": "completed"}
|
|
lessons = get_ordered_lessons_with_learning_objectives(progress=progress)
|
|
assert lessons[0]["completed"] is True
|
|
assert lessons[0]["filename"] == "hello_world.md"
|