mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
generate_site.py writes the Starlight project in website/ and nothing else: mkdocs.yml, the MkDocs hooks, theme overrides and stylesheet go, and the wiki is written in Starlight's syntax. Both workflows set up Node with the official actions/setup-node pinned by commit, run npm ci from the committed lockfile, build with the strict link validator and check the result with validate_site.py. The search weighs a page by what it answers: Start here pages first, a system page's cards and file names before a guide citing them, deep guides, launchers, aliases and frozen copies after. Status badges, hash blocks and the export change lists leave the index. The ranking sits at the centre of a range where the 13 queries of search_gate.py all answer, in the search box itself. 839 pages, every URL and 45,611 ids match the MkDocs build. The tests read the Starlight content and syntax.
217 lines
9.2 KiB
Python
217 lines
9.2 KiB
Python
"""The site's words live in Markdown and YAML, not in the generator.
|
|
|
|
Each platform's guide is in platforms/_guides.yml, read and checked by
|
|
siteguides.load_guides. A wiki page places itself in the navigation from its
|
|
front matter. Both are edited as data: a new platform or a new wiki page
|
|
needs no Python change, and a mistake stops the build with its name.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
|
|
import generate_site # noqa: E402
|
|
import siteguides # noqa: E402
|
|
from siteguides import GUIDES, GuideError, load_guides # noqa: E402
|
|
|
|
GUIDES_FILE = REPO_ROOT / "platforms" / "_guides.yml"
|
|
WIKI = REPO_ROOT / "wiki"
|
|
|
|
|
|
def _registry() -> dict:
|
|
with open(REPO_ROOT / "platforms" / "_registry.yml", encoding="utf-8") as handle:
|
|
return yaml.safe_load(handle)["platforms"]
|
|
|
|
|
|
def _guides_text() -> str:
|
|
return GUIDES_FILE.read_text(encoding="utf-8")
|
|
|
|
|
|
class GuidesLiveInYaml(unittest.TestCase):
|
|
|
|
def _load(self, text: str, registry: dict | None = None) -> dict:
|
|
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
|
|
path = Path(tmp) / "_guides.yml"
|
|
path.write_text(text, encoding="utf-8")
|
|
return load_guides(path, registry if registry is not None else _registry())
|
|
|
|
def test_every_registered_platform_has_a_guide_in_the_file(self):
|
|
written = yaml.safe_load(_guides_text())["guides"]
|
|
self.assertEqual(sorted(written), sorted(_registry()))
|
|
self.assertEqual(sorted(GUIDES), sorted(_registry()))
|
|
|
|
def test_a_platform_without_a_guide_is_named(self):
|
|
data = yaml.safe_load(_guides_text())
|
|
del data["guides"]["knulli"]
|
|
with self.assertRaisesRegex(GuideError, r"knulli is registered and has no guide"):
|
|
self._load(yaml.safe_dump(data))
|
|
|
|
def test_a_guide_for_no_platform_is_named(self):
|
|
registry = _registry()
|
|
del registry["knulli"]
|
|
with self.assertRaisesRegex(GuideError, r"unregistered platforms: knulli"):
|
|
self._load(_guides_text(), registry)
|
|
|
|
def test_a_misspelt_key_is_refused(self):
|
|
data = yaml.safe_load(_guides_text())
|
|
data["guides"]["lakka"]["sumary"] = "A typo."
|
|
with self.assertRaisesRegex(GuideError, r"guides\.lakka: unknown key sumary"):
|
|
self._load(yaml.safe_dump(data))
|
|
|
|
def test_a_terminal_no_tab_describes_is_refused(self):
|
|
data = yaml.safe_load(_guides_text())
|
|
data["guides"]["lakka"]["run"] = ["telnet"]
|
|
with self.assertRaisesRegex(GuideError, r"guides\.lakka\.run: no terminal named telnet"):
|
|
self._load(yaml.safe_dump(data))
|
|
|
|
def test_a_folder_row_without_path_is_the_extract_folder(self):
|
|
"""The folder the pack unzips into is written once, in the registry."""
|
|
registry = _registry()
|
|
written = yaml.safe_load(_guides_text())["guides"]["batocera"]["folders"][0]
|
|
self.assertNotIn("path", written)
|
|
self.assertEqual(GUIDES["batocera"].folders[0],
|
|
(written["where"], f"`{registry['batocera']['extract_to']}`"))
|
|
|
|
def test_a_folder_row_without_path_needs_an_extract_folder(self):
|
|
registry = _registry()
|
|
del registry["lakka"]["extract_to"]
|
|
with self.assertRaisesRegex(GuideError, r"guides\.lakka\.folders\[0\]: no `path`"):
|
|
self._load(_guides_text(), registry)
|
|
|
|
def test_a_device_group_the_file_does_not_describe_is_refused(self):
|
|
registry = _registry()
|
|
registry["lakka"]["device_group"] = "toaster"
|
|
with self.assertRaisesRegex(GuideError, r"lakka\.device_group 'toaster'"):
|
|
self._load(_guides_text(), registry)
|
|
|
|
def test_the_pack_readme_keeps_its_indent(self):
|
|
manual = siteguides.pack_manual("retrobat")
|
|
self.assertTrue(manual.startswith(" Option B: Manual\n ----------------\n"))
|
|
self.assertTrue(manual.endswith(" 4. Overwrite if asked\n\n"))
|
|
self.assertEqual(siteguides.pack_manual("minui"), "")
|
|
|
|
|
|
# A table of prose keyed by platform: what the guides file replaced.
|
|
_PLATFORM_BRANCH = re.compile(r"""(?:==|!=)\s*["']([a-z0-9-]+)["']|["']([a-z0-9-]+)["']\s*(?:==|!=)""")
|
|
# The modules that write the site and the pack README.
|
|
_RENDERERS = ("generate_site.py", "packreadme.py", "generate_readme.py")
|
|
|
|
|
|
class NoPlatformProseInScripts(unittest.TestCase):
|
|
|
|
def test_no_table_of_prose_keyed_by_platform(self):
|
|
platforms = set(_registry())
|
|
found = []
|
|
for path in sorted((REPO_ROOT / "scripts").rglob("*.py")):
|
|
for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))):
|
|
if not isinstance(node, ast.Dict):
|
|
continue
|
|
keyed = [(k.value, v) for k, v in zip(node.keys, node.values)
|
|
if isinstance(k, ast.Constant) and k.value in platforms]
|
|
prose = [key for key, value in keyed if any(
|
|
isinstance(c, ast.Constant) and isinstance(c.value, str)
|
|
and " " in c.value and len(c.value) >= 30
|
|
for c in ast.walk(value))]
|
|
if len(keyed) >= 2 and prose:
|
|
found.append(f"{path.relative_to(REPO_ROOT)}:{node.lineno}")
|
|
self.assertEqual(found, [], "per-platform text belongs in platforms/_guides.yml")
|
|
|
|
def test_no_page_branches_on_one_platform(self):
|
|
platforms = set(_registry())
|
|
found = []
|
|
scripts = REPO_ROOT / "scripts"
|
|
for path in [scripts / name for name in _RENDERERS] + sorted(scripts.glob("site*.py")):
|
|
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
|
for match in _PLATFORM_BRANCH.finditer(line):
|
|
if (match.group(1) or match.group(2)) in platforms:
|
|
found.append(f"{path.name}:{number}: {line.strip()}")
|
|
self.assertEqual(found, [], "a guide field says what one platform needs")
|
|
|
|
|
|
def _wiki_copy(tmp: str) -> Path:
|
|
target = Path(tmp) / "wiki"
|
|
shutil.copytree(WIKI, target)
|
|
return target
|
|
|
|
|
|
class NavFromFrontMatter(unittest.TestCase):
|
|
|
|
def _nav(self, wiki: Path) -> dict:
|
|
nav = generate_site.generate_nav({}, {}, {}, {}, wiki)
|
|
return {next(iter(tab)): next(iter(tab.values())) for tab in nav}
|
|
|
|
def test_the_repository_nav_matches_the_pages(self):
|
|
tabs = self._nav(WIKI)
|
|
self.assertEqual(tabs["Explore"][-2:], [
|
|
{"All installer options": "wiki/installer.md"},
|
|
{"Is it legal?": "wiki/legal.md"},
|
|
])
|
|
self.assertEqual(tabs["Reference"][0], "wiki/index.md")
|
|
|
|
def test_a_new_page_lands_in_its_tab_at_its_order(self):
|
|
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
|
|
wiki = _wiki_copy(tmp)
|
|
(wiki / "save-files.md").write_text(
|
|
"---\nlevel: explore\norder: 35\n---\n\n# Save files\n\nText.\n",
|
|
encoding="utf-8")
|
|
explore = self._nav(wiki)["Explore"]
|
|
targets = [e if isinstance(e, str) else next(iter(e.values())) for e in explore]
|
|
self.assertEqual(targets.index("wiki/save-files.md"),
|
|
targets.index("wiki/installer.md") + 1)
|
|
self.assertIn({"Save files": "wiki/save-files.md"}, explore)
|
|
|
|
def test_a_page_without_a_level_fails_by_name(self):
|
|
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
|
|
wiki = _wiki_copy(tmp)
|
|
(wiki / "orphan.md").write_text("# Orphan\n\nText.\n", encoding="utf-8")
|
|
with self.assertRaisesRegex(generate_site.NavError,
|
|
r"wiki/orphan\.md: front matter needs `level:`"):
|
|
self._nav(wiki)
|
|
|
|
def test_a_page_without_an_order_fails_by_name(self):
|
|
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
|
|
wiki = _wiki_copy(tmp)
|
|
(wiki / "orphan.md").write_text(
|
|
"---\nlevel: reference\n---\n\n# Orphan\n", encoding="utf-8")
|
|
with self.assertRaisesRegex(
|
|
generate_site.NavError,
|
|
r"wiki/orphan\.md: front matter needs `order:`.*Reference tab",
|
|
):
|
|
self._nav(wiki)
|
|
|
|
def test_two_pages_on_one_place_fail_naming_both(self):
|
|
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
|
|
wiki = _wiki_copy(tmp)
|
|
(wiki / "twin.md").write_text(
|
|
"---\nlevel: contribute\norder: 20\n---\n\n# Twin\n", encoding="utf-8")
|
|
with self.assertRaisesRegex(generate_site.NavError,
|
|
r"wiki/adding-a-platform\.md and wiki/twin\.md"):
|
|
self._nav(wiki)
|
|
|
|
def test_nav_false_keeps_a_page_out(self):
|
|
tabs = self._nav(WIKI)
|
|
flat = repr(tabs)
|
|
self.assertNotIn("getting-started.md", flat)
|
|
|
|
def test_every_wiki_page_states_its_place(self):
|
|
for page in sorted(WIKI.glob("*.md")):
|
|
meta = generate_site._front_matter(page.read_text(encoding="utf-8"))
|
|
with self.subTest(page=page.name):
|
|
self.assertIn(meta.get("level"), generate_site.TABS)
|
|
self.assertTrue(meta.get("nav") is False or isinstance(meta.get("order"), int))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|