Files
Jason Wen c8d543a8fe more
2026-03-12 22:38:21 -04:00

503 lines
14 KiB
Python

#!/usr/bin/env python3
"""Tests for the MkDocs -> Discourse markdown converter.
Run: python3 docs_sp/tools/test_converter.py
"""
import sys
from pathlib import Path
# Ensure the tools directory is importable
sys.path.insert(0, str(Path(__file__).parent))
from converter import (
clean_blank_lines,
convert,
convert_admonitions,
convert_emoji_shortcodes,
convert_grid_cards,
convert_tabs,
resolve_internal_links,
strip_front_matter,
)
# ---------------------------------------------------------------------------
# 1. Strip YAML Front Matter
# ---------------------------------------------------------------------------
def test_strip_front_matter_basic():
input_text = """---
title: My Page
description: A test page
---
# Hello
"""
expected = """# Hello
"""
result = strip_front_matter(input_text)
assert result == expected, f"FAIL:\n{result!r}\n!=\n{expected!r}"
print(" PASS: strip_front_matter_basic")
def test_strip_front_matter_absent():
input_text = "# Hello\n\nContent here.\n"
result = strip_front_matter(input_text)
assert result == input_text, f"FAIL:\n{result!r}\n!=\n{input_text!r}"
print(" PASS: strip_front_matter_absent")
# ---------------------------------------------------------------------------
# 2. Convert Admonitions
# ---------------------------------------------------------------------------
def test_basic_warning():
input_text = """!!! warning "Important"
sunnypilot is a **driver assistance** system.
Always pay attention.
"""
expected = """> [!WARNING] Important
> sunnypilot is a **driver assistance** system.
> Always pay attention.
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL basic_warning:\n{result!r}\n!=\n{expected!r}"
print(" PASS: basic_warning")
def test_info_no_title():
input_text = """!!! info
Content line 1
Content line 2
"""
expected = """> [!INFO]
> Content line 1
> Content line 2
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL info_no_title:\n{result!r}\n!=\n{expected!r}"
print(" PASS: info_no_title")
def test_info_with_title():
input_text = """!!! info "Requirements"
- Longitudinal control must be available
- ICBM must be enabled
"""
expected = """> [!INFO] Requirements
> - Longitudinal control must be available
> - ICBM must be enabled
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL info_with_title:\n{result!r}\n!=\n{expected!r}"
print(" PASS: info_with_title")
def test_danger():
input_text = """!!! danger "Important"
sunnypilot is a **driver assistance** system. It is **NOT** a self-driving system.
"""
expected = """> [!DANGER] Important
> sunnypilot is a **driver assistance** system. It is **NOT** a self-driving system.
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL danger:\n{result!r}\n!=\n{expected!r}"
print(" PASS: danger")
def test_tip():
input_text = """!!! tip
The more detail you provide, the faster we can diagnose and fix the issue.
"""
expected = """> [!TIP]
> The more detail you provide, the faster we can diagnose and fix the issue.
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL tip:\n{result!r}\n!=\n{expected!r}"
print(" PASS: tip")
def test_multiline_with_blank():
input_text = """!!! warning
Line 1
Line 2 after blank
"""
expected = """> [!WARNING]
> Line 1
>
> Line 2 after blank
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL multiline_with_blank:\n{result!r}\n!=\n{expected!r}"
print(" PASS: multiline_with_blank")
def test_collapsible():
input_text = """??? warning "Click to see"
Hidden content
"""
expected = """> [!WARNING] Click to see
> Hidden content
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL collapsible:\n{result!r}\n!=\n{expected!r}"
print(" PASS: collapsible")
def test_collapsible_open():
input_text = """???+ info "Open by default"
Visible content
"""
expected = """> [!INFO] Open by default
> Visible content
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL collapsible_open:\n{result!r}\n!=\n{expected!r}"
print(" PASS: collapsible_open")
def test_surrounded_by_content():
input_text = """Some text before.
!!! note "Note Title"
Note content here.
Some text after.
"""
expected = """Some text before.
> [!NOTE] Note Title
> Note content here.
Some text after.
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL surrounded:\n{result!r}\n!=\n{expected!r}"
print(" PASS: surrounded_by_content")
def test_multiple_admonitions():
input_text = """!!! info "Requirements"
- Req 1
- Req 2
!!! warning "Vehicle Restrictions"
- Tesla: disabled on release
- Rivian: always disabled
"""
expected = """> [!INFO] Requirements
> - Req 1
> - Req 2
> [!WARNING] Vehicle Restrictions
> - Tesla: disabled on release
> - Rivian: always disabled
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL multiple:\n{result!r}\n!=\n{expected!r}"
print(" PASS: multiple_admonitions")
def test_real_doc_snippet():
"""Test with an actual snippet from docs_sp content."""
input_text = """## Speed Limit Mode
| Property | Value |
|----------|-------|
| **Param** | `SpeedLimitMode` |
| **Type** | Multi-button selector |
!!! info "Requirements"
- Longitudinal control must be available, **or** ICBM must be enabled
!!! warning "Vehicle Restrictions"
- **Tesla:** Speed Limit Assist mode is disabled on release branches
- **Rivian:** Speed Limit Assist mode is always disabled
---
"""
expected = """## Speed Limit Mode
| Property | Value |
|----------|-------|
| **Param** | `SpeedLimitMode` |
| **Type** | Multi-button selector |
> [!INFO] Requirements
> - Longitudinal control must be available, **or** ICBM must be enabled
> [!WARNING] Vehicle Restrictions
> - **Tesla:** Speed Limit Assist mode is disabled on release branches
> - **Rivian:** Speed Limit Assist mode is always disabled
---
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL real_doc:\n{result!r}\n!=\n{expected!r}"
print(" PASS: real_doc_snippet")
# ---------------------------------------------------------------------------
# 3. Convert Tabs
# ---------------------------------------------------------------------------
def test_tabs_basic():
input_text = """=== "Tab One"
Content for tab one.
=== "Tab Two"
Content for tab two.
"""
expected = """**Tab One**
Content for tab one.
---
**Tab Two**
Content for tab two.
---
"""
result = convert_tabs(input_text)
assert result == expected, f"FAIL tabs_basic:\n{result!r}\n!=\n{expected!r}"
print(" PASS: tabs_basic")
def test_tabs_multiline():
input_text = """=== "Details"
Line 1
Line 2
Line 3
"""
expected = """**Details**
Line 1
Line 2
Line 3
---
"""
result = convert_tabs(input_text)
assert result == expected, f"FAIL tabs_multiline:\n{result!r}\n!=\n{expected!r}"
print(" PASS: tabs_multiline")
# ---------------------------------------------------------------------------
# 4. Convert Grid Cards
# ---------------------------------------------------------------------------
def test_grid_cards_stripped():
input_text = """<div class="grid cards" markdown>
- **Card 1** - Description
- **Card 2** - Description
</div>
"""
expected = """
- **Card 1** - Description
- **Card 2** - Description
"""
result = convert_grid_cards(input_text)
# Normalize whitespace for comparison
assert result.strip() == expected.strip(), f"FAIL grid_cards:\n{result!r}\n!=\n{expected!r}"
print(" PASS: grid_cards_stripped")
# ---------------------------------------------------------------------------
# 5. Convert Emoji Shortcodes
# ---------------------------------------------------------------------------
def test_emoji_known():
input_text = ":material-check: Supported | :material-close: Not supported"
expected = "Y Supported | N Not supported"
result = convert_emoji_shortcodes(input_text)
assert result == expected, f"FAIL emoji_known:\n{result!r}\n!=\n{expected!r}"
print(" PASS: emoji_known")
def test_emoji_unknown_stripped():
input_text = ":material-unknown-icon: Some text"
expected = " Some text"
result = convert_emoji_shortcodes(input_text)
assert result == expected, f"FAIL emoji_unknown:\n{result!r}\n!=\n{expected!r}"
print(" PASS: emoji_unknown_stripped")
def test_emoji_in_grid_card():
input_text = "- :material-rocket-launch: **[Feature](link.md)**"
expected = "- **[Feature](link.md)**"
result = convert_emoji_shortcodes(input_text)
assert result == expected, f"FAIL emoji_grid:\n{result!r}\n!=\n{expected!r}"
print(" PASS: emoji_in_grid_card")
# ---------------------------------------------------------------------------
# 6. Resolve Internal Links
# ---------------------------------------------------------------------------
def test_internal_link_relative():
input_text = "See [ICBM](../features/cruise/icbm.md) for details."
result = resolve_internal_links(
input_text,
file_path="settings/cruise/speed-limit.md",
)
assert "/search?q=" in result, f"FAIL internal_link missing search URL:\n{result!r}"
assert "docs-sync-id" in result, f"FAIL internal_link missing sync-id:\n{result!r}"
assert "features%2Fcruise%2Ficbm.md" in result or "features/cruise/icbm.md" in result, (
f"FAIL internal_link wrong path:\n{result!r}"
)
print(" PASS: internal_link_relative")
def test_internal_link_with_anchor():
input_text = "See [section](./safety.md#driver-responsibility)."
result = resolve_internal_links(
input_text,
file_path="safety/index.md",
)
# Anchors are stripped — Discourse search cannot target sections
assert "#driver-responsibility" not in result, f"FAIL anchor should be stripped:\n{result!r}"
assert "docs-sync-id" in result, f"FAIL missing sync-id:\n{result!r}"
assert "safety" in result, f"FAIL wrong path:\n{result!r}"
print(" PASS: internal_link_with_anchor")
def test_external_link_untouched():
input_text = "Visit [GitHub](https://github.com/sunnypilot/sunnypilot)."
result = resolve_internal_links(
input_text,
file_path="index.md",
)
assert result == input_text, f"FAIL external_link:\n{result!r}"
print(" PASS: external_link_untouched")
# ---------------------------------------------------------------------------
# 7. Clean Blank Lines
# ---------------------------------------------------------------------------
def test_clean_blank_lines():
input_text = "Line 1\n\n\n\n\nLine 2\n"
expected = "Line 1\n\n\nLine 2\n"
result = clean_blank_lines(input_text)
assert result == expected, f"FAIL clean_blanks:\n{result!r}\n!=\n{expected!r}"
print(" PASS: clean_blank_lines")
def test_clean_blank_lines_no_change():
input_text = "Line 1\n\nLine 2\n"
result = clean_blank_lines(input_text)
assert result == input_text, f"FAIL clean_blanks_noop:\n{result!r}"
print(" PASS: clean_blank_lines_no_change")
# ---------------------------------------------------------------------------
# Integration: full convert()
# ---------------------------------------------------------------------------
def test_full_convert():
input_text = """---
title: Test Doc
---
# Test Document
!!! warning "Important"
Pay attention to the road.
See [safety info](../safety/safety.md) for more.
:material-check: Feature supported
"""
result = convert(
input_text,
file_path="features/index.md",
)
# Front matter stripped
assert "---\ntitle:" not in result, f"FAIL front matter not stripped:\n{result!r}"
# Admonition converted
assert "> [!WARNING] Important" in result, f"FAIL admonition:\n{result!r}"
assert "> Pay attention to the road." in result, f"FAIL admonition content:\n{result!r}"
# Link resolved to Discourse search
assert "/search?q=" in result, f"FAIL link not converted to search:\n{result!r}"
assert "docs-sync-id" in result, f"FAIL link missing sync-id:\n{result!r}"
# Emoji converted
assert ":material-check:" not in result, f"FAIL emoji:\n{result!r}"
assert "Y Feature supported" in result, f"FAIL emoji replacement:\n{result!r}"
print(" PASS: full_convert")
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
if __name__ == "__main__":
print("Testing MkDocs -> Discourse converter:")
tests = [
# 1. Front matter
test_strip_front_matter_basic,
test_strip_front_matter_absent,
# 2. Admonitions
test_basic_warning,
test_info_no_title,
test_info_with_title,
test_danger,
test_tip,
test_multiline_with_blank,
test_collapsible,
test_collapsible_open,
test_surrounded_by_content,
test_multiple_admonitions,
test_real_doc_snippet,
# 3. Tabs
test_tabs_basic,
test_tabs_multiline,
# 4. Grid cards
test_grid_cards_stripped,
# 5. Emoji
test_emoji_known,
test_emoji_unknown_stripped,
test_emoji_in_grid_card,
# 6. Internal links
test_internal_link_relative,
test_internal_link_with_anchor,
test_external_link_untouched,
# 7. Blank lines
test_clean_blank_lines,
test_clean_blank_lines_no_change,
# Integration
test_full_convert,
]
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)