Files
sunnypilot/docs_sp/tools/test_nav_parser.py
T
Jason Wen 922ad64d13 sync
2026-03-12 21:46:34 -04:00

239 lines
7.2 KiB
Python

#!/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)