Coverage for tests/test_code_blocks.py: 100%
48 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 14:55 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 14:55 +0000
1"""Tests that validate code blocks embedded in the lesson and slide Markdown files.
3Each YAML and TOML fenced code block is extracted and parsed. A parse error
4means the snippet shown to learners is broken, so we want to catch that early.
5"""
7from __future__ import annotations
9import re
10import tomllib
11from pathlib import Path
13import pytest
14import yaml
16REPO_ROOT = Path(__file__).parent.parent
17MARKDOWN_DIRS = [REPO_ROOT / "lessons", REPO_ROOT / "slides"]
19# Matches any fenced code block, capturing the language tag and body.
20_FENCE_RE = re.compile(r"```(\w+)\n(.*?)```", re.DOTALL)
23def _collect_all_blocks() -> tuple[list[tuple[str, int, str]], list[tuple[str, int, str]]]:
24 """Walk every Markdown file once and return (yaml_blocks, toml_blocks).
26 Each element is a list of (relative_path, block_index, block_text) tuples
27 where block_index counts blocks of that language within the file.
28 """
29 yaml_blocks: list[tuple[str, int, str]] = []
30 toml_blocks: list[tuple[str, int, str]] = []
31 yaml_counters: dict[str, int] = {}
32 toml_counters: dict[str, int] = {}
34 for directory in MARKDOWN_DIRS:
35 for md_file in sorted(directory.rglob("*.md")):
36 rel = str(md_file.relative_to(REPO_ROOT))
37 text = md_file.read_text(encoding="utf-8")
38 for lang, body in _FENCE_RE.findall(text):
39 if lang in ("yaml", "yml"):
40 yaml_counters[rel] = yaml_counters.get(rel, 0) + 1
41 yaml_blocks.append((rel, yaml_counters[rel], body))
42 elif lang == "toml":
43 toml_counters[rel] = toml_counters.get(rel, 0) + 1
44 toml_blocks.append((rel, toml_counters[rel], body))
46 return yaml_blocks, toml_blocks
49def _block_id(val: tuple[str, int, str]) -> str:
50 path, idx, _ = val
51 return f"{path}::block{idx}"
54YAML_BLOCKS, TOML_BLOCKS = _collect_all_blocks()
57@pytest.mark.parametrize("path,idx,block", YAML_BLOCKS, ids=[_block_id(b) for b in YAML_BLOCKS])
58def test_yaml_block_is_valid(path: str, idx: int, block: str) -> None:
59 """Every YAML code block in the lessons/slides must be syntactically valid."""
60 try:
61 yaml.safe_load(block)
62 except yaml.YAMLError as exc:
63 pytest.fail(f"{path} block {idx}: invalid YAML — {exc}")
66@pytest.mark.parametrize("path,idx,block", TOML_BLOCKS, ids=[_block_id(b) for b in TOML_BLOCKS])
67def test_toml_block_is_valid(path: str, idx: int, block: str) -> None:
68 """Every TOML code block in the lessons/slides must be syntactically valid."""
69 try:
70 tomllib.loads(block)
71 except tomllib.TOMLDecodeError as exc:
72 pytest.fail(f"{path} block {idx}: invalid TOML — {exc}")
75# The two checks above only report a failure when a lesson contains a broken
76# block, which — the lessons being correct — never happens on a passing run.
77# That leaves the reporting itself unexercised: a typo in the message, or an
78# `except` clause naming the wrong error, would go unnoticed until the day a
79# lesson actually breaks and the check either says nothing useful or blows up.
80# So feed each check a deliberately broken block and assert on what it says.
83def test_broken_yaml_block_is_reported_with_its_location() -> None:
84 """An invalid YAML block fails the check, naming the file and block index."""
85 with pytest.raises(pytest.fail.Exception, match=r"lessons/broken\.md block 7: invalid YAML"):
86 test_yaml_block_is_valid("lessons/broken.md", 7, "key: [unclosed\n")
89def test_broken_toml_block_is_reported_with_its_location() -> None:
90 """An invalid TOML block fails the check, naming the file and block index."""
91 with pytest.raises(pytest.fail.Exception, match=r"slides/broken\.md block 3: invalid TOML"):
92 test_toml_block_is_valid("slides/broken.md", 3, "key = \n")