Files
github-action/tests/test_build_wiki.py
T
Tirsvad 57d9b63b3c
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 57s
Document the English-comments workflow: guide, README, wiki in three languages
Adds the guide, the README section with its diagram, the wiki page in English,
Danish and Bahasa Malaysia, the Home and Capabilities rows, the sidebar entry
and the changelog entry under Unreleased.

Refs #206
Task: MIL-013#10
2026-10-19 12:34:15 +08:00

321 lines
12 KiB
Python

"""Offline tests for tools/build_wiki.py."""
import importlib.util
import re
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
_SPEC = importlib.util.spec_from_file_location(
"build_wiki", REPO_ROOT / "tools" / "build_wiki.py"
)
assert _SPEC is not None and _SPEC.loader is not None
build_wiki = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(build_wiki)
BASE = build_wiki.SOURCE_BASE_URL
class TestStripHeadingSymbols:
@pytest.mark.parametrize(
("line", "expected"),
[
("## 🧭 Overview", "## Overview"),
("## 🛠️ Setup", "## Setup"),
("## ▶️ Run", "## Run"),
("#### 🕒 Cron", "#### Cron"),
("## Plain", "## Plain"),
("## `code` heading", "## `code` heading"),
],
)
def test_removes_leading_emoji_only(self, line, expected):
assert build_wiki.strip_heading_symbols(line) == expected
class TestRewriteLink:
@pytest.mark.parametrize(
("target", "source", "expected"),
[
("guides/credentials.md", "README.md", "Credentials"),
("guides/onboarding.md#runners", "README.md", "Onboarding#runners"),
("troubleshooting.md", "guides/credentials.md", "Troubleshooting"),
("onboarding.md#runners", "guides/troubleshooting.md", "Onboarding#runners"),
("#-requirements", "README.md", "#requirements"),
("#runners", "guides/onboarding.md", "#runners"),
("https://example.test/a", "README.md", "https://example.test/a"),
("mailto:a@example.test", "README.md", "mailto:a@example.test"),
("docs/project-plan.md", "README.md", f"{BASE}/docs/project-plan.md"),
("../docs/project-plan.md", "guides/onboarding.md", f"{BASE}/docs/project-plan.md"),
(
".gitea/scoped_workflows/sync-github-metadata.yml",
"README.md",
f"{BASE}/.gitea/scoped_workflows/sync-github-metadata.yml",
),
("../README.md", "guides/onboarding.md", "Home"),
],
)
def test_rewrites_by_target_kind(self, target, source, expected):
assert build_wiki.rewrite_link(target, source) == expected
class TestConvertMarkdown:
def test_leaves_fenced_code_untouched(self):
text = "## 🧭 Title\n```bash\n## 🧭 not a heading\n[x](guides/a.md)\n```\n"
converted = build_wiki.convert_markdown(text, "README.md")
assert converted.splitlines()[0] == "## Title"
assert "## 🧭 not a heading" in converted
assert "[x](guides/a.md)" in converted
def test_rewrites_links_outside_code(self):
text = "See [guide](guides/credentials.md) and [plan](docs/project-plan.md)."
converted = build_wiki.convert_markdown(text, "README.md")
assert f"[guide](Credentials) and [plan]({BASE}/docs/project-plan.md)" in converted
class TestSplitH2Sections:
def test_keeps_subsections_and_ignores_h2_in_code(self):
text = (
"# Title\nintro\n"
"## 🧭 One\na\n### Sub\nb\n"
"## Two\n```\n## not a section\n```\n"
)
preamble, sections = build_wiki.split_h2_sections(text)
assert preamble == ["# Title", "intro"]
assert list(sections) == ["One", "Two"]
assert "### Sub" in sections["One"]
assert "## not a section" in sections["Two"]
def _write_sources(root: Path) -> None:
(root / "guides").mkdir()
(root / "README.md").write_text(
"# 🚀 Name\nIntro.\n\n## 📚 Table of Contents\n- [x](#-overview)\n\n"
"## 🧭 Overview\nSee [credentials](guides/credentials.md).\n\n"
"## 🔧 Workflows\nA table and diagrams.\n\n### 🔄 Sync\nDetails.\n\n"
"## 📋 Requirements\nNeeds a runner.\n\n## 🛠️ Setup\nRun [it](#-requirements).\n\n"
"## ▶️ Run\nGo.\n\n## 🧪 Tests\nPytest.\n\n## 🟢 Capabilities\nA table.\n\n### 🔎 Detail\nMore.\n\n"
"## 📄 License\nMIT.\n",
encoding="utf-8",
)
guide_names = (
"onboarding",
"credentials",
"troubleshooting",
"quality-checks",
"quality-checks-cpp",
"quality-checks-csharp",
"code-metrics",
"english-comments",
"upgrading",
)
for name in guide_names:
(root / "guides" / f"{name}.md").write_text(
f"# {name.title()}\nBody.\n", encoding="utf-8"
)
class TestRenderPage:
@pytest.fixture()
def repo(self, tmp_path):
_write_sources(tmp_path)
return tmp_path
def test_home_keeps_overview_to_tests_and_drops_the_rest(self, repo):
home = build_wiki.render_page(build_wiki.PAGES[0], repo)
assert "# Name" in home
assert "## Overview" in home
assert "[credentials](Credentials)" in home
assert "Run [it](#requirements)." in home
assert "Table of Contents" not in home
assert "Capabilities" not in home
assert "License" not in home
def test_capabilities_page_keeps_its_subsections(self, repo):
page = next(p for p in build_wiki.PAGES if p.name == "Capabilities")
text = build_wiki.render_page(page, repo)
assert "## Capabilities" in text
assert "### Detail" in text
assert "### Sync" not in text
assert "## Overview" not in text
def test_every_page_names_its_source_and_warns(self, repo):
for page in build_wiki.PAGES:
text = build_wiki.render_page(page, repo)
assert f"Generated from {page.source}" in text
assert "overwritten" in text
def test_missing_section_is_an_error(self, repo):
(repo / "README.md").write_text("# Name\n", encoding="utf-8")
with pytest.raises(build_wiki.WikiBuildError, match="section 'Overview'"):
build_wiki.render_page(build_wiki.PAGES[0], repo)
def test_missing_source_is_an_error(self, repo):
(repo / "guides" / "onboarding.md").unlink()
page = next(p for p in build_wiki.PAGES if p.name == "Onboarding")
with pytest.raises(build_wiki.WikiBuildError, match="is missing"):
build_wiki.render_page(page, repo)
class TestWriteAndCheck:
@pytest.fixture()
def repo(self, tmp_path):
root = tmp_path / "repo"
root.mkdir()
_write_sources(root)
return root
def test_write_then_check_passes(self, repo, tmp_path):
wiki = tmp_path / "wiki"
changed = build_wiki.write_pages(wiki, repo)
expected = [f"{p.name}.md" for p in build_wiki.PAGES] + [build_wiki.SIDEBAR_NAME]
assert sorted(changed) == sorted(expected)
assert build_wiki.check_pages(wiki, repo) == []
assert build_wiki.write_pages(wiki, repo) == []
def test_check_reports_missing_and_changed_pages(self, repo, tmp_path):
wiki = tmp_path / "wiki"
build_wiki.write_pages(wiki, repo)
(wiki / "Home.md").write_text("edited by hand\n", encoding="utf-8")
(wiki / "Credentials.md").unlink()
problems = build_wiki.check_pages(wiki, repo)
assert "Home.md: differs from its source" in problems
assert "Credentials.md: missing" in problems
def test_check_detects_a_changed_source(self, repo, tmp_path):
wiki = tmp_path / "wiki"
build_wiki.write_pages(wiki, repo)
(repo / "guides" / "onboarding.md").write_text("# Changed\n", encoding="utf-8")
assert build_wiki.check_pages(wiki, repo) == [
"Onboarding.md: differs from its source"
]
def test_committed_wiki_pages_are_current():
"""The pages in wiki/ match what README.md and guides/ produce."""
assert build_wiki.check_pages() == []
class TestLanguageSwitcher:
def test_english_page_links_to_the_translations(self):
assert build_wiki.language_switcher("Home") == (
"English | [Dansk](Home.da) | [Bahasa Melayu](Home.ms)"
)
def test_translation_does_not_link_to_itself(self):
assert build_wiki.language_switcher("Home", "da") == (
"[English](Home) | Dansk | [Bahasa Melayu](Home.ms)"
)
class TestCheckTranslations:
@pytest.fixture()
def repo(self, tmp_path):
root = tmp_path / "repo"
root.mkdir()
_write_sources(root)
return root
@staticmethod
def write_translations(wiki, repo, only_hash=None):
english = build_wiki.build_all(repo)
wiki.mkdir(exist_ok=True)
for page in build_wiki.PAGES:
digest = only_hash or build_wiki.source_hash(
english[f"{page.name}.md"].encode("utf-8")
)
for code, _label in build_wiki.LANGUAGES:
(wiki / f"{page.name}.{code}.md").write_text(
f"<!-- translation of {page.name}.md, source sha256 {digest} -->\n\nText\n",
encoding="utf-8",
)
def test_current_translations_pass(self, repo, tmp_path):
wiki = tmp_path / "wiki"
self.write_translations(wiki, repo)
assert build_wiki.check_translations(wiki, repo) == []
def test_reports_missing_translations(self, repo, tmp_path):
wiki = tmp_path / "wiki"
self.write_translations(wiki, repo)
(wiki / "Home.ms.md").unlink()
assert build_wiki.check_translations(wiki, repo) == ["Home.ms.md: missing"]
def test_reports_a_stale_translation(self, repo, tmp_path):
wiki = tmp_path / "wiki"
self.write_translations(wiki, repo)
(repo / "guides" / "onboarding.md").write_text("# Changed\n", encoding="utf-8")
problems = build_wiki.check_translations(wiki, repo)
assert [p.split(":")[0] for p in problems] == ["Onboarding.da.md", "Onboarding.ms.md"]
assert all("stale" in problem for problem in problems)
def test_reports_a_missing_or_wrong_header(self, repo, tmp_path):
wiki = tmp_path / "wiki"
self.write_translations(wiki, repo)
(wiki / "Home.da.md").write_text("no header\n", encoding="utf-8")
assert build_wiki.check_translations(wiki, repo) == [
"Home.da.md: first line is not the translation header"
]
def _shape(text: str) -> dict:
lines = text.splitlines()
return {
"fences": sum(line.startswith("```") for line in lines),
"table_rows": sum(line.startswith("|") for line in lines),
"heading_levels": [
len(line) - len(line.lstrip("#")) for line in lines if line.startswith("#")
],
}
@pytest.mark.parametrize("page", [page.name for page in build_wiki.PAGES])
@pytest.mark.parametrize("code", [code for code, _label in build_wiki.LANGUAGES])
def test_committed_translations_follow_the_english_structure(page, code):
"""A translation has the same code blocks, table rows and heading levels."""
wiki = REPO_ROOT / "docs" / "wiki"
english = _shape((wiki / f"{page}.md").read_text(encoding="utf-8"))
translated = _shape((wiki / f"{page}.{code}.md").read_text(encoding="utf-8"))
assert translated == english
def test_committed_translations_are_current():
"""No translation follows an older version of its English page."""
assert build_wiki.check_translations() == []
class TestSidebar:
"""Gitea shows subpages only through _Sidebar, so it must reach every page."""
def test_links_every_page_in_every_language(self):
sidebar = build_wiki.render_sidebar()
for page in build_wiki.PAGES:
assert f"]({page.name})" in sidebar
for code, _label in build_wiki.LANGUAGES:
assert f"]({page.name}.{code})" in sidebar
def test_every_link_names_a_committed_page(self):
sidebar = build_wiki.render_sidebar()
targets = re.findall(r"\]\(([^)]+)\)", sidebar)
assert targets
for target in targets:
assert (build_wiki.WIKI_DIR / f"{target}.md").is_file(), target
def test_has_one_label_per_page_and_language(self):
for code in ("en", *(c for c, _ in build_wiki.LANGUAGES)):
assert len(build_wiki.SIDEBAR_LABELS[code]) == len(build_wiki.PAGES)
def test_home_lists_every_workflow_file_with_a_diagram():
"""The Home page names each workflow file of the repository and draws it."""
home = (REPO_ROOT / "docs" / "wiki" / "Home.md").read_text(encoding="utf-8")
files = sorted(
path.name
for folder in (".gitea/scoped_workflows", ".gitea/workflows")
for path in (REPO_ROOT / folder).glob("*.yml")
)
assert files
for name in files:
assert f"[`{name}`]" in home, f"{name} is not listed on the Home page"
assert home.count("@startuml") == len(files)