#!/usr/bin/env python3 """Tests for the zensical.toml nav parser. Run: python3 docs_sp/tools/test_nav_parser.py """ import sys import tempfile from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from nav_parser import NavEntry, parse, parse_all, _flatten_nav ZENSICAL_TOML = Path(__file__).resolve().parent.parent.parent / "zensical.toml" # --------------------------------------------------------------------------- # Unit tests: _flatten_nav # --------------------------------------------------------------------------- def test_flatten_simple_dict(): nav = [{"Page One": "page-one.md"}] result = _flatten_nav(nav) assert len(result) == 1 assert result[0] == NavEntry(title="Page One", path="page-one.md", breadcrumb=("Page One",)) print(" PASS: flatten_simple_dict") def test_flatten_nested(): nav = [ {"Section": [ {"Child A": "section/a.md"}, {"Child B": "section/b.md"}, ]}, ] result = _flatten_nav(nav) assert len(result) == 2 assert result[0].title == "Child A" assert result[0].path == "section/a.md" assert result[0].breadcrumb == ("Section", "Child A") assert result[1].breadcrumb == ("Section", "Child B") print(" PASS: flatten_nested") def test_flatten_deep_nesting(): nav = [ {"L1": [ {"L2": [ {"L3": "deep/page.md"}, ]}, ]}, ] result = _flatten_nav(nav) assert len(result) == 1 assert result[0].breadcrumb == ("L1", "L2", "L3") assert result[0].path == "deep/page.md" print(" PASS: flatten_deep_nesting") def test_flatten_skips_external_links(): nav = [ {"Docs": "docs.md"}, {"Forum": "https://community.sunnypilot.ai"}, ] result = _flatten_nav(nav) assert len(result) == 1 assert result[0].title == "Docs" print(" PASS: flatten_skips_external_links") def test_flatten_bare_string(): nav = ["getting-started/index.md"] result = _flatten_nav(nav) assert len(result) == 1 assert result[0].title == "Index" assert result[0].path == "getting-started/index.md" print(" PASS: flatten_bare_string") def test_flatten_mixed(): nav = [ {"Home": "index.md"}, {"Features": [ "features/index.md", {"ICBM": "features/cruise/icbm.md"}, {"Forum": "https://example.com"}, ]}, ] result = _flatten_nav(nav) assert len(result) == 3 # Home, features/index.md (bare), ICBM titles = [e.title for e in result] assert "Home" in titles assert "ICBM" in titles assert "Forum" not in titles print(" PASS: flatten_mixed") # --------------------------------------------------------------------------- # Unit tests: parse (filters index.md/README.md) # --------------------------------------------------------------------------- def test_parse_filters_index(): """parse() should exclude index.md and README.md entries.""" toml_content = b""" [project] nav = [ {"Home" = "index.md"}, {"Guide" = [ "guide/index.md", {"Setup" = "guide/setup.md"}, ]}, ] """ with tempfile.NamedTemporaryFile(suffix=".toml", delete=False) as f: f.write(toml_content) f.flush() result = parse(f.name) assert len(result) == 1 assert result[0].title == "Setup" assert result[0].path == "guide/setup.md" print(" PASS: parse_filters_index") def test_parse_all_includes_index(): """parse_all() should include index.md entries.""" toml_content = b""" [project] nav = [ {"Home" = "index.md"}, {"Setup" = "guide/setup.md"}, ] """ with tempfile.NamedTemporaryFile(suffix=".toml", delete=False) as f: f.write(toml_content) f.flush() result = parse_all(f.name) assert len(result) == 2 titles = [e.title for e in result] assert "Home" in titles assert "Setup" in titles print(" PASS: parse_all_includes_index") # --------------------------------------------------------------------------- # Integration: parse real zensical.toml # --------------------------------------------------------------------------- def test_parse_real_zensical(): """Parse the actual zensical.toml and verify structure.""" if not ZENSICAL_TOML.exists(): print(" SKIP: parse_real_zensical (zensical.toml not found)") return all_entries = parse_all(ZENSICAL_TOML) filtered_entries = parse(ZENSICAL_TOML) # Sanity checks on totals assert len(all_entries) > 50, f"Expected 50+ total entries, got {len(all_entries)}" assert len(filtered_entries) > 40, f"Expected 40+ filtered entries, got {len(filtered_entries)}" assert len(filtered_entries) < len(all_entries), "Filtering should remove some entries" # Every filtered entry should NOT be index.md or README.md for entry in filtered_entries: assert Path(entry.path).name not in ("index.md", "README.md"), ( f"Filtered list contains {entry.path}" ) # No external links should be present for entry in all_entries: assert not entry.path.startswith("http"), f"External link leaked: {entry.path}" # Check some known entries exist paths = {e.path for e in filtered_entries} assert "getting-started/what-is-sunnypilot.md" in paths, "Missing what-is-sunnypilot" assert "features/cruise/icbm.md" in paths, "Missing ICBM" assert "safety/safety.md" in paths, "Missing safety" # Check breadcrumbs are populated icbm = next(e for e in filtered_entries if e.path == "features/cruise/icbm.md") assert len(icbm.breadcrumb) >= 2, f"ICBM breadcrumb too short: {icbm.breadcrumb}" assert "Features" in icbm.breadcrumb or "Cruise Control" in icbm.breadcrumb print(f" PASS: parse_real_zensical ({len(all_entries)} total, {len(filtered_entries)} filtered)") def test_nav_entry_immutable(): """NavEntry is frozen — attributes cannot be reassigned.""" entry = NavEntry(title="Test", path="test.md", breadcrumb=("Test",)) try: entry.title = "Modified" # type: ignore[misc] assert False, "Should have raised FrozenInstanceError" except AttributeError: pass print(" PASS: nav_entry_immutable") # --------------------------------------------------------------------------- # Runner # --------------------------------------------------------------------------- if __name__ == "__main__": print("Testing nav parser:") tests = [ # Unit test_flatten_simple_dict, test_flatten_nested, test_flatten_deep_nesting, test_flatten_skips_external_links, test_flatten_bare_string, test_flatten_mixed, test_parse_filters_index, test_parse_all_includes_index, test_nav_entry_immutable, # Integration test_parse_real_zensical, ] passed = 0 failed = 0 for test in tests: try: test() passed += 1 except AssertionError as e: print(f" FAIL: {test.__name__}: {e}") failed += 1 except Exception as e: print(f" ERROR: {test.__name__}: {e}") failed += 1 print(f"\n{passed}/{passed + failed} tests passed") sys.exit(1 if failed > 0 else 0)