mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 04:39: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.
259 lines
11 KiB
Python
259 lines
11 KiB
Python
"""The site reads as a funnel, from someone who knows nothing to a contributor.
|
|
|
|
Five tabs in reading order, every registered platform reachable from the
|
|
device picker, a level on every page, and no page for beginners sending its
|
|
reader two levels deeper without saying so.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
|
|
import generate_site # noqa: E402
|
|
from tests import sitepages # noqa: E402
|
|
from common import list_registered_platforms, load_platform_config, load_platform_registry # noqa: E402
|
|
from siteguides import DEVICE_GROUPS, GROUP_ANCHORS, GUIDES # noqa: E402
|
|
|
|
DOCS = sitepages.CONTENT
|
|
ORDER = list(generate_site.LEVELS)
|
|
# Pages link to site addresses once written for the site.
|
|
LINK = re.compile(r"\[[^\]]*\]\((/retrobios/[^)\s#?]*)(?:[#?][^)]*)?\)")
|
|
FENCED = re.compile(r"^\s*```.*?^\s*```", re.S | re.M)
|
|
|
|
|
|
def _registered() -> list[str]:
|
|
return list_registered_platforms(str(REPO_ROOT / "platforms"), include_archived=True)
|
|
|
|
|
|
def _which_pack() -> str:
|
|
with contextlib.chdir(REPO_ROOT):
|
|
return generate_site.generate_which_pack()
|
|
|
|
|
|
def _docs_pages() -> list[Path]:
|
|
return [p for p in sitepages.pages() if p.name not in ("404.md",)]
|
|
|
|
|
|
def _level(page: Path) -> str:
|
|
return str(generate_site._front_matter(page.read_text(encoding="utf-8")).get("level") or "")
|
|
|
|
|
|
class FiveTabs(unittest.TestCase):
|
|
"""Twelve tabs put the walkthrough at position eleven, under "Wiki"."""
|
|
|
|
def test_the_tabs_in_reading_order(self):
|
|
nav = generate_site.generate_nav({}, {}, {}, {})
|
|
self.assertEqual(
|
|
[next(iter(tab)) for tab in nav],
|
|
["Start here", "Your setup", "Explore", "Reference", "Contribute"],
|
|
)
|
|
|
|
def test_each_tab_opens_on_an_index_or_a_first_page(self):
|
|
nav = generate_site.generate_nav({}, {}, {}, {})
|
|
tabs = {next(iter(tab)): next(iter(tab.values())) for tab in nav}
|
|
self.assertEqual(tabs["Start here"][0], "index.md")
|
|
self.assertEqual(tabs["Your setup"][0], "platforms/index.md")
|
|
self.assertEqual(tabs["Reference"][0], "wiki/index.md")
|
|
self.assertEqual(tabs["Contribute"][0], "contributing.md")
|
|
|
|
def test_the_breadcrumbs_are_on(self):
|
|
title = (REPO_ROOT / "website" / "src" / "components" / "PageTitle.astro").read_text(encoding="utf-8")
|
|
route = (REPO_ROOT / "website" / "src" / "routeData.ts").read_text(encoding="utf-8")
|
|
self.assertIn('aria-label="Breadcrumb"', title)
|
|
self.assertIn("rbBreadcrumbs", route)
|
|
|
|
|
|
class EveryPlatformOnThePicker(unittest.TestCase):
|
|
"""KNULLI, MinUI, AmberELEC, EmuELEC, NextUI and MyMinUI were on no table
|
|
of the download page: a hand-written list leaves out what it forgets."""
|
|
|
|
def test_every_platform_has_a_group_and_a_guide(self):
|
|
registry = load_platform_registry(str(REPO_ROOT / "platforms"))
|
|
for key in _registered():
|
|
with self.subTest(platform=key):
|
|
self.assertIn(registry[key].get("device_group"), DEVICE_GROUPS)
|
|
self.assertIn(key, GUIDES)
|
|
|
|
def test_every_group_has_an_anchor(self):
|
|
self.assertEqual(set(GROUP_ANCHORS), set(DEVICE_GROUPS))
|
|
|
|
def test_every_platform_has_a_row(self):
|
|
page = _which_pack()
|
|
for key in _registered():
|
|
name = load_platform_config(key, str(REPO_ROOT / "platforms"))["platform"]
|
|
with self.subTest(platform=key):
|
|
self.assertIn(f"| [{name}](platforms/{key}.md) |", page)
|
|
|
|
def test_every_wiki_page_is_in_the_navigation(self):
|
|
"""A wiki page is published as soon as it exists, but a page left out
|
|
of the nav is reachable only by a link and MkDocs says so at info
|
|
level. getting-started.md is kept out on purpose: it only links the
|
|
pages that took its content, for the old links that point to it."""
|
|
def targets(entries) -> list[str]:
|
|
found = []
|
|
for entry in entries:
|
|
if isinstance(entry, str):
|
|
found.append(entry)
|
|
else:
|
|
for value in entry.values():
|
|
found += targets(value) if isinstance(value, list) else [value]
|
|
return found
|
|
|
|
nav = " ".join(targets(generate_site.generate_nav({}, {}, {}, {})))
|
|
kept_out = {"getting-started.md"}
|
|
wiki = Path(__file__).resolve().parents[1] / "wiki"
|
|
missing = sorted(
|
|
p.name for p in wiki.glob("*.md")
|
|
if p.name not in kept_out and f"wiki/{p.name}" not in nav
|
|
)
|
|
self.assertEqual(missing, [])
|
|
|
|
def test_every_platform_is_in_the_navigation_of_its_group(self):
|
|
registry = load_platform_registry(str(REPO_ROOT / "platforms"))
|
|
coverages = {
|
|
key: {"platform": load_platform_config(key, str(REPO_ROOT / "platforms"))["platform"]}
|
|
for key in _registered()
|
|
}
|
|
nav = generate_site.generate_nav(coverages, {}, {}, registry)
|
|
setup = next(tab["Your setup"] for tab in nav if "Your setup" in tab)
|
|
groups = {next(iter(item)): next(iter(item.values()))
|
|
for item in setup if isinstance(item, dict)}
|
|
for key in coverages:
|
|
label = DEVICE_GROUPS[registry[key]["device_group"]][0]
|
|
with self.subTest(platform=key):
|
|
self.assertIn({coverages[key]["platform"]: f"platforms/{key}.md"}, groups[label])
|
|
|
|
def test_handheld_systems_that_run_retroarch_are_named(self):
|
|
"""muOS, ArkOS and the rest are detected as RetroArch by the
|
|
installer and were named on no page a user reads."""
|
|
page = _which_pack()
|
|
self.assertIn("| muOS | [RetroArch](platforms/retroarch.md) | `/run/muos/storage/bios` |", page)
|
|
self.assertNotIn("| TrimUI |", page)
|
|
|
|
|
|
class SampleCoresLeaveTheNavigation(unittest.TestCase):
|
|
"""13 libretro sample cores (testsw, testgl_ff...) sat in the emulators
|
|
navigation among the cores people look up."""
|
|
|
|
def test_a_core_with_no_system_and_no_file_is_left_out(self):
|
|
profiles = {
|
|
"testsw": {"emulator": "testsw", "type": "libretro", "systems": []},
|
|
"duckstation": {"emulator": "DuckStation", "type": "standalone",
|
|
"systems": ["sony-playstation"], "files": [{"name": "a.bin"}]},
|
|
"zx": {"emulator": "zx81", "type": "libretro", "systems": ["sinclair-zx81"]},
|
|
}
|
|
nav = generate_site._emulator_nav(profiles)
|
|
self.assertEqual(nav[0], "emulators/index.md")
|
|
groups = {next(iter(group)): next(iter(group.values())) for group in nav[1:]}
|
|
self.assertEqual(groups, {
|
|
"D to F": [{"DuckStation": "emulators/duckstation.md"}],
|
|
"V to Z": [{"zx81": "emulators/zx.md"}],
|
|
})
|
|
|
|
|
|
class EveryPageHasALevel(unittest.TestCase):
|
|
def test_wiki_sources_declare_one(self):
|
|
for page in sorted((REPO_ROOT / "wiki").glob("*.md")):
|
|
level = generate_site._front_matter(page.read_text(encoding="utf-8")).get("level")
|
|
with self.subTest(page=page.name):
|
|
self.assertIn(level, generate_site.LEVELS)
|
|
|
|
def test_every_generated_page_has_one_and_its_label(self):
|
|
for page in _docs_pages():
|
|
text = page.read_text(encoding="utf-8")
|
|
level = _level(page)
|
|
with self.subTest(page=str(page.relative_to(DOCS))):
|
|
self.assertIn(level, generate_site.LEVELS)
|
|
self.assertIn(f"\nlevel: {level}\n", text)
|
|
# The label above each title comes from the page's level.
|
|
component = (REPO_ROOT / "website" / "src" / "components" / "PageTitle.astro").read_text()
|
|
for level, label in generate_site.LEVELS.items():
|
|
with self.subTest(level=level):
|
|
self.assertIn(f"{level}: '{label}'", component)
|
|
|
|
def test_the_decoration_keeps_an_authored_level(self):
|
|
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
|
|
page = Path(tmp) / "wiki" / "faq.md"
|
|
page.parent.mkdir()
|
|
page.write_text("---\nlevel: start\n---\n\n# FAQ\n\nQuestions people ask first.\n")
|
|
generate_site.decorate_markdown_pages(Path(tmp))
|
|
generate_site.decorate_markdown_pages(Path(tmp))
|
|
text = page.read_text()
|
|
self.assertEqual(text.count("level: start"), 1)
|
|
self.assertIn("searchWeight: 10\n", text)
|
|
self.assertIn("title: FAQ\n", text)
|
|
|
|
|
|
class DeepLinksCarryALabel(unittest.TestCase):
|
|
"""A beginner page sent its reader to Tools and Architecture as the next
|
|
step. A link from a start or setup page to a page two levels deeper or
|
|
more says what it leads to, with that page's level label before it on
|
|
its line, or in the heading of its section ("## Reference: ...")."""
|
|
|
|
def test_start_and_setup_pages(self):
|
|
import sitestarlight # noqa: PLC0415
|
|
|
|
pages = _docs_pages()
|
|
levels = {page: _level(page) for page in pages}
|
|
by_url = {sitestarlight.page_url(sitepages.name(page)): page for page in pages}
|
|
offenders = []
|
|
for page, level in levels.items():
|
|
if level not in ("start", "setup"):
|
|
continue
|
|
text = FENCED.sub("", page.read_text(encoding="utf-8"))
|
|
heading = ""
|
|
for line in text.splitlines():
|
|
if re.match(r"#{1,3} ", line):
|
|
heading = line.lstrip("#").strip()
|
|
for match in LINK.finditer(line):
|
|
target = by_url.get(match.group(1))
|
|
if target not in levels:
|
|
continue
|
|
deeper = levels[target]
|
|
if ORDER.index(deeper) - ORDER.index(level) < 2:
|
|
continue
|
|
label = f"{generate_site.LEVELS[deeper]}:"
|
|
if label not in line[: match.start()] and not heading.startswith(label):
|
|
offenders.append(f"{page.relative_to(DOCS)}: {match.group(0)}")
|
|
self.assertEqual(offenders, [], "\n".join(offenders[:30]))
|
|
|
|
|
|
class GuidePagesEndOnANextStep(unittest.TestCase):
|
|
def test_home_picker_and_platform_page(self):
|
|
for page in ("index.md", "which-pack.md", "platforms/batocera.md",
|
|
"wiki/first-install.md", "wiki/troubleshooting.md",
|
|
"wiki/what-are-bios-files.md", "wiki/faq.md"):
|
|
if page.startswith(("index", "which", "platforms")):
|
|
sitepages.require()
|
|
path = sitepages.page(page)
|
|
else:
|
|
path = REPO_ROOT / page
|
|
with self.subTest(page=page):
|
|
text = path.read_text(encoding="utf-8")
|
|
self.assertTrue(":::tip[Next]" in text or "## What comes next" in text)
|
|
|
|
def test_the_platform_page_opens_on_its_guide(self):
|
|
"""The folder came after four counters and a metadata table, and was
|
|
the pack-relative `bios/` where Batocera reads /userdata/bios/."""
|
|
sitepages.require()
|
|
text = sitepages.page("platforms/batocera.md").read_text(encoding="utf-8")
|
|
order = [text.index(h) for h in (
|
|
"## Where the files go", "## Install", "## Download", "## How to check",
|
|
"## Reference: every file by system",
|
|
)]
|
|
self.assertEqual(order, sorted(order))
|
|
self.assertLess(text.index("`/userdata/bios/`"), text.index("rb-stat-value"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|