mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-21 11:19:11 -05:00
The nested list structure stays, so each label renders bold instead of showing its asterisks. The platform details block becomes a list as well: its labels were paragraphs followed by items with no blank line between them. validate_site now reports any asterisks that reach a rendered page outside code, which runs in the deploy and in pipeline step 8b.
347 lines
14 KiB
Python
347 lines
14 KiB
Python
"""Tests for rendered-site metadata and link validation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
TMP_ROOT = ROOT / "tmp" / "tests"
|
|
TMP_ROOT.mkdir(parents=True, exist_ok=True)
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
|
|
from validate_site import validate_site # noqa: E402
|
|
|
|
|
|
def _page(title: str, description: str, body: str = "") -> str:
|
|
structured = json.dumps(
|
|
{"@context": "https://schema.org", "@type": "TechArticle"}
|
|
)
|
|
return f"""<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>{title}</title>
|
|
<meta name="description" content="{description}">
|
|
<link rel="canonical" href="https://example.test/retrobios/">
|
|
<script type="application/ld+json">{structured}</script>
|
|
</head>
|
|
<body><main><h1 id="top">{title}</h1>{body}</main></body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
class ProsePlaceholders(unittest.TestCase):
|
|
"""A path written with angle brackets reaches the reader as written."""
|
|
|
|
def test_a_placeholder_is_text_not_a_tag(self):
|
|
from siterender import _admonition_body, _escape_tags
|
|
|
|
self.assertEqual(
|
|
_escape_tags("gameProfiles/<title id>.ini under <system_dir>/Cemu"),
|
|
"gameProfiles/<title id>.ini under <system_dir>/Cemu",
|
|
)
|
|
self.assertIn("<title id>", _admonition_body("read <title id>.ini"))
|
|
|
|
def test_code_spans_and_links_are_left_alone(self):
|
|
from siterender import _escape_tags
|
|
|
|
self.assertEqual(_escape_tags("`<system_dir>/x`"), "`<system_dir>/x`")
|
|
self.assertEqual(_escape_tags("see <https://example.org/a>"), "see <https://example.org/a>")
|
|
self.assertEqual(_escape_tags("size < 4096"), "size < 4096")
|
|
|
|
|
|
class WikiSourceHeadings(unittest.TestCase):
|
|
def test_no_prose_line_starts_with_a_hash(self):
|
|
"""Python-Markdown reads `#79).` at the start of a line as a heading:
|
|
it asks for no space after the hash. A wrapped issue reference gave
|
|
the release page a second H1 and failed the site build."""
|
|
offenders = []
|
|
for page in sorted((ROOT / "wiki").glob("*.md")):
|
|
fenced = False
|
|
for number, line in enumerate(
|
|
page.read_text(encoding="utf-8").splitlines(), start=1
|
|
):
|
|
if line.lstrip().startswith(("```", "~~~")):
|
|
fenced = not fenced
|
|
elif not fenced and line.startswith("#") and not line.startswith(
|
|
("# ", "## ", "### ", "#### ", "##### ", "###### ")
|
|
):
|
|
offenders.append(f"{page.name}:{number}")
|
|
elif not fenced and line[:1].isspace() and line.lstrip().startswith("#"):
|
|
# Inside a list item or an admonition an indented hash is
|
|
# a heading too. Code that needs one goes in a fence.
|
|
offenders.append(f"{page.name}:{number}")
|
|
self.assertEqual(offenders, [])
|
|
|
|
def test_an_indented_hash_renders_as_a_heading(self):
|
|
"""What the check above guards: a wrapped `#79)` inside a list item."""
|
|
try:
|
|
import markdown
|
|
except ImportError:
|
|
self.skipTest("rendering needs markdown")
|
|
html = markdown.markdown(
|
|
"1. First paragraph.\n\n Second paragraph (see issue\n #79). More."
|
|
)
|
|
self.assertIn("<h1", html)
|
|
|
|
|
|
class PlatformDetails(unittest.TestCase):
|
|
def test_lists_and_flags_read_as_prose(self):
|
|
import generate_site
|
|
|
|
lines = generate_site._render_platform_details(
|
|
{"platform_details": {"megacd": {
|
|
"extensions_tried": [".bin", ".zip"],
|
|
"hle_available": False,
|
|
"root": "<system_dir>/x",
|
|
}}}
|
|
)
|
|
self.assertIn(" - extensions_tried: .bin, .zip", lines)
|
|
self.assertIn(" - hle_available: no", lines)
|
|
self.assertIn(" - root: <system_dir>/x", lines)
|
|
|
|
|
|
class RenderedSiteValidation(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.temp = tempfile.TemporaryDirectory(dir=TMP_ROOT)
|
|
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"
|
|
)
|
|
|
|
def tearDown(self) -> None:
|
|
self.temp.cleanup()
|
|
|
|
def test_valid_page_and_base_path_link_pass(self):
|
|
asset = self.site / "assets" / "file.json"
|
|
asset.parent.mkdir()
|
|
asset.write_text("{}\n", encoding="utf-8")
|
|
(self.site / "index.html").write_text(
|
|
_page(
|
|
"Home",
|
|
"Unique home description.",
|
|
'<a href="/retrobios/#top">Top</a>'
|
|
'<a href="/retrobios/assets/file.json">Data</a>'
|
|
'<img src="assets/logo.png" alt="Logo">',
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
(self.site / "assets" / "logo.png").write_bytes(b"png")
|
|
self.assertEqual(validate_site(self.site, self.config), [])
|
|
|
|
def test_broken_fragment_and_missing_alt_fail(self):
|
|
(self.site / "index.html").write_text(
|
|
_page(
|
|
"Home",
|
|
"Unique home description.",
|
|
'<a href="#absent">Missing</a><img src="missing.png">',
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
issues = validate_site(self.site, self.config)
|
|
self.assertTrue(any("missing fragment" in issue for issue in issues))
|
|
self.assertTrue(any("images missing alt" in issue for issue in issues))
|
|
self.assertTrue(any("broken local link" in issue for issue in issues))
|
|
|
|
def test_duplicate_search_metadata_fails(self):
|
|
(self.site / "index.html").write_text(
|
|
_page("Repeated", "Repeated description."), encoding="utf-8"
|
|
)
|
|
child = self.site / "child"
|
|
child.mkdir()
|
|
(child / "index.html").write_text(
|
|
_page("Repeated", "Repeated description."), encoding="utf-8"
|
|
)
|
|
issues = validate_site(self.site, self.config)
|
|
self.assertTrue(any("duplicate title" in issue for issue in issues))
|
|
self.assertTrue(any("duplicate description" in issue for issue in issues))
|
|
|
|
|
|
|
|
class UnrenderedMarkdown(unittest.TestCase):
|
|
"""The clownmdemu analysis block reached readers as "- **bios_hle:**
|
|
True": a list indented one level too deep, read as code."""
|
|
|
|
setUp = RenderedSiteValidation.setUp
|
|
tearDown = RenderedSiteValidation.tearDown
|
|
|
|
def test_asterisks_in_prose_fail_and_in_code_pass(self):
|
|
(self.site / "index.html").write_text(
|
|
_page("Home", "Unique home description.", "<p>- **bios_hle:** True</p>"),
|
|
encoding="utf-8",
|
|
)
|
|
issues = validate_site(self.site, self.config)
|
|
self.assertTrue(any("markdown left unrendered" in issue for issue in issues))
|
|
(self.site / "index.html").write_text(
|
|
_page("Home", "Unique home description.",
|
|
"<pre><code>a = 2 ** 8</code></pre><code>**/*.py</code>"),
|
|
encoding="utf-8",
|
|
)
|
|
self.assertEqual(validate_site(self.site, self.config), [])
|
|
|
|
|
|
class EveryTestDependencyIsInstalledInCi(unittest.TestCase):
|
|
"""The markdown rendering test skipped on every CI run: the test job
|
|
installed pyyaml and jsonschema only. A test that skips where it is
|
|
meant to run guards nothing."""
|
|
|
|
PACKAGES = {"yaml": "pyyaml", "jsonschema": "jsonschema", "markdown": "markdown"}
|
|
|
|
def test_the_test_job_installs_what_the_tests_import(self):
|
|
import ast
|
|
|
|
root = Path(__file__).resolve().parents[1]
|
|
local = (
|
|
{p.stem for p in (root / "scripts").rglob("*.py")}
|
|
| {p.name for p in (root / "scripts").iterdir() if p.is_dir()}
|
|
| {p.stem for p in (root / "tests").glob("*.py")}
|
|
| {"scripts", "tests", "install"}
|
|
)
|
|
imported: set[str] = set()
|
|
for path in (root / "tests").glob("*.py"):
|
|
for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))):
|
|
if isinstance(node, ast.Import):
|
|
imported.update(a.name.split(".")[0] for a in node.names)
|
|
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
|
|
imported.add(node.module.split(".")[0])
|
|
third_party = {
|
|
m for m in imported if m not in sys.stdlib_module_names and m not in local
|
|
}
|
|
workflow = (root / ".github" / "workflows" / "validate.yml").read_text()
|
|
job = workflow[workflow.index("run-tests:"):]
|
|
install_line = next(
|
|
line for line in job.splitlines() if "pip install" in line
|
|
)
|
|
missing = sorted(
|
|
m for m in third_party if self.PACKAGES.get(m, m) not in install_line
|
|
)
|
|
self.assertEqual(missing, [])
|
|
|
|
def test_a_contributor_installs_what_ci_installs(self):
|
|
# The contributing page installed pyyaml and jsonschema only, so the
|
|
# markdown test skipped on a contributor's machine and ran in CI.
|
|
from generate_readme import generate_contributing
|
|
|
|
command = next(
|
|
line for line in generate_contributing().splitlines() if "pip install" in line
|
|
)
|
|
guide = (Path(__file__).resolve().parents[1] / "wiki" / "testing-guide.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
for package in self.PACKAGES.values():
|
|
with self.subTest(package=package):
|
|
self.assertIn(package, command)
|
|
self.assertIn(package, guide)
|
|
|
|
|
|
|
|
class TheTestingGuideNamesEveryModule(unittest.TestCase):
|
|
"""The guide described 18 modules of 144, and a contributor touching
|
|
slots.py or the downloader found no module named for it."""
|
|
|
|
def test_every_module_is_in_the_table(self):
|
|
root = Path(__file__).resolve().parents[1]
|
|
guide = (root / "wiki" / "testing-guide.md").read_text(encoding="utf-8")
|
|
missing = sorted(
|
|
path.stem for path in (root / "tests").glob("test_*.py")
|
|
if f"`{path.stem}`" not in guide
|
|
)
|
|
self.assertEqual(missing, [])
|
|
|
|
|
|
|
|
class TheSitemapIsAnnouncedWhereCrawlersLook(unittest.TestCase):
|
|
"""robots.txt was published as /retrobios/robots.txt, which no crawler
|
|
reads (RFC 9309 reads the host root): the sitemap it announced was
|
|
never found that way."""
|
|
|
|
def test_the_head_names_it_and_no_robots_is_written(self):
|
|
root = Path(__file__).resolve().parents[1]
|
|
override = (root / "docs_assets" / "overrides" / "main.html").read_text()
|
|
self.assertIn('rel="sitemap"', override)
|
|
source = (root / "scripts" / "generate_site.py").read_text()
|
|
self.assertNotIn('"robots.txt"),\n "User-agent', source)
|
|
self.assertNotIn("Sitemap: https://", source)
|
|
|
|
|
|
|
|
class TheHomeTitleSaysWhatTheSiteIs(unittest.TestCase):
|
|
def test_not_the_site_name_twice(self):
|
|
from generate_site import SITE_NAME, _browser_title
|
|
|
|
title = _browser_title(Path("index.md"), SITE_NAME)
|
|
self.assertNotEqual(title, SITE_NAME)
|
|
self.assertIn("BIOS", title)
|
|
|
|
|
|
|
|
class PlatformPagesPrintTheDeclaredCrc32(unittest.TestCase):
|
|
"""System.dat pins aes.zip by crc32 91e93107. The RetroArch page printed
|
|
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():
|
|
self.skipTest("site not generated")
|
|
self.assertIn("CRC32: `91e93107`", page.read_text(encoding="utf-8"))
|
|
|
|
|
|
class PlainHouseStyle(unittest.TestCase):
|
|
"""The stylesheet stays a plain documentation theme.
|
|
|
|
Gradient banners, coloured side stripes on cards, lifting hover cards,
|
|
pill badges and small-caps table headers had piled up into the look of
|
|
a generated landing page. Each pattern below brought one of them back.
|
|
"""
|
|
|
|
PATTERNS = {
|
|
"gradient": re.compile(r"gradient\("),
|
|
"side stripe": re.compile(r"border-(?:left|inline-start)\s*:\s*\d+px"),
|
|
"lifting card": re.compile(r"translateY\("),
|
|
"drop shadow": re.compile(r"box-shadow"),
|
|
"small caps header": re.compile(r"text-transform\s*:\s*uppercase"),
|
|
"pill": re.compile(r"border-radius\s*:\s*(?:[5-9]|\d{2,})px|50%"),
|
|
"box-drawing rule": re.compile("[\u2500-\u257f]"),
|
|
}
|
|
|
|
def test_the_stylesheet_has_none_of_the_patterns(self):
|
|
css = (ROOT / "docs_assets" / "extra.css").read_text(encoding="utf-8")
|
|
for label, pattern in self.PATTERNS.items():
|
|
with self.subTest(pattern=label):
|
|
self.assertIsNone(pattern.search(css), label)
|
|
|
|
def test_headings_do_not_repeat_the_site_name(self):
|
|
# The header bar already names the site, and the browser title gets
|
|
# it from the theme: "# DuckStation - RetroBIOS" said it twice.
|
|
source = (ROOT / "scripts" / "generate_site.py").read_text(encoding="utf-8")
|
|
self.assertNotRegex(source, r'"# [^"]* - (?:\{SITE_NAME\}|RetroBIOS)')
|
|
for page in sorted((ROOT / "wiki").glob("*.md")):
|
|
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()
|