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

1"""Tests that validate code blocks embedded in the lesson and slide Markdown files. 

2 

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

6 

7from __future__ import annotations 

8 

9import re 

10import tomllib 

11from pathlib import Path 

12 

13import pytest 

14import yaml 

15 

16REPO_ROOT = Path(__file__).parent.parent 

17MARKDOWN_DIRS = [REPO_ROOT / "lessons", REPO_ROOT / "slides"] 

18 

19# Matches any fenced code block, capturing the language tag and body. 

20_FENCE_RE = re.compile(r"```(\w+)\n(.*?)```", re.DOTALL) 

21 

22 

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

25 

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] = {} 

33 

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

45 

46 return yaml_blocks, toml_blocks 

47 

48 

49def _block_id(val: tuple[str, int, str]) -> str: 

50 path, idx, _ = val 

51 return f"{path}::block{idx}" 

52 

53 

54YAML_BLOCKS, TOML_BLOCKS = _collect_all_blocks() 

55 

56 

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

64 

65 

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

73 

74 

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. 

81 

82 

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

87 

88 

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