feat: build the site with Astro Starlight

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.
This commit is contained in:
Abdessamad Derraz committed 2026-10-22 18:47:22 +02:00
1 parent abe240073c
commit 123d698e43
64 files changed
+742 -3118

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
"""The generated site pages, for the tests that read them.
generate_site.py writes each page into the Starlight project's content
collection, as .md or as .mdx when it uses a component. A test names a
page by its docs path (``which-pack.md``) and gets whichever file holds it.
"""
from __future__ import annotations
import os
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CONTENT = REPO_ROOT / "website" / "src" / "content" / "docs"
def require() -> Path:
"""The content folder, or a skip when the site was not generated (a
failure in CI, which generates it before the suite)."""
if not (CONTENT / "index.md").is_file():
if os.environ.get("CI"):
raise AssertionError("CI generates the site before the suite")
raise unittest.SkipTest("site not generated (python scripts/generate_site.py)")
return CONTENT
def page(name: str) -> Path:
"""The file of a page named by its docs path."""
stem = name[:-3] if name.endswith(".md") else name
for suffix in (".md", ".mdx"):
candidate = CONTENT / f"{stem}{suffix}"
if candidate.is_file():
return candidate
return CONTENT / f"{stem}.md"
def pages() -> list[Path]:
"""Every page file, sorted."""
root = require()
return sorted(p for p in root.rglob("*") if p.suffix in (".md", ".mdx"))
def name(path: Path) -> str:
"""A page file's docs path, the name tests use for it."""
return path.relative_to(CONTENT).with_suffix(".md").as_posix()
+28 -26
View File
@@ -258,7 +258,7 @@ class WorkflowRegressions(unittest.TestCase):
and acted on afterwards rather than swallowed. A best-effort side
action such as adding a label is not a check and keeps its `|| true`.
"""
checks = ("python scripts/", "unittest", "mkdocs build")
checks = ("python scripts/", "unittest", "npm run build")
for name in ("validate.yml", "deploy-site.yml"):
workflow = self._workflow(name)
for job_name, job in workflow["jobs"].items():
@@ -4311,7 +4311,7 @@ class AFileCardSaysAllItKnows(unittest.TestCase):
))
# The card is a heading with this id, linked by the heading's own
# permalink.
self.assertIn("{ #file-neogeo-zip .rb-file-title }", text)
self.assertIn('<h3 id="file-neogeo-zip" class="rb-file-title"', text)
self.assertIn("Standalone path: `Firmware/neogeo.zip`", text)
self.assertIn("`m11.bin`", text)
self.assertIn("CRC32 `0000000b`", text)
@@ -4954,7 +4954,7 @@ class CollectionCardsCarryFullHashes(unittest.TestCase):
"crc32": "504086c1", "sha256": "b" * 64}
text = "\n".join(_system_file_card(entry, FileConsumers({}, {})))
for value in (entry["sha1"], entry["md5"], entry["crc32"], entry["sha256"]):
self.assertIn(f"`{value}`", text)
self.assertIn(f">{value}</code>", text)
@@ -4975,17 +4975,11 @@ class NullReadsAsNone(unittest.TestCase):
self.assertIn("none: the source is the project itself", page)
def test_the_rendered_site_has_no_python_none(self):
docs = ROOT / "docs"
if not docs.is_dir():
if os.environ.get("CI"):
self.fail("CI generates docs/ before the suite")
self.skipTest("docs/ not generated")
pattern = re.compile(r"\| None \||: None$|: None \|", re.M)
from scripts.generate_site import LOCAL_NOTE_DIRS
from tests import sitepages
found = [p.relative_to(docs).as_posix() for p in sorted(docs.rglob("*.md"))
if not set(p.relative_to(docs).parts[:-1]) & set(LOCAL_NOTE_DIRS)
and pattern.search(p.read_text(encoding="utf-8"))]
pattern = re.compile(r"\| None \||: None$|: None \|", re.M)
found = [sitepages.name(p) for p in sitepages.pages()
if pattern.search(p.read_text(encoding="utf-8"))]
self.assertEqual(found, [])
@@ -5029,7 +5023,7 @@ class VersionAnchorsFollowContent(unittest.TestCase):
"raze", {"emulator": "Raze", "type": "standalone", "files": files},
{"files": {}, "indexes": {}},
)
return re.findall(r"\{ #(file-[^ ]+) \.rb-file-title \}", page)
return re.findall(r'<h3 id="(file-[^"]+)" class="rb-file-title"', page)
def test_inserting_a_version_keeps_the_others(self):
a = {"name": "DUKE3D.GRP", "path": "DUKE3D.GRP", "crc32": "bbc9ce44"}
@@ -5051,18 +5045,26 @@ class SitemapDatesComeFromThePage(unittest.TestCase):
"""All 664 sitemap URLs carried the build date: eduke32, re-read on
2026-10-04, was announced as changed on the day of every build."""
def test_the_hook_takes_the_page_date_or_none(self):
from types import SimpleNamespace
def test_the_page_date_or_none_reaches_the_sitemap(self):
import tempfile
from scripts.mkdocs_hooks import on_page_markdown
from scripts import generate_site
dated = SimpleNamespace(meta={"date_modified": "2026-10-04"}, update_date="today")
undated = SimpleNamespace(meta={}, update_date="today")
on_page_markdown("", dated, None, None)
on_page_markdown("", undated, None, None)
self.assertEqual((dated.update_date, undated.update_date), ("2026-10-04", ""))
config = (ROOT / "mkdocs.yml").read_text(encoding="utf-8")
self.assertIn("hooks:\n - scripts/mkdocs_hooks.py", config)
saved = dict(generate_site.PAGE_DATA)
self.addCleanup(lambda: (generate_site.PAGE_DATA.clear(),
generate_site.PAGE_DATA.update(saved)))
with tempfile.TemporaryDirectory(dir=ROOT / "tmp") as tmp:
docs = Path(tmp)
(docs / "emulators").mkdir()
(docs / "emulators" / "dated.md").write_text("# Dated\n\nA page with its own date.\n")
(docs / "emulators" / "undated.md").write_text("# Undated\n\nA page with no date.\n")
generate_site.PAGE_DATA["emulators/dated.md"] = {"dateModified": "2026-10-04"}
lastmod = generate_site.decorate_markdown_pages(docs)
self.assertEqual(lastmod, {"/retrobios/emulators/dated/": "2026-10-04"})
# The sitemap reads that map, and leaves every other page undated.
config = (ROOT / "website" / "astro.config.mjs").read_text(encoding="utf-8")
self.assertIn("lastmod.json", config)
self.assertIn("serialize(item)", config)
class SiteCodeSpansHoldTheirValue(unittest.TestCase):
@@ -6082,8 +6084,8 @@ class ThePipelineRunsTheSchemaCheckCIRuns(unittest.TestCase):
duplicate ids passed it and failed the deploy."""
workflow = (ROOT / ".github" / "workflows" / "deploy-site.yml").read_text(encoding="utf-8")
source = (ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8")
self.assertIn("mkdocs build --strict", workflow)
self.assertIn('["mkdocs", "build", "--strict"]', source)
self.assertIn("npm run build --prefix website", workflow)
self.assertIn('["npm", "run", "build", "--prefix", "website"]', source)
self.assertIn("scripts/validate_site.py", workflow)
self.assertIn('"scripts/validate_site.py"', source)
+8 -11
View File
@@ -22,7 +22,7 @@ import generate_site # noqa: E402
def _normalized(text: str) -> str:
# The site ends each page with its next step in the reading order. That
# is navigation, not contributing text, and a repository file has none.
text = text.split('\n\n!!! tip "Next"\n', 1)[0].rstrip("\n")
text = text.split("\n\n:::tip[Next]\n", 1)[0].rstrip("\n")
text = re.sub(r"\]\([^)]*\)", "](link)", text)
return "\n".join(text.splitlines()[1:]).replace("on this site", "on the documentation site")
@@ -73,17 +73,14 @@ class BeforeOpeningAPullRequest(unittest.TestCase):
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
self.assertIn("validate_schemas.py --source-only", workflow)
def test_it_installs_the_packages_the_site_render_needs(self):
"""Without mkdocs-material and pymdown-extensions the pipeline's
render step (8b) is skipped for every contributor."""
deploy = (REPO_ROOT / ".github" / "workflows" / "deploy-site.yml").read_text(
encoding="utf-8"
)
specs = re.findall(r'"((?:mkdocs-material|pymdown-extensions)[^"]*)"', deploy)
self.assertEqual(len(specs), 2)
def test_it_installs_what_the_checks_need_and_no_site_package(self):
"""The site is built by Node from website/package-lock.json: a pip
line naming the old theme installed packages nothing reads."""
text = generate_readme.generate_contributing()
for spec in specs:
self.assertIn(f'"{spec}"', text)
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
self.assertIn('pip install pyyaml "jsonschema[format-nongpl]" markdown', section)
self.assertNotIn("mkdocs", section)
self.assertTrue((REPO_ROOT / "website" / "package-lock.json").is_file())
class OneFileThroughTheBrowser(unittest.TestCase):
+10 -10
View File
@@ -85,22 +85,22 @@ class CorrectedFilesPage(unittest.TestCase):
def test_single_file_is_downloadable_raw(self):
self.assertIn(
"(https://raw.githubusercontent.com/Abdess/retrobios/main/exports/recalbox/es_bios.xml)",
'href="https://raw.githubusercontent.com/Abdess/retrobios/main/exports/recalbox/es_bios.xml"',
self.page,
)
def test_several_files_link_to_their_directory(self):
self.assertIn("tree/main/exports/retropie", self.page)
self.assertIn('??? note "The 2 original files it is based on"', self.page)
self.assertIn("<summary>The 2 original files it is based on</summary>", self.page)
def test_every_change_is_listed(self):
self.assertIn('??? note "Hashes updated to the emulator source (12)"', self.page)
self.assertIn("<summary>Hashes updated to the emulator source (12)</summary>", self.page)
for i in range(12):
self.assertIn(f" - `sys/file{i}.bin md5`", self.page)
self.assertIn(f"\n- `sys/file{i}.bin md5`", self.page)
def test_a_failing_file_is_said_to_fail(self):
self.assertIn(" - bash -n failed on a.sh", self.page)
self.assertIn(" - a size the format has no field for", self.page)
self.assertIn("\n- bash -n failed on a.sh", self.page)
self.assertIn("\n- a size the format has no field for", self.page)
def test_nothing_published_says_how_to_build(self):
self.assertIn("export_native.py --all --fetch", generate_exports_page([], {}))
@@ -412,16 +412,16 @@ class ThePageShowsEachValueBeforeAndAfter(unittest.TestCase):
def test_a_corrected_hash_shows_both_values(self):
self.assertIn(
f" - `psx/scph5501.bin` md5 `5626{'0' * 28}` becomes `c523{'1' * 28}`",
f"\n- `psx/scph5501.bin` md5 `5626{'0' * 28}` becomes `c523{'1' * 28}`",
self.page,
)
def test_a_requirement_names_the_entry_and_its_value(self):
self.assertIn('??? note "Required flags aligned with the code (1)"', self.page)
self.assertIn(" - `psx/scph5501.bin` becomes optional", self.page)
self.assertIn("<summary>Required flags aligned with the code (1)</summary>", self.page)
self.assertIn("\n- `psx/scph5501.bin` becomes optional", self.page)
def test_a_filled_hash_shows_its_value(self):
self.assertIn(f" - `psx/scph5501.bin` sha1 `{'f' * 40}` added", self.page)
self.assertIn(f"\n- `psx/scph5501.bin` sha1 `{'f' * 40}` added", self.page)
def test_no_arrow_glyph(self):
for glyph in (chr(0x2192), "->", "=>"):
+12 -10
View File
@@ -56,6 +56,9 @@ def _tracked() -> list[Path]:
def _semicolons(text: str) -> list[str]:
# An HTML character reference (&amp; &#123;) ends in a semicolon that
# joins no clauses.
text = re.sub(r"&(?:#\d+|#x[0-9a-fA-F]+|[a-zA-Z]+);", "&", text)
return [
line.strip()
for line in BACKTICKS.sub("``", text).splitlines()
@@ -356,18 +359,17 @@ class GeneratedSiteProse(unittest.TestCase):
sys.path.insert(0, str(ROOT / "scripts"))
from generate_site import LOCAL_NOTE_DIRS # noqa: PLC0415
docs = ROOT / "docs"
pages = [
p for p in docs.rglob("*.md")
if not set(p.relative_to(docs).parts[:-1]) & set(LOCAL_NOTE_DIRS)
] if docs.is_dir() else []
if not pages:
if os.environ.get("CI"):
self.fail("CI generates docs/ before the suite")
self.skipTest("docs/ not generated (python scripts/generate_site.py)")
from tests import sitepages # noqa: PLC0415
pages = [p for p in sitepages.pages()
if not set(p.relative_to(sitepages.CONTENT).parts[:-1]) & set(LOCAL_NOTE_DIRS)]
offenders = []
for page in pages:
text = re.sub(r"<script.*?</script>", "", page.read_text(encoding="utf-8"), flags=re.S)
text = page.read_text(encoding="utf-8")
# The front matter carries the page's structured data, not prose.
if text.startswith("---\n"):
text = text[text.index("\n---\n", 4) + 5:]
text = re.sub(r"<script.*?</script>", "", text, flags=re.S)
text = FENCED.sub("", text)
for number, line in enumerate(text.splitlines(), 1):
if GLYPHS.search(line) or _semicolons(line) or ONE_PLURAL.search(line):
+2 -2
View File
@@ -42,7 +42,7 @@ class SemanticManifestValidation(unittest.TestCase):
(install / "synthetic.json").write_text(
json.dumps({"files": None}), encoding="utf-8"
)
api = root / "docs" / "api" / "v1"
api = root / "website" / "public" / "api" / "v1"
api.mkdir(parents=True)
(api / "catalog.json").write_text(
json.dumps({"count": 1, "items": None}), encoding="utf-8"
@@ -58,7 +58,7 @@ class SemanticManifestValidation(unittest.TestCase):
text = output.getvalue().replace("\\", "/")
self.assertIn("database.json", text)
self.assertIn("install/synthetic.json", text)
self.assertIn("docs/api/v1/catalog.json", text)
self.assertIn("website/public/api/v1/catalog.json", text)
self.assertIn("files", text)
self.assertIn("items", text)
self.assertNotIn("Traceback", text)
+1 -1
View File
@@ -148,7 +148,7 @@ def _wiki_copy(tmp: str) -> Path:
class NavFromFrontMatter(unittest.TestCase):
def _nav(self, wiki: Path) -> dict:
nav = generate_site.generate_mkdocs_nav({}, {}, {}, {}, wiki)
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):
+54 -41
View File
@@ -21,7 +21,10 @@ from dataclasses import dataclass, field
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
DOCS = REPO_ROOT / "docs"
sys.path.insert(0, str(REPO_ROOT))
from tests import sitepages # noqa: E402
DOCS = sitepages.CONTENT
# Level 0: the Start here pages, with their own ceiling on words.
START_PAGES = {
@@ -142,15 +145,19 @@ def guide_part(markdown: str) -> str:
def parse(markdown: str) -> Page:
"""Blocks of prose a reader sees, code and HTML left out."""
"""Blocks of prose a reader sees, code and HTML left out.
A collapsed block (<details>) counts, but not before the first action:
the reader opens it on purpose. Of a group of tabs the first is read and
the others only stand in for it.
"""
text = guide_part(markdown)
text = re.sub(r"<script.*?</script>", "", text, flags=re.S)
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
page = Page()
seen_words = 0
collapsed_indent: int | None = None
tab_indent: int | None = None # the tabs of a group a reader picks one of
hidden_indent: int | None = None
details = 0 # depth of collapsed blocks
tab = 0 # the tab of a group being read, 0 outside a group
fence: str | None = None
current: list[str] = []
kind = "paragraph"
@@ -162,8 +169,7 @@ def parse(markdown: str) -> Page:
body = strip_inline(" ".join(current))
if WORD.search(body):
term = page.blocks[-1].text.lower() if kind == "definition" and page.blocks else ""
block = Block(kind, body, collapsed_indent is not None, term,
hidden_indent is not None)
block = Block(kind, body, details > 0, term, tab > 1)
page.blocks.append(block)
if (page.before_action is None and not block.collapsed
and not block.hidden and kind not in ("table", "heading")):
@@ -185,45 +191,52 @@ def parse(markdown: str) -> Page:
if stripped.startswith(fence):
fence = None
continue
if stripped and tab_indent is not None and indent <= tab_indent and not stripped.startswith("=== "):
flush()
tab_indent = hidden_indent = None
if stripped.startswith(("```", "~~~")):
fence = stripped[:3]
if hidden_indent is None:
if tab < 2:
act()
continue
if collapsed_indent is not None and stripped and indent <= collapsed_indent:
flush()
collapsed_indent = None
if not stripped:
flush()
continue
if re.match(r"(\?\?\?\+?|!!!) ", stripped):
flush()
if stripped.startswith("??? "):
collapsed_indent = indent
if stripped.startswith("import ") and stripped.endswith(";"):
continue
if stripped.startswith("=== "):
flush()
if tab_indent == indent:
hidden_indent = indent
else:
tab_indent, hidden_indent = indent, None
continue
if stripped.startswith("<") or stripped.startswith("</"):
if stripped.startswith(":::"):
flush()
continue
if ".md-button" in stripped:
if stripped.startswith("<details"):
flush()
details += 1
continue
if stripped.startswith("</details>"):
flush()
details = max(0, details - 1)
continue
if stripped.startswith("<Tabs"):
flush()
tab = 0
continue
if stripped.startswith("<TabItem"):
flush()
tab += 1
continue
if stripped.startswith("</Tabs>"):
flush()
tab = 0
continue
if "rb-button" in stripped or stripped.startswith("<FilePicker"):
act()
continue
if re.match(r"</?(?!abbr\b|a\b|code\b|strong\b|em\b)[A-Za-z]", stripped):
flush()
continue
if stripped.startswith("#"):
flush()
if run:
page.list_runs.append(run)
run = 0
kind = "heading"
current = [stripped.lstrip("#").strip()]
current = [re.sub(r"\s*\{[^}]*\}\s*$", "", stripped.lstrip("#").strip())]
flush()
kind = "paragraph"
continue
@@ -311,25 +324,23 @@ def held_pages(docs: Path) -> list[tuple[str, str, int]]:
"""(relative path, level, ceiling) for every page the budgets hold."""
pages = [(name, "start", ceiling) for name, ceiling in START_PAGES.items()]
pages += [
(str(path.relative_to(docs)), "setup", SETUP_CEILING)
for path in sorted((docs / "platforms").glob("*.md"))
if path.name != "index.md"
(sitepages.name(path), "setup", SETUP_CEILING)
for path in sorted((docs / "platforms").glob("*.md*"))
if path.stem != "index"
and "\n## Where the files go" in path.read_text(encoding="utf-8")
]
pages.append(("platforms/index.md", "setup", SETUP_INDEX_CEILING))
return [(name, level, ceiling) for name, level, ceiling in pages if (docs / name).is_file()]
return [(name, level, ceiling) for name, level, ceiling in pages
if sitepages.page(name).is_file()]
def _docs() -> Path:
if not (DOCS / "index.md").is_file():
if os.environ.get("CI"):
raise AssertionError("CI generates docs/ before the suite")
raise unittest.SkipTest("docs/ not generated (python scripts/generate_site.py)")
return DOCS
return sitepages.require()
def _load(name: str) -> Page:
return parse((_docs() / name).read_text(encoding="utf-8"))
_docs()
return parse(sitepages.page(name).read_text(encoding="utf-8"))
class CopyBudgets(unittest.TestCase):
@@ -400,8 +411,10 @@ class TheMeasureItself(unittest.TestCase):
SAMPLE = (
"---\nlevel: start\n---\n\n# Title\n\nOne two three. Four five.\n\n"
"- Item **one**.\n- Item [two](x.md).\n\n```sh\nnot counted at all\n```\n\n"
"??? note \"Hidden\"\n\n Collapsed words here.\n\n"
"=== \"A\"\n\n First tab.\n\n=== \"B\"\n\n Second tab words.\n\n"
'<details class="rb-details rb-details-note">\n<summary>Hidden</summary>\n\n'
"Collapsed words here.\n\n</details>\n\n"
'<Tabs syncKey="os">\n<TabItem label="A">\n\nFirst tab.\n\n</TabItem>\n'
'<TabItem label="B">\n\nSecond tab words.\n\n</TabItem>\n</Tabs>\n\n'
"| A | B |\n|---|---|\n| cell | cell |\n\n## Reference: deep\n\nNot read.\n"
)
@@ -429,7 +442,7 @@ def report(docs: Path) -> str:
rows = ["| Page | Words | Before action | Max block | Median / max sentence |",
"|---|---:|---:|---:|---|"]
for name, _level, _ceiling in held_pages(docs):
m = measure(parse((docs / name).read_text(encoding="utf-8")))
m = measure(parse(sitepages.page(name).read_text(encoding="utf-8")))
before = "none" if m["before_action"] is None else m["before_action"]
rows.append(f"| {name} | {m['words']} | {before} | {m['max_block']} | "
f"{m['median_sentence']:g} / {m['max_sentence']} |")
+2 -1
View File
@@ -36,7 +36,8 @@ class SiteCoreCounts(unittest.TestCase):
page = generate_site.generate_cross_reference(coverages, profiles)
shown = {
m.group(1): int(m.group(2))
for m in re.finditer(r'\?\?\? abstract "([^"]+)"\n\n.*\n\n {4}\*\*(\d+) cores', page)
for m in re.finditer(
r'<summary>([^<]+)</summary>\n\n.*\n\n\*\*(\d+) cores', page)
}
for name, cov in coverages.items():
with self.subTest(platform=name):
+8 -5
View File
@@ -86,7 +86,7 @@ class ACardKeepsItsAddress(unittest.TestCase):
files = [{"name": "DUKE3D.GRP", "md5": "aaaa1111bbbb"}]
page = _page("raze", {"type": "standalone", "files": files})
self.assertIn('<span id="file-duke3d-grp"></span>', page)
self.assertIn("{ #file-duke3d-grp-aaaa1111 .rb-file-title }", page)
self.assertIn('<h3 id="file-duke3d-grp-aaaa1111" class="rb-file-title"', page)
# A card already holding the plain id gives it to no one else.
files = [{"name": "x.bin"}, {"name": "x.bin", "sha1": "ab" * 20}]
anchors = gs.emulator_file_anchors(files)
@@ -121,8 +121,8 @@ class WhatASearchSnippetSays(unittest.TestCase):
"only validate_schemas.py and this")
def test_structured_url_is_encoded_like_the_canonical(self):
"""mkdocs encodes idtech4a++ as idtech4a%2B%2B in the canonical link
and the sitemap, and the JSON-LD named the unencoded URL."""
"""The canonical link and the sitemap encode idtech4a++ as
idtech4a%2B%2B, and the JSON-LD named the unencoded URL."""
import json
import tempfile
@@ -132,8 +132,11 @@ class WhatASearchSnippetSays(unittest.TestCase):
page.write_text("# idTech4A++\n\nSome text long enough to be a description.\n")
gs.decorate_markdown_pages(Path(tmp))
text = page.read_text()
data = json.loads(text.split('<script type="application/ld+json">')[1]
.split("</script>")[0].replace("<\\/", "</"))
import yaml
front = yaml.safe_load(text.split("---\n")[1])
script = next(entry for entry in front["head"] if entry["tag"] == "script")
data = json.loads(script["content"].replace("<\\/", "</"))
self.assertEqual(data["url"],
"https://abdess.github.io/retrobios/emulators/idtech4a%2B%2B/")
+17 -14
View File
@@ -236,7 +236,7 @@ class SiteReferenceContracts(unittest.TestCase):
decorate_markdown_pages(self.docs)
decorate_markdown_pages(self.docs)
content = page.read_text()
self.assertEqual(content.count('type="application/ld+json"'), 1)
self.assertEqual(content.count("type: application/ld+json"), 1)
self.assertIn("Current RetroBIOS verification gaps", content)
self.assertIn('"@type":"TechArticle"', content)
@@ -357,11 +357,11 @@ class AVariantShowsWhoPinsIt(unittest.TestCase):
platforms={"c" * 40: {"batocera", "retrobat"}}, emulators={"a" * 40: {"mame"}})
page = generate_site.generate_system_page(
"Entex", {"Adventure Vision": [primary, variant]}, consumers)
block = page[page.index('??? note "Variants (1)"'):]
self.assertIn(" - Collection path: `bios/Entex/Adventure Vision/.variants/advision.zip.1c086064`", block)
block = page[page.index("<summary>Variants (1)</summary>"):]
self.assertIn("- Collection path: `bios/Entex/Adventure Vision/.variants/advision.zip.1c086064`", block)
self.assertIn("[batocera](../platforms/batocera.md)", block)
self.assertIn("[retrobat](../platforms/retrobat.md)", block)
self.assertIn('`' + "c" * 40 + '`', block)
self.assertIn('<code class="rb-hash">' + "c" * 40 + "</code>", block)
self.assertIn("Catalogued", block)
@@ -402,10 +402,10 @@ class EmulatorPagesPrintWholeHashes(unittest.TestCase):
from generate_site import _hash_spans
sha1 = "0c3de403b524107809fa9308b730d60e8e41ba93"
self.assertIn(f"`{sha1}`", _hash_spans(sha1))
self.assertIn(f">{sha1}</code>", _hash_spans(sha1))
both = _hash_spans(f"{'a' * 64}, {'b' * 64}")
self.assertIn("`" + "a" * 64 + "`", both)
self.assertIn("`" + "b" * 64 + "`", both)
self.assertIn(">" + "a" * 64 + "</code>", both)
self.assertIn(">" + "b" * 64 + "</code>", both)
self.assertNotIn("...", both)
@@ -724,15 +724,16 @@ class AFileHasAnAddressByContent(unittest.TestCase):
[{"sha1": "ab" * 20, "url": "systems/roland/#sha1-" + "ab" * 6}],
)
self.assertIn('id="rb-file-lookup-status"', generate_site.generate_files_page())
script = (Path(__file__).resolve().parents[1] / "docs_assets" / "site.js").read_text()
script = (Path(__file__).resolve().parents[1] / "website" / "src" / "scripts"
/ "site.ts").read_text()
self.assertIn("api/v1/lookup/", script)
self.assertIn('params.has("sha1")', script)
self.assertIn("params.has('sha1')", script)
from cross_reference import FileConsumers # noqa: PLC0415
card = "\n".join(generate_site._system_file_card(
{"name": "x.rom", "sha1": "ab" * 20, "size": 1}, FileConsumers({"files": {}}),
))
self.assertIn("../../files/?sha1=" + "ab" * 20, card)
self.assertIn("/retrobios/files/?sha1=" + "ab" * 20, card)
class ACardNamesThePacksThatInstallIt(unittest.TestCase):
@@ -825,7 +826,7 @@ class EveryAcceptedHashIsItsOwnWord(unittest.TestCase):
md5s = ["96cf" + "0" * 28, "d5af" + "0" * 28]
text = _hash_spans(",".join(md5s))
for md5 in md5s:
self.assertIn(f"`{md5}`</span>", text)
self.assertIn(f'<code class="rb-hash">{md5}</code>', text)
class AMismatchNamesTheDumpTheCollectionHolds(unittest.TestCase):
@@ -924,12 +925,14 @@ class NestedProfileBlocksRenderAsLists(unittest.TestCase):
lines = generate_site._render_platform_details(
{"platform_details": {"megacd": {"root": "x"}, "note": "y"}}
)
lines[0] = lines[0].replace("???+", "!!!")
try:
import markdown
except ImportError:
self.skipTest("markdown is not installed")
html = markdown.markdown("\n".join(lines), extensions=["admonition"])
# The block's body, read as the engine reads Markdown inside HTML.
body = lines[lines.index("<summary>Platform details</summary>") + 1:
lines.index("</details>")]
html = markdown.markdown("\n".join(body))
self.assertIn("<li>root: x</li>", html)
self.assertNotIn("**", html)
@@ -959,7 +962,7 @@ class TheOneLinerIsWhereUsersLand(unittest.TestCase):
"verified": 0, "missing": 0, "details": [], "config": {"systems": {}}}
page = generate_site.generate_platform_page("batocera", cov)
self.assertIn("install.sh | sh -s -- --platform batocera", page)
self.assertLess(page.index("--platform batocera"), page.index("md-button"))
self.assertLess(page.index("--platform batocera"), page.index("rb-button"))
# Batocera runs the command on the device itself. A platform detected
# on Windows gets the PowerShell form.
cov = dict(cov, platform="RetroArch", mode="existence")
+43 -27
View File
@@ -19,12 +19,14 @@ 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 = REPO_ROOT / "docs"
DOCS = sitepages.CONTENT
ORDER = list(generate_site.LEVELS)
LINK = re.compile(r"\[[^\]]*\]\(([^)\s#]+\.md)(?:#[^)]*)?\)")
# 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)
@@ -38,13 +40,7 @@ def _which_pack() -> str:
def _docs_pages() -> list[Path]:
if not DOCS.is_dir() or not (DOCS / "index.md").is_file():
if os.environ.get("CI"):
raise AssertionError("CI generates docs/ before the suite")
raise unittest.SkipTest("docs/ not generated (python scripts/generate_site.py)")
skipped = set(generate_site.LOCAL_NOTE_DIRS)
return [p for p in sorted(DOCS.rglob("*.md"))
if not set(p.relative_to(DOCS).parts[:-1]) & skipped]
return [p for p in sitepages.pages() if p.name not in ("404.md",)]
def _level(page: Path) -> str:
@@ -55,14 +51,14 @@ 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_mkdocs_nav({}, {}, {}, {})
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_mkdocs_nav({}, {}, {}, {})
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")
@@ -70,8 +66,10 @@ class FiveTabs(unittest.TestCase):
self.assertEqual(tabs["Contribute"][0], "contributing.md")
def test_the_breadcrumbs_are_on(self):
source = (REPO_ROOT / "scripts" / "generate_site.py").read_text(encoding="utf-8")
self.assertIn(" - navigation.path\n", source)
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):
@@ -100,8 +98,17 @@ class EveryPlatformOnThePicker(unittest.TestCase):
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."""
# mkdocs.yml is what generate_site writes, and CI checks it is current.
nav = (Path(__file__).resolve().parents[1] / "mkdocs.yml").read_text(encoding="utf-8")
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(
@@ -116,7 +123,7 @@ class EveryPlatformOnThePicker(unittest.TestCase):
key: {"platform": load_platform_config(key, str(REPO_ROOT / "platforms"))["platform"]}
for key in _registered()
}
nav = generate_site.generate_mkdocs_nav(coverages, {}, {}, registry)
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)}
@@ -166,7 +173,12 @@ class EveryPageHasALevel(unittest.TestCase):
level = _level(page)
with self.subTest(page=str(page.relative_to(DOCS))):
self.assertIn(level, generate_site.LEVELS)
self.assertIn(generate_site.level_label(level), text)
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:
@@ -177,9 +189,8 @@ class EveryPageHasALevel(unittest.TestCase):
generate_site.decorate_markdown_pages(Path(tmp))
text = page.read_text()
self.assertEqual(text.count("level: start"), 1)
self.assertEqual(text.count(generate_site.level_label("start")), 1)
self.assertIn(" boost: 5\n", text)
self.assertIn("# FAQ\n", text)
self.assertIn("searchWeight: 10\n", text)
self.assertIn("title: FAQ\n", text)
class DeepLinksCarryALabel(unittest.TestCase):
@@ -189,8 +200,11 @@ class DeepLinksCarryALabel(unittest.TestCase):
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"):
@@ -201,7 +215,7 @@ class DeepLinksCarryALabel(unittest.TestCase):
if re.match(r"#{1,3} ", line):
heading = line.lstrip("#").strip()
for match in LINK.finditer(line):
target = (page.parent / match.group(1)).resolve()
target = by_url.get(match.group(1))
if target not in levels:
continue
deeper = levels[target]
@@ -218,18 +232,20 @@ class GuidePagesEndOnANextStep(unittest.TestCase):
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"):
path = DOCS / page if page.startswith(("index", "which", "platforms")) else REPO_ROOT / page
if not path.is_file():
_docs_pages()
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)
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/."""
text = (DOCS / "platforms" / "batocera.md").read_text(encoding="utf-8") \
if (DOCS / "platforms" / "batocera.md").is_file() else _docs_pages()
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",
+8 -2
View File
@@ -200,9 +200,15 @@ class SharingCards(unittest.TestCase):
ET.fromstring(sitediagrams.og_card_svg(section, b"png"))
def test_the_template_names_every_section(self):
template = (ROOT / "docs_assets" / "overrides" / "main.html").read_text(encoding="utf-8")
named = set(re.findall(r"section = '([a-z]+)'", template))
import sitestarlight # noqa: PLC0415
named = {sitestarlight.og_section("index.md", "")} | {
sitestarlight.og_section("wiki/page.md", level)
for level in ("start", "setup", "explore", "reference", "contribute")}
self.assertEqual(named, set(sitediagrams.OG_SECTIONS))
for section in named:
with self.subTest(section=section):
self.assertTrue((ROOT / "docs_assets" / "og" / f"{section}.png").is_file())
class ImagesStillNeeded(unittest.TestCase):
+24 -18
View File
@@ -126,16 +126,18 @@ class StructuredBlocks(unittest.TestCase):
page = self.render(self.profile("firmware_titles", [
{"id": "0005001b-10042400", "name": "Shared fonts",
"content": ["CafeCn.ttf", "CafeKr.ttf"]}]))
self.assertIn(" - id: 0005001b-10042400, name: Shared fonts\n", page)
self.assertIn(" - **content:**", page)
self.assertIn(" - CafeKr.ttf", page)
self.assertIn("\n- id: 0005001b-10042400, name: Shared fonts\n", page)
self.assertIn("\n - **content:**", page)
self.assertIn("\n - CafeKr.ttf", page)
def test_a_folded_note_stays_inside_its_block(self):
page = self.render(self.profile("analysis", {"tmss": {
"note": "The core points\nsystem/tmss_path at the system directory."}}))
self.assertIn("- **note:** The core points system/tmss_path at the", page)
body = page.split("\n")[1:]
self.assertEqual([line for line in body if line and not line.startswith(" ")], [])
# Nothing of the note is left after the block closes.
lines = page.split("\n")
end = lines.index("</details>")
self.assertEqual([line for line in lines[end + 1:] if line.strip()], [])
def test_a_long_note_is_not_cut(self):
note = "word " * 80 + "end."
@@ -315,7 +317,7 @@ class TheSearchLandsOnTheCard(unittest.TestCase):
self.assertEqual(len(headings), 11)
self.assertTrue(headings[0].endswith("{ #system-00 }"))
self.assertNotIn('<a id="system-00"></a>', lines)
self.assertIn('??? note "1 file (OK)"', lines)
self.assertIn("<summary>1 file (OK)</summary>", lines)
def test_an_emulator_card_is_a_heading_with_its_anchor(self):
import generate_site
@@ -323,8 +325,8 @@ class TheSearchLandsOnTheCard(unittest.TestCase):
entry = {"name": "scph5501.bin", "required": True}
lines = generate_site._render_emulator_file(
entry, {"files": [entry]}, None, [entry], lambda f: True, "file-scph5501-bin")
self.assertTrue(lines[2].startswith("### `scph5501.bin` "))
self.assertTrue(lines[2].endswith("{ #file-scph5501-bin .rb-file-title }"))
self.assertTrue(lines[2].startswith('<h3 id="file-scph5501-bin" class="rb-file-title"'))
self.assertIn("<code>scph5501.bin</code>", lines[2])
def test_a_system_card_is_a_heading_with_its_anchor(self):
import generate_site
@@ -332,8 +334,8 @@ class TheSearchLandsOnTheCard(unittest.TestCase):
entry = {"name": "a.bin", "sha1": "a" * 40, "md5": "b" * 32, "size": 3}
lines = generate_site._system_file_card(entry, FileConsumers({"files": {}}))
self.assertTrue(lines[2].startswith("### `a.bin` "))
self.assertTrue(lines[2].endswith("{ #sha1-aaaaaaaaaaaa .rb-file-title }"))
self.assertTrue(lines[2].startswith('<h3 id="sha1-aaaaaaaaaaaa" class="rb-file-title"'))
self.assertIn("<code>a.bin</code>", lines[2])
class TheSearchSkipsLinkLists(unittest.TestCase):
@@ -353,11 +355,14 @@ class TheSearchSkipsLinkLists(unittest.TestCase):
consumers.emulators = {sha1: {"duckstation"}}
entry = {"name": "a.bin", "sha1": sha1, "md5": "b" * 32, "size": 3}
text = "\n".join(generate_site._system_file_card(entry, consumers))
searched, skipped = text.split("<section data-search-exclude markdown>")
self.assertIn("`a.bin`", searched)
searched, _sep, skipped = text.partition("<section data-pagefind-ignore>")
self.assertIn("<code>a.bin</code>", searched)
self.assertNotIn("- MD5:", searched)
for line in ("- MD5:", "- Emulators:", "- Platforms:", "- Also installed by:"):
for line in ("- MD5:", "- Emulators:", "- Also installed by:"):
self.assertIn(line, skipped)
# The platforms line names the file as each setup does: the site
# search reads it, so a file name finds its card.
self.assertIn("- Platforms:", searched + skipped.split("</section>", 1)[1])
def test_a_game_data_card_skips_its_name_too(self):
import generate_site
@@ -366,7 +371,8 @@ class TheSearchSkipsLinkLists(unittest.TestCase):
entry = {"name": "0.ogg", "sha1": "a" * 40, "size": 3}
lines = generate_site._system_file_card(
entry, FileConsumers({"files": {}}), searchable=False)
self.assertTrue(lines[2].endswith("data-search-exclude }"))
self.assertIn("data-pagefind-ignore", lines[2])
self.assertNotIn("data-pagefind-weight", lines[2])
def test_runs_alternate_in_card_order(self):
import generate_site
@@ -377,10 +383,10 @@ class TheSearchSkipsLinkLists(unittest.TestCase):
out = [line for line in generate_site._search_skipped_apart(lines) if line]
self.assertEqual(out, [
"- Path: `p`",
'<div class="rb-search-skip" data-search-exclude markdown>',
'<div class="rb-search-skip" data-pagefind-ignore>',
"- SHA1: x", "- Source: `a.c:1`", "- Platforms: p", "</div>",
"- Destination: `d`",
'<div class="rb-search-skip" data-search-exclude markdown>',
'<div class="rb-search-skip" data-pagefind-ignore>',
"- Contents (1 members):", " - `m`", "</div>",
"- Note",
])
@@ -391,8 +397,8 @@ class TheSearchSkipsLinkLists(unittest.TestCase):
short = "| a |\n|---|\n| 1 |"
long = " | a |\n |---|\n" + "\n".join(f" | {'x' * 50} |" for _ in range(80))
text = generate_site.tables_out_of_search(f"# T\n\n{short}\n\n{long}\n")
self.assertEqual(text.count("data-search-exclude"), 1)
self.assertIn(" <section data-search-exclude markdown>\n\n | a |", text)
self.assertEqual(text.count("data-pagefind-ignore"), 1)
self.assertIn(" <section data-pagefind-ignore>\n\n | a |", text)
self.assertIn(short, text)
+1 -1
View File
@@ -143,7 +143,7 @@ class AnIdNamesOneCardPerPage(unittest.TestCase):
"path": "bios/Nintendo/DS/a.bin"}
page = gs.generate_system_page(
"Nintendo", {"DS": [entry], "DSi": [entry]}, FileConsumers({"files": {}}))
self.assertEqual(page.count("#sha1-aaaaaaaaaaaa .rb-file-title"), 1)
self.assertEqual(page.count('id="sha1-aaaaaaaaaaaa" class="rb-file-title"'), 1)
self.assertEqual(page.count("rb-sys-file"), 2)
+23 -43
View File
@@ -101,9 +101,9 @@ class PlatformDetails(unittest.TestCase):
"root": "<system_dir>/x",
}}}
)
self.assertIn(" - extensions_tried: .bin, .zip", lines)
self.assertIn(" - hle_available: no", lines)
self.assertIn(" - root: &lt;system_dir>/x", lines)
self.assertIn(" - extensions_tried: .bin, .zip", lines)
self.assertIn(" - hle_available: no", lines)
self.assertIn(" - root: &lt;system_dir>/x", lines)
class RenderedSiteValidation(unittest.TestCase):
@@ -112,10 +112,8 @@ class RenderedSiteValidation(unittest.TestCase):
self.root = Path(self.temp.name)
self.site = self.root / "site"
self.site.mkdir()
self.config = self.root / "mkdocs.yml"
self.config.write_text(
"site_url: https://example.test/retrobios/\n", encoding="utf-8"
)
# The path the site is served under.
self.config = "/retrobios/"
def tearDown(self) -> None:
self.temp.cleanup()
@@ -174,8 +172,7 @@ class TheSiteStaysWithinItsBudgets(unittest.TestCase):
self.temp = tempfile.TemporaryDirectory(dir=TMP_ROOT)
self.site = Path(self.temp.name) / "site"
self.site.mkdir()
self.config = Path(self.temp.name) / "mkdocs.yml"
self.config.write_text("site_url: https://example.test/retrobios/\n", encoding="utf-8")
self.config = "/retrobios/"
def tearDown(self) -> None:
self.temp.cleanup()
@@ -214,17 +211,16 @@ class TheSiteStaysWithinItsBudgets(unittest.TestCase):
self.assertTrue(any("drawn by a script at runtime" in issue for issue in issues))
def test_a_search_index_over_its_budget(self):
import random
import validate_site as module
(self.site / "search").mkdir()
rng = random.Random(1)
noise = "".join(rng.choice("0123456789abcdef") for _ in range(module.MAX_SEARCH_INDEX_BYTES + 1))
(self.site / "search" / "search_index.json").write_text(noise, encoding="utf-8")
index = self.site / "pagefind" / "index"
index.mkdir(parents=True)
(index / "en_big.pf_index").write_bytes(b"x" * (module.MAX_PAGEFIND_CHUNK_BYTES + 1))
(self.site / "pagefind" / "pagefind.en_x.pf_meta").write_bytes(
b"x" * (module.MAX_PAGEFIND_META_BYTES + 1))
issues = self._issues("")
self.assertTrue(any("search_index.json" in issue and "gzip" not in issue for issue in issues))
self.assertTrue(any("search_index.json" in issue and "gzip" in issue for issue in issues))
self.assertTrue(any("en_big.pf_index" in issue for issue in issues), issues)
self.assertTrue(any(".pf_meta" in issue for issue in issues), issues)
class UnrenderedMarkdown(unittest.TestCase):
@@ -327,9 +323,12 @@ class TheSitemapIsAnnouncedWhereCrawlersLook(unittest.TestCase):
never found that way."""
def test_the_head_names_it_and_no_robots_is_written(self):
# Starlight links the sitemap from every page's head when the site
# has an address, and the config keeps sitemap.xml at its old path.
root = Path(__file__).resolve().parents[1]
override = (root / "docs_assets" / "overrides" / "main.html").read_text()
self.assertIn('rel="sitemap"', override)
config = (root / "website" / "astro.config.mjs").read_text()
self.assertIn("site: SITE", config)
self.assertIn("sitemap.xml", config)
source = (root / "scripts" / "generate_site.py").read_text()
self.assertNotIn('"robots.txt"),\n "User-agent', source)
self.assertNotIn("Sitemap: https://", source)
@@ -351,11 +350,10 @@ class PlatformPagesPrintTheDeclaredCrc32(unittest.TestCase):
only its sha1 and md5, so the crc a clrmamepro user pastes found nothing."""
def test_the_rendered_page(self):
page = ROOT / "docs" / "platforms" / "retroarch.md"
if not page.is_file():
if os.environ.get("CI"):
self.fail("CI generates docs/ before the suite")
self.skipTest("site not generated")
from tests import sitepages # noqa: PLC0415
sitepages.require()
page = sitepages.page("platforms/retroarch.md")
self.assertIn("CRC32: `91e93107`", page.read_text(encoding="utf-8"))
@@ -378,7 +376,7 @@ class PlainHouseStyle(unittest.TestCase):
}
def test_the_stylesheet_has_none_of_the_patterns(self):
css = (ROOT / "docs_assets" / "extra.css").read_text(encoding="utf-8")
css = (ROOT / "website" / "src" / "styles" / "retrobios.css").read_text(encoding="utf-8")
for label, pattern in self.PATTERNS.items():
with self.subTest(pattern=label):
self.assertIsNone(pattern.search(css), label)
@@ -392,21 +390,3 @@ class PlainHouseStyle(unittest.TestCase):
first = page.read_text(encoding="utf-8").splitlines()[0]
with self.subTest(page=page.name):
self.assertFalse(first.endswith(" - RetroBIOS"), first)
class APreviewLeavesTheTrackedConfig(unittest.TestCase):
"""--docs-dir moved the pages but still rewrote the tracked mkdocs.yml."""
def test_another_folder_gets_its_own_config(self):
from generate_site import _mkdocs_config
path, text = _mkdocs_config(Path("docs"), "hooks:\n - scripts/mkdocs_hooks.py\n")
self.assertEqual(path, Path("mkdocs.yml"))
path, text = _mkdocs_config(Path("tmp/preview"), "hooks:\n - scripts/mkdocs_hooks.py\n")
self.assertEqual(path, Path("tmp/preview.mkdocs.yml"))
self.assertTrue(text.startswith("docs_dir: preview\n"))
self.assertIn("../scripts/mkdocs_hooks.py", text)
if __name__ == "__main__":
unittest.main()
+27 -18
View File
@@ -18,8 +18,9 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import generate_site # noqa: E402
import mkdocs_hooks # noqa: E402
import sitestarlight # noqa: E402
import siterender # noqa: E402
import sitemarkup as markup # noqa: E402
from cross_reference import FileConsumers # noqa: E402
@@ -54,7 +55,7 @@ class ASystemPageOverItsBudgetIsSplit(unittest.TestCase):
self.assertIn("## Big", summary)
self.assertIn("## Small", summary)
# The small section keeps its cards in place.
self.assertIn("{ #sha1-" + f"{900:012x}", summary)
self.assertIn('id="sha1-' + f"{900:012x}", summary)
parts = [stem for stem in pages if stem.startswith("maker-big")]
self.assertGreater(len(parts), 1)
self.assertEqual(parts[0], "maker-big")
@@ -73,7 +74,9 @@ class ASystemPageOverItsBudgetIsSplit(unittest.TestCase):
def test_a_console_on_one_page_is_linked_directly(self):
self.consoles["Big"] = self.consoles["Big"][:12]
whole = self._pages(10**9)["maker"][0]
# The budget is measured in the Markdown it was set on.
with markup.using("mkdocs"):
whole = self._pages(10**9)["maker"][0]
pages = self._pages(len(whole) - 1)
self.assertIn("maker-big", pages)
self.assertNotIn("maker-big-2", pages)
@@ -99,8 +102,8 @@ class AnEmulatorWithTooManyCardsIsSplit(unittest.TestCase):
for anchor in anchors:
self.assertIn(f'<span id="{anchor}"></span>', main)
self.assertIn('class="rb-card-index"', main)
self.assertNotIn(".rb-file-title", main)
self.assertIn(f"{{ #{anchors[0]} .rb-file-title }}", pages["big-files-1"])
self.assertNotIn("rb-file-title", main)
self.assertIn(f'id="{anchors[0]}" class="rb-file-title"', pages["big-files-1"])
def test_cards_are_linked_on_the_page_that_holds_them(self):
files = [{"name": f"set{i:04d}.zip"} for i in range(generate_site.EMULATOR_CARDS_PER_PAGE + 1)]
@@ -147,21 +150,27 @@ class TheNavigationOpensOneInitial(unittest.TestCase):
class TheTableOfContentsOfACardPageStopsAtLevelTwo(unittest.TestCase):
@staticmethod
def _item(level: int, children: list | None = None) -> SimpleNamespace:
return SimpleNamespace(level=level, children=children or [])
@staticmethod
def _contents(rel: str, sections: int):
import yaml # noqa: PLC0415
body = "# T\n\n" + "".join(f"## S{i}\n\n### card\n\n" for i in range(sections))
_target, text = sitestarlight.finalize_page(
rel, body, level="", weight=None, browser_title="T", description="d",
structured={}, terms={})
return yaml.safe_load(text.split("---\n")[1]).get("tableOfContents", "default")
def test_cards_leave_the_toc_and_wiki_pages_keep_level_three(self):
def page(uri: str) -> SimpleNamespace:
h2 = self._item(2, [self._item(3)])
return SimpleNamespace(file=SimpleNamespace(src_uri=uri),
toc=SimpleNamespace(items=[self._item(1, [h2])]))
for uri, kept in (("systems/sony.md", 0), ("platforms/retroarch.md", 0),
("emulators/mame.md", 0), ("wiki/faq.md", 1)):
current = page(uri)
mkdocs_hooks.on_page_content("", current, None, None)
with self.subTest(uri=uri):
self.assertEqual(len(current.toc.items[0].children[0].children), kept)
level_two = {"minHeadingLevel": 2, "maxHeadingLevel": 2}
for rel, sections, expected in (
("systems/sony.md", 3, level_two), ("platforms/retroarch.md", 5, level_two),
("emulators/mame.md", 3, level_two), ("wiki/faq.md", 3, "default"),
# One or two headings over hundreds of cards: no table at all,
# whose script watched every block of the page.
("emulators/mame-files-2.md", 1, False), ("systems/arcade-arcade.md", 2, False),
):
with self.subTest(rel=rel):
self.assertEqual(self._contents(rel, sections), expected)
if __name__ == "__main__":
+17 -18
View File
@@ -79,15 +79,8 @@ class DeploySiteTriggers(unittest.TestCase):
}
)
self.assertIn("generate_site.py", entry_points)
# mkdocs build runs the hooks mkdocs.yml names: a script of the build.
# The file carries Python tags safe_load refuses, but the list is plain.
mkdocs = (REPO_ROOT / "mkdocs.yml").read_text(encoding="utf-8")
hooks = re.search(r"^hooks:\n((?: - .+\n)+)", mkdocs, re.M)
entry_points += [
line.strip()[2:].removeprefix("scripts/")
for line in (hooks.group(1).splitlines() if hooks else [])
if line.strip()[2:].startswith("scripts/")
]
# The Starlight project is an input of its own build.
self.assertIn("website/**", patterns)
uncovered = sorted(
path
for path in closure(entry_points)
@@ -109,9 +102,9 @@ class DeploySiteTriggers(unittest.TestCase):
class ThePullRequestBuildsTheSite(unittest.TestCase):
"""mkdocs was installed by deploy-site.yml alone, which runs after the
merge, and validate.yml did not watch wiki/: a dead link or a line read as
a second heading passed every check and failed the deploy on main."""
"""The site build ran in deploy-site.yml alone, after the merge, and
validate.yml did not watch wiki/: a dead link or a line read as a second
heading passed every check and failed the deploy on main."""
@staticmethod
def _workflow(name: str) -> dict:
@@ -124,17 +117,23 @@ class ThePullRequestBuildsTheSite(unittest.TestCase):
runs = "\n".join(str(step.get("run", "")) for step in steps)
for command in (
"python scripts/generate_site.py",
"mkdocs build --strict",
"python scripts/validate_site.py",
"npm ci --prefix website",
"npm run build --prefix website",
"python scripts/validate_site.py --site-dir website/dist",
):
with self.subTest(command=command):
self.assertIn(command, runs)
for package in ("mkdocs-material", "pymdown-extensions"):
with self.subTest(package=package):
self.assertIn(package, runs)
# The same Node, from the official action pinned by commit, in both.
uses = {str(step.get("uses", "")) for step in steps}
deploy = {str(step.get("uses", ""))
for step in self._workflow("deploy-site.yml")["jobs"]["build"]["steps"]}
node = {use for use in uses if use.startswith("actions/setup-node@")}
self.assertEqual(len(node), 1)
self.assertTrue(node <= deploy)
self.assertRegex(next(iter(node)), r"@[0-9a-f]{40}$")
def test_the_pages_exist_before_the_suite_reads_them(self):
"""docs/ is gitignored. Built after the suite, the tests that read
"""The pages are gitignored. Built after the suite, the tests that read
the rendered pages found none and skipped on every CI run."""
steps = self._workflow("validate.yml")["jobs"]["run-tests"]["steps"]
runs = [str(step.get("run", "")) for step in steps]