"""Loading and validating the typing game's curriculum file.""" from __future__ import annotations from pathlib import Path import pytest from ruamel.yaml import YAML from musicmouse.tippen.curriculum import CurriculumError, load_curriculum VALID: dict = { "worlds": [ { "number": 1, "title": "Riff", "emoji": "🐠", "reward": "clownfish", "lessons": [ {"title": "A und S", "subtitle": "Die Startfinger", "keys": ["a", "s"]}, { "title": "Wörter", "subtitle": "Kurze Wörter", "kind": "words", "words": ["as", "sass"], }, ], }, { "number": 2, "title": "Höhle", "emoji": "🐙", "reward": "octopus", "lessons": [{"title": "D und F", "subtitle": "Weiter", "keys": ["d", "f"]}], }, { "number": 3, "title": "Riff2", "emoji": "🦑", "reward": "seahorse", "lessons": [{"title": "J und K", "subtitle": "Weiter", "keys": ["j", "k"]}], }, { "number": 4, "title": "Riff3", "emoji": "🐳", "reward": "turtle", "lessons": [{"title": "L", "subtitle": "Weiter", "keys": ["l"]}], }, { "number": 5, "title": "Riff4", "emoji": "🧜", "reward": "pearlmussel", "lessons": [{"title": "Ö", "subtitle": "Weiter", "keys": ["ö"]}], }, ] } def _write(directory: Path, data: dict, name: str = "curriculum.yaml") -> Path: path = directory / name with path.open("w", encoding="utf-8") as handle: YAML(typ="safe").dump(data, handle) return path def test_a_valid_curriculum_loads(tmp_path: Path) -> None: curriculum = load_curriculum(_write(tmp_path, VALID)) assert [w.reward for w in curriculum.worlds] == [ "clownfish", "octopus", "seahorse", "turtle", "pearlmussel", ] assert len(curriculum.lessons) == 6 assert curriculum.lessons[0].id == "l01" assert curriculum.lessons[0].new_keys == ("a", "s") assert curriculum.lessons[1].kind == "words" assert curriculum.lessons[1].words == ("as", "sass") def test_missing_file_is_a_curriculum_error(tmp_path: Path) -> None: with pytest.raises(CurriculumError, match="Cannot read"): load_curriculum(tmp_path / "no-such-file.yaml") def test_not_yaml_is_a_curriculum_error(tmp_path: Path) -> None: path = tmp_path / "curriculum.yaml" path.write_text("worlds: [this is not: valid: yaml", encoding="utf-8") with pytest.raises(CurriculumError, match="not valid YAML"): load_curriculum(path) def test_unknown_key_is_rejected(tmp_path: Path) -> None: data = {"worlds": VALID["worlds"], "extra_top_level_key": True} with pytest.raises(CurriculumError, match="extra_top_level_key"): load_curriculum(_write(tmp_path, data)) def test_wrong_world_count_is_one_problem(tmp_path: Path) -> None: data = {"worlds": VALID["worlds"][:4]} with pytest.raises(CurriculumError, match="expected 5 worlds, found 4"): load_curriculum(_write(tmp_path, data)) def test_multiple_problems_are_all_reported_at_once(tmp_path: Path) -> None: worlds = [dict(w) for w in VALID["worlds"]] # Two independent problems in one file: a reused reward, and a lesson with too many # keys - both should show up in a single error, not just the first one found. worlds[1] = {**worlds[1], "reward": "clownfish"} worlds[2] = { **worlds[2], "lessons": [{"title": "Zu viel", "subtitle": "x", "keys": ["a", "s", "d"]}], } with pytest.raises(CurriculumError) as excinfo: load_curriculum(_write(tmp_path, {"worlds": worlds})) message = str(excinfo.value) assert "2 problems" in message assert "reuses reward" in message assert "at most two keys" in message def test_letters_kind_cannot_have_words(tmp_path: Path) -> None: worlds = [dict(w) for w in VALID["worlds"]] worlds[0] = { **worlds[0], "lessons": [{"title": "x", "subtitle": "x", "words": ["hallo"]}], } with pytest.raises(CurriculumError, match="has words but no explicit kind"): load_curriculum(_write(tmp_path, {"worlds": worlds})) def test_non_letters_kind_needs_words(tmp_path: Path) -> None: worlds = [dict(w) for w in VALID["worlds"]] worlds[0] = { **worlds[0], "lessons": [{"title": "x", "subtitle": "x", "kind": "words"}], } with pytest.raises(CurriculumError, match='kind "words" needs a non-empty words list'): load_curriculum(_write(tmp_path, {"worlds": worlds})) def test_unlocks_is_carried_through_unresolved(tmp_path: Path) -> None: worlds = [dict(w) for w in VALID["worlds"]] lessons = [dict(lesson) for lesson in worlds[0]["lessons"]] lessons[0] = {**lessons[0], "unlocks": "~/Music/Musik/Album/05Track.mp3"} worlds[0] = {**worlds[0], "lessons": lessons} curriculum = load_curriculum(_write(tmp_path, {"worlds": worlds})) assert curriculum.lessons[0].unlocks == "~/Music/Musik/Album/05Track.mp3" assert curriculum.lessons[1].unlocks is None