#!/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 = """
- **Card 1** - Description - **Card 2** - Description
""" 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)