mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
Platform guides, device groups, terminal tabs, pack README manuals and logo credits move from Python tables to platforms/_guides.yml, checked against the registry on load. A folder row without a path is the registry's extract_to, written once. Wiki pages place themselves in the navigation with order and nav_title in their front matter. Generated docs/ and mkdocs.yml are unchanged.
217 lines
9.3 KiB
Python
217 lines
9.3 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_mkdocs_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()
|