mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-25 13:19:03 -05:00
feat: cite sources and files on reference pages
Platform pages link each reader's source line at its pinned revision, name the collection file that serves each entry, show launcher records with their lines, name systems with the id beside them and the list they were read from in words. The largest system lists move to pages of their own so a platform page stays under 500 KB. Gaps: plain numbers with a bar, a badge only for missing required firmware, Files to source as a heading. Cross-reference: counts per platform, one page per platform, one row per upstream project. Exports: gaps listed once, status, category and the serving file in the CSVs and SQLite, the commit in each API envelope, the provenance Wanted list as CSV. Catalog dates in words on the provenance page.
This commit is contained in:
1 parent
14a6518d49
commit
8b26a8fcac
10 files changed
+1114
-211
No files matched your search
@@ -19,6 +19,11 @@
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"commit": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{40}$",
|
||||
"description": "The repository commit the data was read at."
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
+630
-202
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
import urllib.parse
|
||||
from dataclasses import dataclass
|
||||
import urllib.request
|
||||
|
||||
import upstream
|
||||
@@ -345,3 +346,98 @@ _PATH_RE = re.compile(r"^[A-Za-z0-9_.@/+~-]+$")
|
||||
def _guarded_spans(line: str) -> list[tuple[int, int]]:
|
||||
"""Code spans and links of a markdown line."""
|
||||
return [m.span() for m in re.finditer(r"`[^`]*`|\[[^\]]*\]\([^)]*\)", line)]
|
||||
|
||||
|
||||
# Where a platform's list was read: the forge, the repository, the revision
|
||||
# and the path inside it, out of the URL its scraper pinned.
|
||||
_LIST_URL_RES = (
|
||||
re.compile(r"^https://raw\.githubusercontent\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/"
|
||||
r"(?P<rev>[^/]+)/(?P<path>.+)$"),
|
||||
re.compile(r"^https://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/(?:blob|tree)/"
|
||||
r"(?P<rev>[^/]+)(?:/(?P<path>.+))?$"),
|
||||
re.compile(r"^https://codeload\.github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/"
|
||||
r"tar\.gz/(?P<rev>[^/]+)$"),
|
||||
re.compile(r"^https://(?P<host>gitlab\.com)/(?P<owner>[^/]+)/(?P<repo>[^/]+)/-/"
|
||||
r"(?:raw|blob)/(?P<rev>[^/]+)/(?P<path>.+)$"),
|
||||
)
|
||||
_BRANCHES = frozenset({"master", "main", "db"})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ListSource:
|
||||
"""A platform's list as its scraper read it."""
|
||||
|
||||
host: str
|
||||
owner: str
|
||||
repo: str
|
||||
rev: str
|
||||
path: str
|
||||
|
||||
@property
|
||||
def moving(self) -> bool:
|
||||
"""The revision is a branch: what was read is not recorded."""
|
||||
return not self.rev or self.rev in _BRANCHES
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
"""`batocera-systems at tag batocera-43.1`, said in words."""
|
||||
name = self.path.rsplit("/", 1)[-1] if self.path else f"{self.repo} repository"
|
||||
if not self.rev:
|
||||
return f"{name}, default branch"
|
||||
if re.fullmatch(r"[0-9a-f]{40}", self.rev):
|
||||
return f"{name} at commit {self.rev[:12]}"
|
||||
if self.moving:
|
||||
return f"{name} on branch {self.rev}"
|
||||
return f"{name} at tag {self.rev}"
|
||||
|
||||
def browse(self, path: str = "", start: int | None = None,
|
||||
end: int | None = None) -> str:
|
||||
"""The page a reader opens: the file, or the tree, at the revision."""
|
||||
path = path or self.path
|
||||
if self.host == "gitlab.com":
|
||||
url = f"https://gitlab.com/{self.owner}/{self.repo}/-/blob/{self.rev}/{path}"
|
||||
anchor = f"#L{start}" + (f"-{end}" if end and end != start else "")
|
||||
else:
|
||||
base = f"https://github.com/{self.owner}/{self.repo}"
|
||||
if not self.rev:
|
||||
return base
|
||||
kind = "blob" if path else "tree"
|
||||
url = f"{base}/{kind}/{self.rev}" + (f"/{path}" if path else "")
|
||||
anchor = f"#L{start}" + (f"-L{end}" if end and end != start else "")
|
||||
return url + (anchor if start else "")
|
||||
|
||||
|
||||
def list_source(url: str) -> ListSource | None:
|
||||
"""The forge, repository, revision and path of a platform's `source:`."""
|
||||
url = str(url or "").strip()
|
||||
for pattern in _LIST_URL_RES:
|
||||
found = pattern.match(url)
|
||||
if found:
|
||||
parts = found.groupdict()
|
||||
return ListSource(parts.get("host") or "github.com", parts["owner"],
|
||||
parts["repo"], parts["rev"], parts.get("path") or "")
|
||||
bare = re.match(r"^https://github\.com/([^/]+)/([^/]+?)/?$", url)
|
||||
if bare:
|
||||
return ListSource("github.com", bare[1], bare[2], "", "")
|
||||
return None
|
||||
|
||||
|
||||
def launcher_ref_markdown(source: ListSource | None, ref: str) -> str:
|
||||
"""A launcher record's source_ref, each part a pinned link.
|
||||
|
||||
Linked only where the caller knows the launcher code sits in the
|
||||
repository and revision of the platform's list. Otherwise each part is
|
||||
plain code: a link to another repository is a false citation.
|
||||
"""
|
||||
rendered: list[str] = []
|
||||
for part in split_source_ref(ref):
|
||||
display = part.path.rsplit("/", 1)[-1]
|
||||
if part.start is not None:
|
||||
display += f":{part.start}"
|
||||
if part.end is not None and part.end != part.start:
|
||||
display += f"-{part.end}"
|
||||
if source is None or not source.rev or not _PATH_RE.fullmatch(part.path):
|
||||
rendered.append(f"`{display}`")
|
||||
else:
|
||||
rendered.append(f"[`{display}`]({source.browse(part.path, part.start, part.end)})")
|
||||
return ", ".join(rendered)
|
||||
@@ -33,12 +33,17 @@ class SiteCoreCounts(unittest.TestCase):
|
||||
for name in list_registered_platforms(platforms, include_archived=True):
|
||||
config = load_platform_config(name, platforms)
|
||||
coverages[name] = {"platform": config.get("platform", name), "config": config}
|
||||
page = generate_site.generate_cross_reference(coverages, profiles)
|
||||
moved: dict[str, str] = {}
|
||||
page = generate_site.generate_cross_reference(coverages, profiles, moved)
|
||||
shown = {
|
||||
m.group(1): int(m.group(2))
|
||||
for m in re.finditer(
|
||||
r'<summary>([^<]+)</summary>\n\n.*\n\n\*\*(\d+) cores', page)
|
||||
for m in re.finditer(r"^\| \[([^\]]+)\]\(platforms/[^)]+\) \| (\d+)", page, re.M)
|
||||
}
|
||||
for name, cov in coverages.items():
|
||||
with self.subTest(page=name):
|
||||
rows = [line for line in moved[f"cross-reference-{name}"].splitlines()
|
||||
if line.startswith("| [")]
|
||||
self.assertEqual(len(rows), shown[cov["platform"]])
|
||||
for name, cov in coverages.items():
|
||||
with self.subTest(platform=name):
|
||||
expected = len(resolve_platform_cores(cov["config"], unique))
|
||||
|
||||
@@ -621,7 +621,9 @@ class APlatformPageNamesTheFileItTranscribes(unittest.TestCase):
|
||||
return generate_site.generate_platform_page("p", cov)
|
||||
|
||||
tag = "https://raw.githubusercontent.com/o/r/batocera-43.1/x/batocera-systems"
|
||||
self.assertIn(f"| Transcribed from | <{tag}> |", page(tag))
|
||||
self.assertIn(
|
||||
"| Transcribed from | [batocera-systems at tag batocera-43.1]"
|
||||
"(https://github.com/o/r/blob/batocera-43.1/x/batocera-systems) |", page(tag))
|
||||
branch = "https://raw.githubusercontent.com/o/r/master/x.json"
|
||||
self.assertIn("revision read is not recorded", page(branch))
|
||||
self.assertIn("revision read is not recorded", page("https://github.com/RetroDECK/components"))
|
||||
|
||||
@@ -10,11 +10,13 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import siterender # noqa: E402
|
||||
from generate_site import _render_platform_systems # noqa: E402
|
||||
|
||||
A, B, C = "a" * 32, "b" * 32, "c" * 32
|
||||
@@ -36,7 +38,9 @@ class EachRowItsDeclaration(unittest.TestCase):
|
||||
{"name": "boot.rom", "status": "ok", "system": "saturn"},
|
||||
],
|
||||
}
|
||||
text = "\n".join(_render_platform_systems(by_system, config_files, {}))
|
||||
names = {"psx": "Sony PlayStation", "saturn": "Sega Saturn"}
|
||||
with mock.patch.dict(siterender.SYSTEM_NAMES, names):
|
||||
text = "\n".join(_render_platform_systems(by_system, config_files, {}))
|
||||
psx, saturn = text.split("saturn", 1)[0], text.split("saturn", 1)[1]
|
||||
self.assertIn(A, psx)
|
||||
self.assertNotIn(B, psx)
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
"""Reference pages (round 2, package 8).
|
||||
|
||||
A reference page cites a hash, a source line or a count, and each citation
|
||||
has to hold when an expert checks it: the reader of a pinned dump is the
|
||||
profile entry accepting its hash, a source line is a pinned link, an entry
|
||||
names the file that serves it, and the exports carry what the page shows.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import generate_site # noqa: E402
|
||||
import siterender # noqa: E402
|
||||
from common import ( # noqa: E402
|
||||
list_registered_platforms,
|
||||
load_emulator_profiles,
|
||||
load_platform_config,
|
||||
)
|
||||
from exporter.baseline import HASH_FIELDS, _hash_values # noqa: E402
|
||||
from exporter.candidates import EntryCategories # noqa: E402
|
||||
from sitesources import launcher_ref_markdown, list_source # noqa: E402
|
||||
|
||||
PLATFORMS = str(REPO_ROOT / "platforms")
|
||||
DIST = REPO_ROOT / "website" / "dist"
|
||||
|
||||
|
||||
def _accepted(profile_entry: dict) -> set[str]:
|
||||
values = {v for f in HASH_FIELDS for v in _hash_values(profile_entry, f)}
|
||||
for member in profile_entry.get("contents") or []:
|
||||
if isinstance(member, dict):
|
||||
values |= {v for f in HASH_FIELDS for v in _hash_values(member, f)}
|
||||
return values
|
||||
|
||||
|
||||
class ReaderMatchedByHash(unittest.TestCase):
|
||||
"""Batocera's scph1001.bin (md5 dc2b9bf8, SCPH-1001 v2.0) cited
|
||||
DuckStation's bios.cpp:36, the line of the v2.2 dump: the reader was
|
||||
matched by name."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
||||
|
||||
def test_batocera_scph1001_cites_the_v20_line(self):
|
||||
config = load_platform_config("batocera", PLATFORMS)
|
||||
cats = EntryCategories(config, self.profiles)
|
||||
entry = next(e for e in config["systems"]["sony-playstation"]["files"]
|
||||
if e["name"] == "scph1001.bin")
|
||||
found = cats.explain(entry, entry.get("destination") or entry["name"],
|
||||
"sony-playstation")
|
||||
duck = next(r for r in found["read_by"] if r["emulator"] == "duckstation")
|
||||
self.assertEqual(duck["entries"], [
|
||||
{"name": "scph1001_v20.bin", "source_ref": "src/core/bios.cpp:42"}])
|
||||
|
||||
def test_every_cited_entry_accepts_the_md5_or_says_by_name(self):
|
||||
by_name = 0
|
||||
for platform in list_registered_platforms(PLATFORMS, include_archived=True):
|
||||
config = load_platform_config(platform, PLATFORMS)
|
||||
cats = EntryCategories(config, self.profiles)
|
||||
for sys_id, system in (config.get("systems") or {}).items():
|
||||
for entry in system.get("files") or []:
|
||||
md5s = set(_hash_values(entry, "md5")) | set(_hash_values(entry, "alt_md5"))
|
||||
if not md5s:
|
||||
continue
|
||||
# The same dump, named by whichever hash both sides state.
|
||||
declared = md5s | {v for f in HASH_FIELDS for v in _hash_values(entry, f)}
|
||||
found = cats.explain(entry, entry.get("destination") or entry["name"], sys_id)
|
||||
for reader in found.get("read_by", []):
|
||||
if reader["emulator"] == "launcher":
|
||||
continue
|
||||
files = self.profiles[reader["emulator"]].get("files") or []
|
||||
for cited in reader["entries"]:
|
||||
if cited.get("by") == "name":
|
||||
by_name += 1
|
||||
continue
|
||||
accepted = set().union(*(
|
||||
_accepted(f) for f in files
|
||||
if isinstance(f, dict) and f.get("name") == cited["name"]))
|
||||
with self.subTest(platform=platform, entry=entry["name"],
|
||||
emulator=reader["emulator"]):
|
||||
self.assertTrue(any(
|
||||
a == m or (29 <= len(m) < 32 and a.startswith(m))
|
||||
for m in declared for a in accepted))
|
||||
self.assertGreater(by_name, 0)
|
||||
|
||||
def test_a_contradicting_homonym_is_not_cited(self):
|
||||
profiles = {"core": {"type": "libretro", "systems": ["s"], "files": [
|
||||
{"name": "bios.bin", "md5": "1" * 32, "source_ref": "a.c:1"},
|
||||
{"name": "bios_v2.bin", "md5": "2" * 32, "source_ref": "a.c:2"},
|
||||
]}}
|
||||
config = {"cores": ["core"], "systems": {"s": {"files": []}}}
|
||||
cats = EntryCategories(config, profiles)
|
||||
found = cats.explain({"name": "bios.bin", "md5": "2" * 32}, "bios.bin", "s")
|
||||
self.assertEqual(found["read_by"][0]["entries"],
|
||||
[{"name": "bios_v2.bin", "source_ref": "a.c:2"}])
|
||||
found = cats.explain({"name": "bios.bin", "md5": "3" * 32}, "bios.bin", "s")
|
||||
self.assertEqual(found["read_by"][0]["entries"], [])
|
||||
|
||||
|
||||
class EvidenceLinksItsSource(unittest.TestCase):
|
||||
"""Platform pages printed refs as code with the role word in front:
|
||||
`source frontend/libretro.c:3864-3867`."""
|
||||
|
||||
def test_pinned_link_and_by_name(self):
|
||||
evidence = {"read_by": [{
|
||||
"emulator": "pcsx_rearmed", "proof": "name", "runs_system": True,
|
||||
"entries": [{"name": "scph1001.bin", "by": "name",
|
||||
"source_ref": "source frontend/libretro.c:3864-3867 (listed_bios[6])"}],
|
||||
}]}
|
||||
text = generate_site._category_evidence_md(evidence)
|
||||
self.assertRegex(text, r"\[`frontend/libretro\.c:3864-3867`\]\(https://github\.com/"
|
||||
r"libretro/pcsx_rearmed/blob/[0-9a-f]{40}/frontend/libretro\.c"
|
||||
r"#L3864-L3867\)")
|
||||
self.assertIn("(by name)", text)
|
||||
self.assertNotIn("`source ", text)
|
||||
|
||||
def test_launcher_record_named_with_its_lines(self):
|
||||
evidence = {"read_by": [{"emulator": "launcher", "proof": "launcher",
|
||||
"entries": [], "runs_system": True}]}
|
||||
config = load_platform_config("batocera", PLATFORMS)
|
||||
launched = generate_site._launcher_refs("batocera", config)["scph1001.bin"]
|
||||
text = generate_site._category_evidence_md(evidence, launched)
|
||||
self.assertNotIn("(by launcher)", text)
|
||||
self.assertIn("https://github.com/batocera-linux/batocera.linux/blob/batocera-43.1/"
|
||||
"package/batocera/core/batocera-configgen/", text)
|
||||
self.assertIn("duckstationGenerator.py:115-136", text)
|
||||
|
||||
def test_a_launcher_in_another_repository_is_not_linked(self):
|
||||
source = list_source("https://gitlab.com/recalbox/recalbox/-/raw/10.1.1/x/es_bios.xml")
|
||||
self.assertEqual(launcher_ref_markdown(None, "a/b.py:3"), "`b.py:3`")
|
||||
self.assertIn("gitlab.com/recalbox/recalbox/-/blob/10.1.1/a/b.py#L3",
|
||||
launcher_ref_markdown(source, "a/b.py:3"))
|
||||
|
||||
|
||||
class EachEntryNamesItsFile(unittest.TestCase):
|
||||
def test_served_held_and_missing(self):
|
||||
path, digest = next(
|
||||
(p, d) for p, d in generate_site._collection_sha1_by_path().items()
|
||||
if p.startswith("bios/") and p.count("/") >= 3)
|
||||
served = generate_site._served_line({"status": "ok", "path": path})
|
||||
self.assertRegex(served, rf"\]\(\.\./systems/[^)]+\.md#sha1-{digest[:12]}\)")
|
||||
self.assertTrue(served.startswith("Served by: "))
|
||||
self.assertIn(f"`{digest[:8]}`", served)
|
||||
self.assertEqual(generate_site._served_line({"status": "missing"}), "Not held")
|
||||
self.assertTrue(generate_site._served_line(
|
||||
{"status": "untested", "path": path}).startswith("Held, not the declared hash"))
|
||||
# Existence mode reports no path: nothing is claimed, held or not.
|
||||
self.assertEqual(generate_site._served_line({"status": "ok"}), "")
|
||||
|
||||
def test_a_long_system_list_is_cut_between_files(self):
|
||||
body = ["2/2 files verified", ""]
|
||||
for n in range(4):
|
||||
body += [f"- `f{n}.bin` - OK", " " + "x" * 100]
|
||||
runs = generate_site._file_runs(body, 250)
|
||||
self.assertEqual(len(runs), 2)
|
||||
for run in runs:
|
||||
self.assertEqual(run[:2], body[:2])
|
||||
self.assertTrue(run[2].startswith("- "))
|
||||
self.assertEqual(sum(len(run) - 2 for run in runs), len(body) - 2)
|
||||
|
||||
|
||||
class AlsoNamedIsNotCited(unittest.TestCase):
|
||||
def test_other_system_cores_are_named_only(self):
|
||||
evidence = {"read_by": [
|
||||
{"emulator": "duckstation", "proof": "name", "runs_system": True,
|
||||
"entries": [{"name": "scph1001_v20.bin", "source_ref": "src/core/bios.cpp:42"}]},
|
||||
{"emulator": "mame", "proof": "name", "runs_system": False,
|
||||
"entries": [{"name": "x.zip", "source_ref": "src/mame/x.cpp:1", "by": "name"}]},
|
||||
]}
|
||||
text = generate_site._category_evidence_md(evidence)
|
||||
also = text.split(", also named by ", 1)[1]
|
||||
self.assertNotIn("x.cpp", also)
|
||||
self.assertIn("mame", also)
|
||||
|
||||
|
||||
class PlatformReferenceInWords(unittest.TestCase):
|
||||
def _page(self, mode: str, hash_type: str, systems: dict | None = None,
|
||||
registry: dict | None = None) -> str:
|
||||
details = [{"name": "f.bin", "status": "ok", "system": s} for s in (systems or {})]
|
||||
cov = {"platform": "P", "details": details, "total": 1, "verified": 1,
|
||||
"present": 1, "missing": 0, "untested": 0, "mode": mode,
|
||||
"config": {"hash_type": hash_type, "systems": systems or {},
|
||||
"source": "https://github.com/o/r/tree/" + "a" * 40}}
|
||||
return generate_site.generate_platform_page("p", cov, registry or {})
|
||||
|
||||
def test_hash_type_only_when_it_differs(self):
|
||||
self.assertNotIn("Hash type", self._page("md5", "md5"))
|
||||
self.assertNotIn("Hashes in the list", self._page("md5", "md5"))
|
||||
page = self._page("existence", "sha1")
|
||||
self.assertIn("| Hashes in the list | SHA-1 |", page)
|
||||
self.assertIn(">name only</span>", page)
|
||||
self.assertIn(">MD5</span>", self._page("md5", "md5"))
|
||||
self.assertIn("[r repository at commit aaaaaaaaaaaa](https://github.com/o/r/tree/"
|
||||
+ "a" * 40 + ")", page)
|
||||
|
||||
def test_systems_by_name_with_the_id_in_small_text(self):
|
||||
systems = {"sony-playstation": {"files": [{"name": "f.bin"}]}}
|
||||
page = self._page("md5", "md5", systems)
|
||||
name = siterender.system_name("sony-playstation")
|
||||
self.assertIn(f"| [{name}](#sony-playstation) <small>sony-playstation</small> |", page)
|
||||
heading = next(line for line in page.splitlines() if line.startswith("### ")
|
||||
and "sony-playstation" in line)
|
||||
self.assertIn(name, heading)
|
||||
self.assertTrue(heading.endswith("<small>sony-playstation</small> { #sony-playstation }"))
|
||||
|
||||
def test_logo_in_front_matter_not_floated(self):
|
||||
page = self._page("md5", "md5", registry={"p": {"logo": "https://x.test/l.png"}})
|
||||
self.assertTrue(page.startswith('---\nlogo: "https://x.test/l.png"\n---\n'))
|
||||
self.assertNotIn('align="right"', page)
|
||||
|
||||
|
||||
class GapsPageNumbers(unittest.TestCase):
|
||||
def test_badge_only_for_missing_required_firmware(self):
|
||||
self.assertNotIn("rb-badge", generate_site._ratio_cell(99, 100))
|
||||
self.assertIn('class="rb-bar"', generate_site._ratio_cell(99, 100))
|
||||
self.assertIn("rb-badge-danger", generate_site._ratio_cell(99, 100, 1))
|
||||
cov = {"details": [
|
||||
{"name": "a", "system": "s", "status": "missing", "required": True, "category": "bios"},
|
||||
{"name": "b", "system": "s", "status": "missing", "required": True,
|
||||
"category": "game_data"},
|
||||
{"name": "c", "system": "s", "status": "missing", "required": False},
|
||||
]}
|
||||
self.assertEqual(generate_site._missing_required_firmware(cov), 1)
|
||||
|
||||
def test_headings_and_header(self):
|
||||
source = Path(generate_site.__file__).read_text(encoding="utf-8")
|
||||
body = source[source.index("def generate_gap_analysis("):]
|
||||
body = body[:body.index("\ndef ", 10)]
|
||||
self.assertIn('"# What the packs do not hold"', body)
|
||||
self.assertIn("On its BIOS list (destinations)", body)
|
||||
self.assertIn('"### Files to source"', body)
|
||||
self.assertNotIn("### Files to source (", body)
|
||||
|
||||
|
||||
class UpstreamsGrouped(unittest.TestCase):
|
||||
def test_mame_trees_are_one_project(self):
|
||||
keys = {generate_site.upstream_project(u)[0] for u in (
|
||||
"https://github.com/mamedev/mame",
|
||||
"https://github.com/mamedev/mame/tree/mame0289",
|
||||
"https://github.com/mamedev/mame/tree/mame0135u4",
|
||||
)}
|
||||
self.assertEqual(keys, {"github.com/mamedev/mame"})
|
||||
self.assertEqual(generate_site.upstream_project(
|
||||
"https://github.com/mamedev/mame/tree/mame0289")[1], "mame0289")
|
||||
|
||||
def test_nested_groups_notes_and_archives(self):
|
||||
project = generate_site.upstream_project
|
||||
self.assertEqual(
|
||||
project("https://gitlab.com/recalbox/packages/recalbox-cores/libretro-exel-hence"),
|
||||
("gitlab.com/recalbox/packages/recalbox-cores/libretro-exel-hence", ""))
|
||||
self.assertEqual(project("https://gitlab.com/jgemu/geolith/-/tree/v1"),
|
||||
("gitlab.com/jgemu/geolith", "v1"))
|
||||
self.assertEqual(
|
||||
project("https://git.9front.org/plan9front/plan9front (sys/src/games/nes/)"),
|
||||
("git.9front.org/plan9front/plan9front", "sys/src/games/nes/"))
|
||||
key, _ = project("https://archive.softwareheritage.org/browse/revision/"
|
||||
"dc20a615275f66bee20a4fd851ef0231daca4f14/")
|
||||
self.assertEqual(generate_site._upstream_label(key),
|
||||
"Software Heritage revision dc20a615275f")
|
||||
|
||||
def test_overview_and_a_page_per_platform(self):
|
||||
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
||||
coverages = {}
|
||||
for name in ("batocera", "retroarch"):
|
||||
config = load_platform_config(name, PLATFORMS)
|
||||
coverages[name] = {"platform": config.get("platform", name), "config": config}
|
||||
moved: dict[str, str] = {}
|
||||
page = generate_site.generate_cross_reference(coverages, profiles, moved)
|
||||
self.assertEqual(set(moved), {"cross-reference-batocera", "cross-reference-retroarch"})
|
||||
self.assertNotIn("<details", page)
|
||||
upstream = page[page.index("## By upstream emulator"):]
|
||||
self.assertEqual(len(re.findall(r"\| \[mamedev/<wbr>mame\]\(", upstream)), 1)
|
||||
header = next(line for line in upstream.splitlines() if line.startswith("| Cores"))
|
||||
self.assertTrue(header.startswith("| Cores | Upstream |"))
|
||||
|
||||
|
||||
class ExportsComplete(unittest.TestCase):
|
||||
def test_gap_rows_once_with_their_outcome(self):
|
||||
path = next(iter(generate_site._collection_sha1_by_path()))
|
||||
detail = {"name": "x.bin", "system": "s", "status": "untested", "path": path,
|
||||
"category": "bios"}
|
||||
coverages = {"p": {"platform": "P", "details": [
|
||||
{"name": "h.xml", "system": "s", "status": "missing"},
|
||||
{"name": "h.xml", "system": "s", "status": "missing"}, detail]}}
|
||||
db = json.loads((REPO_ROOT / "database.json").read_text(encoding="utf-8"))
|
||||
rows = generate_site._gap_export_rows(coverages, {}, db)
|
||||
self.assertEqual([r["name"] for r in rows], ["h.xml", "x.bin"])
|
||||
held = rows[1]
|
||||
self.assertEqual(held["collection_path"], path)
|
||||
self.assertEqual(held["resolved_sha1"], generate_site._collection_sha1_by_path()[path])
|
||||
self.assertEqual(held["category"], "bios")
|
||||
|
||||
def test_platform_rows_carry_status_and_file(self):
|
||||
db = json.loads((REPO_ROOT / "database.json").read_text(encoding="utf-8"))
|
||||
sha1, entry = next(iter(db["files"].items()))
|
||||
coverages = {"p": {"platform": "P", "config": {"systems": {"s": {"files": [
|
||||
{"name": "a.bin"}, {"name": "b.bin"}]}}}, "details": [
|
||||
{"name": "a.bin", "system": "s", "status": "ok", "path": entry["path"],
|
||||
"category": "bios"},
|
||||
{"name": "b.bin", "system": "s", "status": "missing", "category": "game_data"}]}}
|
||||
_items, rows = generate_site._platform_export_rows(coverages, db)
|
||||
self.assertEqual((rows[0]["status"], rows[0]["resolved_sha1"]), ("ok", sha1))
|
||||
self.assertEqual(rows[0]["collection_path"], entry["path"])
|
||||
self.assertEqual((rows[1]["status"], rows[1]["category"], rows[1]["collection_path"]),
|
||||
("missing", "game_data", ""))
|
||||
|
||||
def test_commit_and_wanted_list(self):
|
||||
commit = generate_site._source_commit()
|
||||
self.assertRegex(commit, r"^[0-9a-f]{40}$")
|
||||
schema = json.loads((REPO_ROOT / "schemas" / "site-api-envelope.schema.json")
|
||||
.read_text(encoding="utf-8"))
|
||||
self.assertIn("commit", schema["properties"])
|
||||
db = json.loads((REPO_ROOT / "database.json").read_text(encoding="utf-8"))
|
||||
rows = generate_site._provenance_wanted_rows(db)
|
||||
self.assertTrue(rows)
|
||||
self.assertTrue(all(r["catalog"] and r["name"] for r in rows))
|
||||
text = generate_site._csv_document(list(generate_site.WANTED_FIELDS), rows)
|
||||
self.assertEqual(next(csv.reader(io.StringIO(text))), list(generate_site.WANTED_FIELDS))
|
||||
|
||||
|
||||
class DatesAndNames(unittest.TestCase):
|
||||
def test_snapshot_date_in_words(self):
|
||||
self.assertEqual(generate_site._date_in_words("2026-08-07T10:00:00Z"),
|
||||
'<time datetime="2026-08-07">7 August 2026</time>')
|
||||
|
||||
def test_data_page_title(self):
|
||||
self.assertTrue(generate_site.generate_data_page([]).startswith("# Data and API\n"))
|
||||
|
||||
|
||||
@unittest.skipUnless((DIST / "platforms" / "batocera" / "index.html").is_file(),
|
||||
"site not built")
|
||||
class BuiltBatoceraPage(unittest.TestCase):
|
||||
def test_served_by_and_pinned_links(self):
|
||||
main = (DIST / "platforms" / "batocera" / "index.html").read_text(encoding="utf-8")
|
||||
self.assertTrue("batocera-systems at tag batocera-43.1" in main)
|
||||
# PlayStation's list is long enough to sit on a page of its own.
|
||||
pages = [main] + [p.read_text(encoding="utf-8") for p in
|
||||
(DIST / "platforms").glob("batocera-sony-playstation*/index.html")]
|
||||
html = "".join(pages)
|
||||
for text in ("Served by:", "scph1001_v20.bin", "https://github.com/stenzek/duckstation/blob/"):
|
||||
with self.subTest(text=text):
|
||||
self.assertTrue(text in html)
|
||||
self.assertFalse("(by launcher)" in html)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -312,7 +312,9 @@ class TheSearchLandsOnTheCard(unittest.TestCase):
|
||||
|
||||
by_system = {f"system-{i:02d}": [{"name": f"f{i}.bin", "status": "ok"}]
|
||||
for i in range(11)}
|
||||
lines = generate_site._render_platform_systems(by_system, {}, {})
|
||||
names = {sys_id: f"System {sys_id[-2:]}" for sys_id in by_system}
|
||||
with mock.patch.dict(siterender.SYSTEM_NAMES, names):
|
||||
lines = generate_site._render_platform_systems(by_system, {}, {})
|
||||
headings = [line for line in lines if line.startswith("### ")]
|
||||
self.assertEqual(len(headings), 11)
|
||||
self.assertTrue(headings[0].endswith("{ #system-00 }"))
|
||||
|
||||
@@ -353,8 +353,12 @@ class PlatformPagesPrintTheDeclaredCrc32(unittest.TestCase):
|
||||
from tests import sitepages # noqa: PLC0415
|
||||
|
||||
sitepages.require()
|
||||
page = sitepages.page("platforms/retroarch.md")
|
||||
self.assertIn("CRC32: `91e93107`", page.read_text(encoding="utf-8"))
|
||||
# The page and the systems moved to pages of their own.
|
||||
text = "".join(
|
||||
path.read_text(encoding="utf-8") for path in sitepages.pages()
|
||||
if sitepages.name(path) == "platforms/retroarch.md"
|
||||
or sitepages.name(path).startswith("platforms/retroarch-"))
|
||||
self.assertIn("CRC32: `91e93107`", text)
|
||||
|
||||
|
||||
class PlainHouseStyle(unittest.TestCase):
|
||||
|
||||
@@ -56,7 +56,7 @@ Every module in `tests/`, by the area it guards.
|
||||
| Handheld port | stubbed PortMaster dialogs, fake installer | `test_port` |
|
||||
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` |
|
||||
| Scrapers, upstream and provenance | inline sources, synthetic | `test_check_freshness`, `test_dedup`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_profile_sync`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_scraper_contract`, `test_upstream`, `test_upstream_index` |
|
||||
| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_schema_semantic_gate`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_p0`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
|
||||
| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_schema_semantic_gate`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
|
||||
|
||||
## Test architecture
|
||||
|
||||
|
||||
Reference in new issue
Block a user