mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
chore: merge the site visual system
This commit is contained in:
commit
f39bcebb22
122 files changed
+2710
-215
No files matched your search
@@ -0,0 +1,219 @@
|
||||
"""Images on the site: diagrams written from data, and local copies only.
|
||||
|
||||
The site loaded its platform logos from GitHub avatars and branch tips, and
|
||||
its system icons from retroarch-assets ``master``: a rename upstream broke an
|
||||
image in silence. Diagrams were drawn at runtime by a mermaid script from a
|
||||
CDN. These tests hold the replacements in place.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import sitediagrams # noqa: E402
|
||||
|
||||
SVG = "{http://www.w3.org/2000/svg}"
|
||||
|
||||
|
||||
class EveryDiagramIsAccessibleSvg(unittest.TestCase):
|
||||
def test_each_layout_parses_and_is_named(self):
|
||||
for image_id in sitediagrams.IMAGE_IDS:
|
||||
figure = sitediagrams.figures()[image_id]
|
||||
for layout in ("wide", "narrow"):
|
||||
with self.subTest(image=image_id, layout=layout):
|
||||
root = ET.fromstring(sitediagrams.svg(figure, layout, ""))
|
||||
self.assertEqual(root.tag, f"{SVG}svg")
|
||||
self.assertEqual(root.get("role"), "img")
|
||||
title, desc = root.find(f"{SVG}title"), root.find(f"{SVG}desc")
|
||||
self.assertTrue(title is not None and title.text)
|
||||
self.assertTrue(desc is not None and desc.text)
|
||||
self.assertEqual(
|
||||
root.get("aria-labelledby").split(), [title.get("id"), desc.get("id")]
|
||||
)
|
||||
self.assertTrue(root.findall(f".//{SVG}text"))
|
||||
|
||||
def test_the_output_does_not_change_between_runs(self):
|
||||
first = [sitediagrams.figure_html(i) for i in sitediagrams.IMAGE_IDS]
|
||||
sitediagrams.figures.cache_clear()
|
||||
self.assertEqual(first, [sitediagrams.figure_html(i) for i in sitediagrams.IMAGE_IDS])
|
||||
|
||||
def test_text_carries_no_typographic_tics(self):
|
||||
for image_id in sitediagrams.IMAGE_IDS:
|
||||
html = sitediagrams.figure_html(image_id)
|
||||
with self.subTest(image=image_id):
|
||||
self.assertIsNone(re.search("[–—←-⇿─-╿…]", html))
|
||||
self.assertNotIn("; ", re.sub(r"<[^>]+>", " ", html))
|
||||
|
||||
def test_a_second_copy_on_one_page_gets_its_own_ids(self):
|
||||
page = sitediagrams.resolve_images(
|
||||
"<!-- image: install-flow -->\n\ntext\n\n<!-- image: install-flow -->\n"
|
||||
)
|
||||
ids = re.findall(r'\bid="([^"]+)"', page)
|
||||
self.assertEqual(len(ids), len(set(ids)))
|
||||
|
||||
|
||||
class PlaceholdersResolve(unittest.TestCase):
|
||||
"""A page asks for a diagram by id. A name no function answers stays a
|
||||
comment, which validate_site reports in the built page."""
|
||||
|
||||
def test_every_placeholder_in_the_sources_names_a_diagram(self):
|
||||
sources = [*sorted((ROOT / "wiki").glob("*.md")), *sorted((ROOT / "scripts").glob("*.py"))]
|
||||
found = 0
|
||||
for path in sources:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for image_id in sitediagrams.PLACEHOLDER_ANYWHERE.findall(text):
|
||||
found += 1
|
||||
with self.subTest(page=path.name, image=image_id):
|
||||
self.assertIn(image_id, sitediagrams.IMAGE_IDS)
|
||||
self.assertGreater(found, 0)
|
||||
|
||||
def test_a_block_placeholder_becomes_a_figure(self):
|
||||
page = sitediagrams.resolve_images("Intro\n\n<!-- image: bios-chain -->\n\nMore\n")
|
||||
self.assertIn('<figure class="rb-figure"', page)
|
||||
self.assertNotIn("<!-- image:", page)
|
||||
|
||||
def test_an_indented_placeholder_keeps_its_indent_and_escapes_markdown(self):
|
||||
page = sitediagrams.resolve_images("1. Step\n\n <!-- image: data-layers -->\n")
|
||||
line = page.splitlines()[2]
|
||||
self.assertTrue(line.startswith(" <span class=\"rb-figure"))
|
||||
self.assertNotIn("_shared", line)
|
||||
self.assertIn("_shared", line)
|
||||
|
||||
def test_an_unknown_name_is_left_for_the_validator(self):
|
||||
page = "<!-- image: no-such-diagram -->\n"
|
||||
self.assertEqual(sitediagrams.resolve_images(page), page)
|
||||
self.assertEqual(sitediagrams.unresolved_images(page), ["no-such-diagram"])
|
||||
|
||||
|
||||
class DiagramsMatchTheData(unittest.TestCase):
|
||||
def test_every_platform_appears_under_its_mode(self):
|
||||
boxes = sitediagrams._mode_boxes()
|
||||
text = " ".join(" ".join(box.lines) for box in boxes)
|
||||
for display, mode, _ in sitediagrams.platform_modes().values():
|
||||
with self.subTest(platform=display):
|
||||
self.assertIn(mode, sitediagrams.MODE_TEXT)
|
||||
self.assertIn(display, text)
|
||||
|
||||
def test_each_example_platform_runs_its_emulator(self):
|
||||
from common import load_emulator_profiles, load_platform_config, resolve_platform_cores
|
||||
|
||||
profiles = load_emulator_profiles(str(ROOT / "emulators"), skip_aliases=False)
|
||||
for platform, emulator in sitediagrams.CHAIN_EXAMPLES:
|
||||
with self.subTest(platform=platform, emulator=emulator):
|
||||
config = load_platform_config(platform, str(ROOT / "platforms"))
|
||||
self.assertIn(emulator, resolve_platform_cores(config, profiles))
|
||||
|
||||
def test_minui_tags_are_folders_minui_hands_its_cores(self):
|
||||
import yaml
|
||||
|
||||
config = yaml.safe_load((ROOT / "platforms" / "minui.yml").read_text(encoding="utf-8"))
|
||||
folders = set(config["core_system_dirs"])
|
||||
for _, tag in sitediagrams.MINUI_EXAMPLES:
|
||||
with self.subTest(tag=tag):
|
||||
self.assertIn(f"Bios/{tag}", folders)
|
||||
self.assertTrue(sitediagrams.minui_files().get(tag))
|
||||
|
||||
def test_the_pipeline_drawing_is_the_step_list(self):
|
||||
from pipeline import STEP_LABELS
|
||||
|
||||
numbers = [box.number for box in sitediagrams._pipeline_boxes()]
|
||||
self.assertEqual(numbers, [label.split(" ", 1)[0] for label in STEP_LABELS.values()])
|
||||
|
||||
|
||||
class NoMermaidAtRuntime(unittest.TestCase):
|
||||
"""A mermaid fence made Material load mermaid@11 (3.5 MB) from unpkg."""
|
||||
|
||||
def test_no_page_source_uses_a_mermaid_fence(self):
|
||||
for path in sorted((ROOT / "wiki").glob("*.md")):
|
||||
with self.subTest(page=path.name):
|
||||
self.assertNotIn("```mermaid", path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class ImagesAreServedFromTheSite(unittest.TestCase):
|
||||
def _manifest(self, folder: str) -> list[dict]:
|
||||
path = ROOT / "docs_assets" / folder / "sources.json"
|
||||
return json.loads(path.read_text(encoding="utf-8"))["images"]
|
||||
|
||||
def test_every_registry_logo_has_a_local_copy(self):
|
||||
import yaml
|
||||
|
||||
registry = yaml.safe_load((ROOT / "platforms" / "_registry.yml").read_text())["platforms"]
|
||||
copies = {entry["platform"]: entry for entry in self._manifest("logos")}
|
||||
for name, entry in registry.items():
|
||||
if not entry.get("logo"):
|
||||
continue
|
||||
with self.subTest(platform=name):
|
||||
self.assertIn(name, copies, "run scripts/fetch_site_images.py --only logos")
|
||||
self.assertEqual(copies[name]["source"], entry["logo"])
|
||||
self.assertTrue((ROOT / "docs_assets" / "logos" / copies[name]["file"]).is_file())
|
||||
self.assertTrue(copies[name]["owner"] and copies[name]["licence"])
|
||||
|
||||
def test_every_recorded_icon_exists(self):
|
||||
for entry in self._manifest("system-icons"):
|
||||
with self.subTest(icon=entry["file"]):
|
||||
self.assertTrue((ROOT / "docs_assets" / "system-icons" / entry["file"]).is_file())
|
||||
|
||||
def test_remote_links_with_a_copy_are_rewritten(self):
|
||||
logo = self._manifest("logos")[0]
|
||||
page = f"| {{ width=20 }} | <img src=\"{logo['source']}\"> |"
|
||||
local = sitediagrams.localize_images(page, Path("platforms/x.md"))
|
||||
self.assertNotIn(logo["source"], local)
|
||||
self.assertEqual(local.count(f"../assets/logos/{logo['file']}"), 2)
|
||||
|
||||
def test_icon_paths_are_link_safe(self):
|
||||
icon = self._manifest("system-icons")[0]
|
||||
local = sitediagrams.localize_images(f"", Path("systems/x.md"))
|
||||
self.assertNotIn(" ", local)
|
||||
|
||||
def test_the_validator_refuses_a_remote_image(self):
|
||||
from validate_site import _parse_page
|
||||
|
||||
with tempfile.TemporaryDirectory(dir=ROOT / "tests") as tmp:
|
||||
path = Path(tmp) / "index.html"
|
||||
path.write_text(
|
||||
'<html lang="en"><body><img alt="x" src="https://example.org/a.png">'
|
||||
'<img alt="y" src="../assets/logos/a.png"><!-- image: nope --></body></html>'
|
||||
)
|
||||
page = _parse_page(path)
|
||||
self.assertEqual(page.remote_images, ["https://example.org/a.png"])
|
||||
self.assertEqual(page.placeholders, ["nope"])
|
||||
|
||||
|
||||
class SharingCards(unittest.TestCase):
|
||||
def test_one_card_per_section_at_the_open_graph_size(self):
|
||||
import struct
|
||||
|
||||
for section in sitediagrams.OG_SECTIONS:
|
||||
with self.subTest(section=section):
|
||||
data = (ROOT / "docs_assets" / "og" / f"{section}.png").read_bytes()
|
||||
self.assertEqual(data[:8], b"\x89PNG\r\n\x1a\n")
|
||||
self.assertEqual(struct.unpack(">II", data[16:24]), (1200, 630))
|
||||
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))
|
||||
self.assertEqual(named, set(sitediagrams.OG_SECTIONS))
|
||||
|
||||
|
||||
class ImagesStillNeeded(unittest.TestCase):
|
||||
"""Screens that need a real device are listed, never drawn as a mock-up."""
|
||||
|
||||
def test_the_list_gives_each_capture_its_alt_text(self):
|
||||
text = (ROOT / "docs_assets" / "images-needed.md").read_text(encoding="utf-8")
|
||||
entries = re.findall(r"(?m)^## ", text)
|
||||
self.assertGreater(len(entries), 3)
|
||||
self.assertEqual(len(entries), len(re.findall(r"(?m)^\*\*Alt text\*\*", text)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+18
-56
@@ -1,15 +1,10 @@
|
||||
"""Renderings shared by the site pages, and the icon cache they read.
|
||||
|
||||
No network: icon checks are answered by a stub.
|
||||
"""
|
||||
"""Renderings shared by the site pages, and the icon copies they read."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
@@ -19,65 +14,32 @@ sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
import siterender # noqa: E402
|
||||
|
||||
|
||||
class _Served:
|
||||
status = 200
|
||||
|
||||
def __enter__(self) -> "_Served":
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc: object) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class IconCacheIsReplacedWhole(unittest.TestCase):
|
||||
"""The cache was truncated then dumped in place: a concurrent reader saw
|
||||
half a file, took it for empty, and an offline run then wrote ``{}``
|
||||
over every recorded icon."""
|
||||
class IconsComeFromTheLocalCopies(unittest.TestCase):
|
||||
"""Icons were checked against upstream with a HEAD request per system on
|
||||
every build, and linked from a branch tip. The build now reads the copies
|
||||
in docs_assets/system-icons and makes no request."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
self.path = Path(tmp.name) / "system_icons.json"
|
||||
patcher = mock.patch.object(siterender, "ICON_CACHE_PATH", self.path)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
saved = dict(siterender._icon_available)
|
||||
self.addCleanup(lambda: (siterender._icon_available.clear(),
|
||||
siterender._icon_available.update(saved)))
|
||||
|
||||
def test_an_offline_run_leaves_an_unreadable_cache_untouched(self):
|
||||
self.path.write_text('{"Sega - Mega Drive": tr')
|
||||
offline = mock.patch.object(
|
||||
siterender.urllib.request, "urlopen",
|
||||
side_effect=urllib.error.URLError("offline"))
|
||||
with offline, mock.patch("sys.stderr"), mock.patch("builtins.print"):
|
||||
siterender.prime_system_icons({"Sega - Mega Drive"})
|
||||
self.assertEqual(self.path.read_text(), '{"Sega - Mega Drive": tr')
|
||||
|
||||
def test_names_another_run_recorded_meanwhile_are_kept(self):
|
||||
self.path.write_text(json.dumps({"A - One": True}))
|
||||
|
||||
def answer(*_args: object, **_kwargs: object) -> _Served:
|
||||
# A second build records its own check while this one runs.
|
||||
self.path.write_text(json.dumps({"A - One": True, "C - Three": False}))
|
||||
return _Served()
|
||||
|
||||
with mock.patch.object(siterender.urllib.request, "urlopen", side_effect=answer), \
|
||||
def test_availability_is_the_folder(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
folder = Path(tmp.name) / "docs_assets" / "system-icons"
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "A - One.png").write_bytes(b"png")
|
||||
with mock.patch.object(siterender, "ICON_DIR", folder), \
|
||||
mock.patch("builtins.print"):
|
||||
siterender.prime_system_icons({"A - One", "B - Two"})
|
||||
self.assertEqual(json.loads(self.path.read_text()),
|
||||
{"A - One": True, "B - Two": True, "C - Three": False})
|
||||
self.assertEqual(sorted(p.name for p in self.path.parent.iterdir()),
|
||||
["system_icons.json"])
|
||||
self.assertTrue(siterender._icon_available["A - One"])
|
||||
self.assertFalse(siterender._icon_available["B - Two"])
|
||||
self.assertEqual(siterender.system_icon_markdown("B", "Two"), "")
|
||||
self.assertIn("A%20-%20One.png", siterender.system_icon_markdown("A", "One"))
|
||||
|
||||
def test_the_write_goes_through_a_scratch_file(self):
|
||||
with mock.patch.object(siterender.urllib.request, "urlopen",
|
||||
return_value=_Served()), \
|
||||
mock.patch.object(siterender, "write_text_atomic",
|
||||
wraps=siterender.write_text_atomic) as atomic, \
|
||||
mock.patch("builtins.print"):
|
||||
siterender.prime_system_icons({"B - Two"})
|
||||
atomic.assert_called_once()
|
||||
def test_no_request_is_made(self):
|
||||
self.assertNotIn("urllib.request", (REPO_ROOT / "scripts" / "siterender.py").read_text())
|
||||
|
||||
|
||||
class EveryPageAddressesPlatformsAsPeers(unittest.TestCase):
|
||||
|
||||
Reference in new issue
Block a user