mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 12:19:13 -05:00
feat: publish manifests, id indexes and licence
The CSV and SQLite carry zipped_file and the Recalbox flags, contracts leave registry keys in a registry field and validate, every envelope carries commit, lookup URLs are site-absolute, systems.json and cores.json answer an id, the Data page shows the manifest fields and a folder check, and platform overviews link emulators by name.
This commit is contained in:
1 parent
c3b45182ca
commit
2b64eaab9b
7 files changed
+907
-63
No files matched your search
@@ -685,14 +685,14 @@ class AHashFindsEveryPageThatNamesIt(unittest.TestCase):
|
||||
records = generate_site.file_lookup_records(db, coverages, profiles, locations, members)
|
||||
found = {(r["layer"], r.get("owner", ""), r["algorithm"], r["value"]): r for r in records}
|
||||
self.assertEqual(found[("collection", "", "md5", md5)]["url"],
|
||||
"systems/sony/#sha1-" + "ab" * 6)
|
||||
"/retrobios/systems/sony/#sha1-" + "ab" * 6)
|
||||
self.assertIn(("collection", "", "crc32", "8d8cb7e4"), found)
|
||||
self.assertEqual(found[("data-directory", "", "sha1", "cd" * 20)]["name"], "font.bin")
|
||||
self.assertEqual(found[("platform", "batocera", "md5", md5)]["url"],
|
||||
"platforms/batocera/#sony-playstation")
|
||||
"/retrobios/platforms/batocera/#sony-playstation")
|
||||
self.assertIn(("platform", "batocera", "md5", "0123456789abcdef0123456789abcdef"), found)
|
||||
self.assertEqual(found[("emulator", "duckstation", "md5", md5)]["url"],
|
||||
"emulators/duckstation/#file-scph5501-bin-490f666e")
|
||||
"/retrobios/emulators/duckstation/#file-scph5501-bin-490f666e")
|
||||
self.assertEqual(found[("emulator", "duckstation", "crc32", "11223344")]["name"], "m.rom")
|
||||
self.assertNotIn(("emulator", "an-alias", "md5", md5), found)
|
||||
|
||||
@@ -724,7 +724,7 @@ class AFileHasAnAddressByContent(unittest.TestCase):
|
||||
}}
|
||||
self.assertEqual(
|
||||
generate_site.file_location_items(db),
|
||||
[{"sha1": "ab" * 20, "url": "systems/roland/#sha1-" + "ab" * 6}],
|
||||
[{"sha1": "ab" * 20, "url": "/retrobios/systems/roland/#sha1-" + "ab" * 6}],
|
||||
)
|
||||
self.assertIn('id="rb-file-lookup-status"', generate_site.generate_files_page())
|
||||
script = (Path(__file__).resolve().parents[1] / "website" / "src" / "scripts"
|
||||
@@ -865,7 +865,8 @@ class ADataDirectoryFileCanBeLookedUp(unittest.TestCase):
|
||||
members = data_directory_members(Path(tmp))
|
||||
items = file_location_items(
|
||||
{"files": {}}, {"dolphin": {"data_directories": [{"ref": "dolphin-sys"}]}}, members)
|
||||
self.assertEqual(items, [{"sha1": "c" * 40, "url": "emulators/dolphin/#sha1-cccccccccccc"}])
|
||||
self.assertEqual(items, [{"sha1": "c" * 40,
|
||||
"url": "/retrobios/emulators/dolphin/#sha1-cccccccccccc"}])
|
||||
|
||||
|
||||
class ACardSaysWhyAFileIsLeftOut(unittest.TestCase):
|
||||
|
||||
@@ -69,8 +69,9 @@ class AnUntestedRowNamesBothHashes(unittest.TestCase):
|
||||
db = {"files": {"f" * 40: {"path": path, "sha256": held}}}
|
||||
table = "\n".join(gs._render_problem_files([problem], db=db))
|
||||
row = next(line for line in table.splitlines() if "dsifirmware" in line)
|
||||
self.assertIn(f"sha256 `{declared}`", row)
|
||||
self.assertIn(f"sha256 `{held}`", row)
|
||||
# Cut to 12 characters in the cell, the whole value in its title.
|
||||
self.assertIn(f'sha256 <code title="{declared}">{declared[:12]}</code>', row)
|
||||
self.assertIn(f'sha256 <code title="{held}">{held[:12]}</code>', row)
|
||||
self.assertIn(f"`{path}`", row)
|
||||
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ class TheRules(unittest.TestCase):
|
||||
{"files": {}}, {"dolphin": {"data_directories": [{"ref": "dolphin-sys"}]}},
|
||||
{"dolphin-sys": members})
|
||||
last = f"{generate_site.DATA_TREE_ROWS_PER_PAGE:040x}"
|
||||
self.assertIn({"sha1": last, "url": f"emulators/data-dolphin-sys-2/#sha1-{last[:12]}"}, items)
|
||||
self.assertIn({"sha1": last, "url": f"/retrobios/emulators/data-dolphin-sys-2/#sha1-{last[:12]}"}, items)
|
||||
|
||||
def test_no_float_rule_for_right_aligned_images(self):
|
||||
css = (REPO_ROOT / "website" / "src" / "styles" / "retrobios.css").read_text(encoding="utf-8")
|
||||
|
||||
@@ -0,0 +1,469 @@
|
||||
"""Reference pages and data (round 3, package 8).
|
||||
|
||||
The CSV and SQLite dropped zipped_file, so a check built from them judged
|
||||
106 good Batocera files wrong. The manifest a pack installs was nowhere on
|
||||
the site. Contracts failed their own schema, 716 documents lacked `commit`,
|
||||
no endpoint answered a system id or a buildbot core name, lookup URLs
|
||||
resolved to 404s, the data's licence went unstated, platform overviews
|
||||
named emulators as unlinked ids, gaps tables overflowed with full hashes
|
||||
and a source link went into a repository that does not hold the file.
|
||||
Tests that read the built site skip when it has not been generated.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import urllib.parse
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import check_platform_folder # noqa: E402
|
||||
import generate_site # noqa: E402
|
||||
import sitesources # noqa: E402
|
||||
|
||||
PUBLIC = REPO_ROOT / "website" / "public"
|
||||
API = PUBLIC / "api" / "v1"
|
||||
SITE = "https://abdess.github.io/retrobios/"
|
||||
TMP = REPO_ROOT / "tmp"
|
||||
|
||||
|
||||
def _built(path: Path) -> Path:
|
||||
if not path.exists():
|
||||
raise unittest.SkipTest(f"{path.relative_to(REPO_ROOT)} not generated")
|
||||
return path
|
||||
|
||||
|
||||
def _verify_status(entry: dict, path: Path, mode: str) -> str:
|
||||
"""What verify.py says of one entry, the platform's native logic."""
|
||||
from verify import verify_entry_existence, verify_entry_md5, verify_entry_sha1
|
||||
|
||||
local = str(path) if path.is_file() else None
|
||||
if mode == "existence":
|
||||
return verify_entry_existence(entry, local)["status"]
|
||||
if mode == "sha1":
|
||||
return verify_entry_sha1(entry, local)["status"]
|
||||
return verify_entry_md5(entry, local)["status"]
|
||||
|
||||
|
||||
def _as_snippet(status: str) -> str:
|
||||
return {"ok": "ok", "untested": "wrong", "missing": "missing"}[status]
|
||||
|
||||
|
||||
class TheExportsKeepWhatACheckNeeds(unittest.TestCase):
|
||||
"""8.1: zipped_file and the Recalbox flags reach the CSV and SQLite."""
|
||||
|
||||
COVERAGE = {"batocera": {
|
||||
"platform": "Batocera", "details": [],
|
||||
"config": {"platform": "Batocera", "verification_mode": "md5",
|
||||
"install": {"detect": []}, "logo": "x.svg", "_key": "batocera",
|
||||
"systems": {"bbc": {"files": [
|
||||
{"name": "bbcb.zip", "destination": "bbcb.zip",
|
||||
"md5": "521a197d", "zipped_file": "basic1.rom"},
|
||||
{"name": "n.rom", "destination": "n.rom", "md5": "ab",
|
||||
"hash_match_mandatory": True, "mandatory_declared": False,
|
||||
"shared_group": "group-x"},
|
||||
]}}},
|
||||
}}
|
||||
|
||||
def test_rows_carry_the_member_and_the_flags(self):
|
||||
_items, rows = generate_site._platform_export_rows(self.COVERAGE)
|
||||
zipped, flagged = rows
|
||||
self.assertEqual(zipped["zipped_file"], "basic1.rom")
|
||||
self.assertIs(flagged["hash_match_mandatory"], True)
|
||||
self.assertIs(flagged["mandatory_declared"], False)
|
||||
self.assertEqual(flagged["shared_group"], "group-x")
|
||||
self.assertEqual(flagged["zipped_file"], "")
|
||||
|
||||
def test_the_csv_header_and_the_table_name_them(self):
|
||||
source = Path(generate_site.__file__).read_text(encoding="utf-8")
|
||||
self.assertIn('"collection_path", "resolved_sha1", *ENTRY_EXPORT_FIELDS,', source)
|
||||
for field in generate_site.ENTRY_EXPORT_FIELDS:
|
||||
self.assertIn(f":{field}", source)
|
||||
for field in ("zipped_file", "hash_match_mandatory", "mandatory_declared", "curated"):
|
||||
self.assertIn(field, generate_site.ENTRY_EXPORT_FIELDS)
|
||||
|
||||
def test_the_built_csv_has_the_column(self):
|
||||
text = _built(PUBLIC / "downloads" / "platform-files.csv").read_text(encoding="utf-8")
|
||||
rows = list(csv.DictReader(io.StringIO(text)))
|
||||
batocera = [row for row in rows if row["platform_id"] == "batocera"]
|
||||
self.assertGreater(sum(1 for row in batocera if row["zipped_file"]), 300)
|
||||
with sqlite3.connect(_built(PUBLIC / "downloads" / "retrobios.sqlite")) as db:
|
||||
count = db.execute("SELECT count(*) FROM platform_files "
|
||||
"WHERE platform_id = 'batocera' AND zipped_file != ''").fetchone()[0]
|
||||
license_row = db.execute("SELECT value FROM metadata WHERE key = 'license'").fetchone()
|
||||
self.assertGreater(count, 300)
|
||||
self.assertEqual(license_row[0], generate_site.DATA_LICENSE)
|
||||
|
||||
|
||||
class AFolderCheckAgreesWithTheVerifier(unittest.TestCase):
|
||||
"""8.1 and 8.3: the published example gives verify.py's verdicts."""
|
||||
|
||||
def _folder(self, tmp: Path) -> tuple[dict, Path]:
|
||||
member = b"basic rom bytes"
|
||||
root = tmp / "bios"
|
||||
root.mkdir()
|
||||
with zipfile.ZipFile(root / "bbcb.zip", "w") as archive:
|
||||
archive.writestr("BASIC1.ROM", member)
|
||||
(root / "plain.bin").write_bytes(b"plain")
|
||||
(root / "wrong.bin").write_bytes(b"not it")
|
||||
(root / "nohash.bin").write_bytes(b"x")
|
||||
md5 = lambda data: hashlib.md5(data).hexdigest() # noqa: E731
|
||||
contract = {"verification_mode": "md5", "systems": {"s": {"files": [
|
||||
{"name": "bbcb.zip", "destination": "bbcb.zip", "md5": md5(member),
|
||||
"zipped_file": "basic1.rom"},
|
||||
{"name": "bbcb.zip", "destination": "bbcb.zip", "md5": "0" * 32,
|
||||
"zipped_file": "basic1.rom"},
|
||||
{"name": "plain.bin", "destination": "plain.bin", "md5": md5(b"plain")[:29]},
|
||||
{"name": "wrong.bin", "destination": "wrong.bin", "md5": md5(b"plain")},
|
||||
{"name": "nohash.bin", "destination": "nohash.bin"},
|
||||
{"name": "gone.bin", "destination": "gone.bin", "md5": "1" * 32},
|
||||
]}}}
|
||||
return contract, root
|
||||
|
||||
def test_each_rule_on_a_sample_folder(self):
|
||||
with tempfile.TemporaryDirectory(dir=TMP) as tmp:
|
||||
contract, root = self._folder(Path(tmp))
|
||||
platforms = Path(tmp) / "platforms"
|
||||
platforms.mkdir()
|
||||
(platforms / "sample.json").write_text(
|
||||
json.dumps({"items": [{"contract": contract}]}), encoding="utf-8")
|
||||
found = check_platform_folder.check("sample", root, platforms.as_uri() + "/")
|
||||
expected = [
|
||||
(entry["destination"], _as_snippet(_verify_status(entry, root / entry["destination"], "md5")))
|
||||
for entry in contract["systems"]["s"]["files"]
|
||||
]
|
||||
self.assertEqual(found, expected)
|
||||
self.assertEqual([v for _d, v in found], ["ok", "wrong", "ok", "wrong", "ok", "missing"])
|
||||
|
||||
def test_batocera_from_the_csv_matches_the_verifier(self):
|
||||
"""A check driven by platform-files.csv, with zipped_file, gives the
|
||||
verifier's verdict on every Batocera entry of a folder filled from
|
||||
the collection."""
|
||||
text = _built(PUBLIC / "downloads" / "platform-files.csv").read_text(encoding="utf-8")
|
||||
rows = [row for row in csv.DictReader(io.StringIO(text)) if row["platform_id"] == "batocera"]
|
||||
with tempfile.TemporaryDirectory(dir=TMP) as tmp:
|
||||
root = Path(tmp)
|
||||
for row in rows:
|
||||
source = REPO_ROOT / row["collection_path"] if row["collection_path"] else None
|
||||
target = root / row["destination"]
|
||||
if source and source.is_file() and not target.exists():
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
os.link(source, target)
|
||||
except OSError:
|
||||
shutil.copyfile(source, target)
|
||||
held = [row for row in rows if (root / row["destination"]).is_file()]
|
||||
if len(held) < 100:
|
||||
raise unittest.SkipTest("the collection files are not in this checkout")
|
||||
# The verifier reads the platform's own entries, the check the CSV.
|
||||
contract = json.loads(_built(API / "platforms" / "batocera.json").read_text())[
|
||||
"items"][0]["contract"]
|
||||
declared = [entry for _sys, system in sorted(contract["systems"].items())
|
||||
for entry in system.get("files") or []]
|
||||
self.assertEqual([e["name"] for e in declared], [row["name"] for row in rows])
|
||||
mismatched = []
|
||||
zipped_ok = 0
|
||||
for row, platform_entry in zip(rows, declared):
|
||||
entry = {key: value for key, value in row.items() if value}
|
||||
path = root / row["destination"]
|
||||
ours = "ok" if check_platform_folder.accepts(entry, path, "md5") else (
|
||||
"wrong" if path.is_file() else "missing")
|
||||
theirs = _as_snippet(_verify_status(platform_entry, path, "md5"))
|
||||
if ours != theirs:
|
||||
mismatched.append((row["destination"], row.get("zipped_file"), ours, theirs))
|
||||
zipped_ok += ours == "ok" and bool(row["zipped_file"])
|
||||
self.assertEqual(mismatched, [])
|
||||
self.assertGreater(zipped_ok, 100)
|
||||
|
||||
def test_the_data_page_shows_the_script_itself(self):
|
||||
script = (REPO_ROOT / "scripts" / "check_platform_folder.py").read_text(encoding="utf-8")
|
||||
page = generate_site.generate_data_page([])
|
||||
self.assertIn("```python\n" + script.rstrip("\n") + "\n```", page)
|
||||
self.assertLess(len(script.splitlines()), 70)
|
||||
self.assertIn("## Check a folder for one platform", page)
|
||||
|
||||
|
||||
class TheContractsValidate(unittest.TestCase):
|
||||
"""8.4: registry keys leave the contract, every $id is its served URL."""
|
||||
|
||||
def test_every_contract_validates_against_the_platform_schema(self):
|
||||
from jsonschema import Draft202012Validator # noqa: PLC0415
|
||||
|
||||
from common import list_registered_platforms, load_platform_config # noqa: PLC0415
|
||||
|
||||
schema = json.loads((REPO_ROOT / "schemas" / "platform.schema.json").read_text())
|
||||
validator = Draft202012Validator(schema)
|
||||
platforms = str(REPO_ROOT / "platforms")
|
||||
for name in list_registered_platforms(platforms, include_archived=True):
|
||||
config = load_platform_config(name, platforms)
|
||||
contract, registry = generate_site._split_contract(config)
|
||||
with self.subTest(platform=name):
|
||||
errors = [error.message for error in validator.iter_errors(contract)]
|
||||
self.assertEqual(errors, [])
|
||||
self.assertIn("install", registry)
|
||||
self.assertNotIn("_key", registry)
|
||||
self.assertEqual(set(contract) | set(registry) | {"_key"}, set(config) | {"_key"})
|
||||
|
||||
def test_every_schema_id_is_the_address_it_is_served_at(self):
|
||||
for path in sorted((REPO_ROOT / "schemas").glob("*.schema.json")):
|
||||
schema = json.loads(path.read_text())
|
||||
with self.subTest(schema=path.name):
|
||||
self.assertEqual(schema.get("$id"), f"{SITE}api/v1/schemas/{path.name}")
|
||||
|
||||
def test_every_schema_is_published(self):
|
||||
published = {p.name for p in _built(API / "schemas").glob("*.json")}
|
||||
self.assertEqual(published, {p.name for p in (REPO_ROOT / "schemas").glob("*.schema.json")})
|
||||
|
||||
def test_the_built_api_validates(self):
|
||||
from jsonschema import Draft202012Validator # noqa: PLC0415
|
||||
|
||||
schemas = {p.name: json.loads(p.read_text()) for p in (REPO_ROOT / "schemas").glob("*.json")}
|
||||
envelope = Draft202012Validator(schemas["site-api-envelope.schema.json"])
|
||||
platform = Draft202012Validator(schemas["platform.schema.json"])
|
||||
document = json.loads(_built(API / "platforms.json").read_text())
|
||||
self.assertEqual(list(envelope.iter_errors(document)), [])
|
||||
for item in document["items"]:
|
||||
with self.subTest(platform=item["id"]):
|
||||
self.assertEqual([e.message for e in platform.iter_errors(item["contract"])], [])
|
||||
for name in ("systems.json", "cores.json", "index.json", "catalog.json"):
|
||||
with self.subTest(document=name):
|
||||
self.assertEqual(
|
||||
[e.message for e in envelope.iter_errors(json.loads((API / name).read_text()))], [])
|
||||
|
||||
|
||||
class EveryEnvelopeCarriesTheCommit(unittest.TestCase):
|
||||
"""8.5: emulators/ and systems/ lacked `commit` (716 documents)."""
|
||||
|
||||
def test_the_writers_pass_it(self):
|
||||
source = Path(generate_site.__file__).read_text(encoding="utf-8")
|
||||
self.assertIn('str(db.get("generated_at") or _timestamp()), _source_commit(),\n )\n'
|
||||
" clash", source)
|
||||
block = source[source.index("public_profiles = {"):source.index("pages = emulator_pages(")]
|
||||
self.assertIn("write_single_item(", block)
|
||||
self.assertIn("_source_commit()", block)
|
||||
|
||||
def test_every_built_envelope_has_it(self):
|
||||
_built(API / "catalog.json")
|
||||
if not generate_site._source_commit():
|
||||
raise unittest.SkipTest("not a git checkout")
|
||||
lacking = [
|
||||
path.relative_to(API).as_posix() for path in sorted(API.rglob("*.json"))
|
||||
if path.parent.name not in ("schemas", "manifests")
|
||||
and path.name not in ("database.json", "stats.json", "release.json")
|
||||
and "commit" not in json.loads(path.read_text())
|
||||
]
|
||||
self.assertEqual(lacking, [])
|
||||
|
||||
|
||||
class ManifestsArePublished(unittest.TestCase):
|
||||
"""8.2: the manifest a pack installs, byte for byte, in the catalog."""
|
||||
|
||||
def test_each_manifest_is_the_repository_file(self):
|
||||
catalog = json.loads(_built(API / "catalog.json").read_text())
|
||||
listed = {item["url"]: item for item in catalog["items"]}
|
||||
for source in sorted((REPO_ROOT / "install").glob("*.json")):
|
||||
with self.subTest(manifest=source.name):
|
||||
item = listed[f"manifests/{source.name}"]
|
||||
self.assertEqual(item["sha256"], hashlib.sha256(source.read_bytes()).hexdigest())
|
||||
self.assertEqual(item["schema"], "schemas/install-manifest.schema.json")
|
||||
self.assertIn("release.json", listed)
|
||||
self.assertEqual(catalog["license"], generate_site.DATA_LICENSE)
|
||||
|
||||
def test_the_data_page_explains_the_fields_from_the_schema(self):
|
||||
page = generate_site.generate_data_page([])
|
||||
schema = json.loads((REPO_ROOT / "schemas" / "install-manifest.schema.json").read_text())
|
||||
for field in ("dest", "repo_path", "release_asset", "cores"):
|
||||
description = schema["$defs"]["file"]["properties"][field]["description"]
|
||||
self.assertIn(f"| `{field}` | {generate_site._escape_tags(description)} |", page)
|
||||
self.assertIn("## Platform manifests", page)
|
||||
self.assertIn("**Licence:**", page)
|
||||
self.assertIn("## Hosting and limits", page)
|
||||
# The endpoints first, the DAT below them.
|
||||
self.assertLess(page.index("## Stable JSON endpoints"), page.index("The DAT is Logiqx XML"))
|
||||
|
||||
|
||||
class AnIdLeadsToItsData(unittest.TestCase):
|
||||
"""8.6: a system by its id, a core by the name the buildbot gives it."""
|
||||
|
||||
def test_a_buildbot_name_designates_its_profile(self):
|
||||
profiles = {
|
||||
"beetle_psx": {"type": "libretro", "cores": ["mednafen_psx", "mednafen_psx_hw"]},
|
||||
"pcsx2": {"type": "standalone"},
|
||||
"lrps2": {"type": "libretro", "cores": ["pcsx2"]},
|
||||
"mednafen_psx_alias": {"type": "alias", "alias_of": "beetle_psx"},
|
||||
"probe": {"type": "test", "cores": ["probe_core"]},
|
||||
}
|
||||
items = generate_site.core_index_items(profiles)
|
||||
by_name: dict[str, list[tuple[str, bool]]] = {}
|
||||
for item in items:
|
||||
by_name.setdefault(item["name"], []).append((item["profile"], item["preferred"]))
|
||||
self.assertEqual(by_name["mednafen_psx"], [("beetle_psx", True)])
|
||||
self.assertEqual(by_name["pcsx2"], [("pcsx2", True), ("lrps2", False)])
|
||||
self.assertEqual(by_name["mednafen_psx_alias"], [("beetle_psx", True)])
|
||||
self.assertNotIn("probe_core", by_name)
|
||||
|
||||
def test_the_built_index_resolves_ids_to_sections(self):
|
||||
items = {item["id"]: item for item in
|
||||
json.loads(_built(API / "systems.json").read_text())["items"]}
|
||||
color = items["wonderswan-color"]
|
||||
self.assertEqual(color["name"], "Bandai WonderSwan Color")
|
||||
self.assertTrue(color["url"].startswith("/retrobios/systems/bandai/"), color["url"])
|
||||
self.assertTrue(items["sony-playstation"]["sha1"])
|
||||
cores = json.loads((API / "cores.json").read_text())["items"]
|
||||
self.assertIn({"name": "mednafen_psx", "profile": "beetle_psx", "preferred": True}, cores)
|
||||
|
||||
|
||||
class LookupUrlsResolve(unittest.TestCase):
|
||||
"""8.7: a record's url, joined to the shard holding it, is a page."""
|
||||
|
||||
def test_site_absolute(self):
|
||||
db = {"files": {"ab" * 20: {"path": "bios/Sony/PlayStation/scph5501.bin"}}}
|
||||
[item] = generate_site.file_location_items(db)
|
||||
self.assertTrue(item["url"].startswith("/retrobios/systems/"), item["url"])
|
||||
shard = SITE + "api/v1/lookup/ab.json"
|
||||
self.assertTrue(urllib.parse.urljoin(shard, item["url"]).startswith(SITE + "systems/"))
|
||||
|
||||
def test_built_records_land_on_built_pages(self):
|
||||
shard = json.loads(_built(API / "lookup" / "05.json").read_text())
|
||||
dist = REPO_ROOT / "website" / "dist"
|
||||
for record in shard["items"][:200]:
|
||||
with self.subTest(url=record["url"]):
|
||||
self.assertTrue(record["url"].startswith("/retrobios/"))
|
||||
if dist.is_dir():
|
||||
page = urllib.parse.unquote(record["url"].split("#")[0].removeprefix("/retrobios/"))
|
||||
self.assertTrue((dist / page / "index.html").is_file(), page)
|
||||
|
||||
|
||||
class PlatformPagesLinkTheirSystemsAndEmulators(unittest.TestCase):
|
||||
"""8.9: the overview named emulators as ids, none of 1,463 linked."""
|
||||
|
||||
def test_an_emulator_is_named_and_linked(self):
|
||||
link = generate_site._emulator_named_link("beetle_psx", "../")
|
||||
self.assertRegex(link, r"^\[[^\]]+\]\(\.\./emulators/beetle_psx\.md\)$")
|
||||
self.assertNotEqual(link, "[beetle_psx](../emulators/beetle_psx.md)")
|
||||
|
||||
def test_the_overview_column_uses_it(self):
|
||||
source = Path(generate_site.__file__).read_text(encoding="utf-8")
|
||||
block = source[source.index("### Systems overview"):source.index("_render_platform_systems(by_system")]
|
||||
self.assertIn("_emulator_named_link(e, \"../\")", block)
|
||||
self.assertNotIn('", ".join(sys_emus[:3])', block)
|
||||
|
||||
def test_a_moved_list_opens_on_its_console_and_names_its_parent(self):
|
||||
# Past PLATFORM_SECTION_BUDGET, so the list leaves for a page of its own.
|
||||
files = [{"name": f"{'f' * 200}{i}.bin", "status": "ok"} for i in range(1200)]
|
||||
moved: dict[str, str] = {}
|
||||
generate_site._render_platform_systems(
|
||||
{"sony-playstation": files}, {}, {}, ("retroarch", "RetroArch"), moved, {})
|
||||
self.assertTrue(moved)
|
||||
page = next(iter(moved.values()))
|
||||
self.assertTrue(page.startswith('---\nparent:\n label: "RetroArch"\n'
|
||||
' link: "/platforms/retroarch/"\n---\n'), page[:120])
|
||||
self.assertRegex(page, r"RetroArch's list for \[Sony PlayStation\]\(\.\./systems/")
|
||||
|
||||
def test_part_pages_of_gaps_and_cross_reference_name_their_parent(self):
|
||||
moved: dict[str, str] = {}
|
||||
generate_site._list_pages("gaps-x", "T", "intro", [], [["| a |"]], moved,
|
||||
parent=("What the packs do not hold", "/gaps/"))
|
||||
self.assertIn('parent:\n label: "What the packs do not hold"\n link: "/gaps/"', moved["gaps-x"])
|
||||
source = Path(generate_site.__file__).read_text(encoding="utf-8")
|
||||
self.assertIn('_parent_front_matter(("Cores by platform", "/cross-reference/"))', source)
|
||||
self.assertIn('parent=("Dump provenance", "/provenance/")', source)
|
||||
|
||||
|
||||
class TablesFitTheirColumn(unittest.TestCase):
|
||||
"""8.12 and 8.13: a hash in a cell is 12 characters, the rest in its title."""
|
||||
|
||||
def test_the_downloads_table(self):
|
||||
item = {"title": "SQLite snapshot", "url": "downloads/retrobios.sqlite",
|
||||
"media_type": "application/vnd.sqlite3", "bytes": 10, "sha256": "a" * 64}
|
||||
page = generate_site.generate_data_page([item])
|
||||
row = next(line for line in page.splitlines() if "SQLite snapshot" in line and "|" in line)
|
||||
self.assertIn(f'<code title="{"a" * 64}">{"a" * 12}</code>', row)
|
||||
self.assertNotIn("a" * 13, row.replace(f'title="{"a" * 64}"', ""))
|
||||
|
||||
def test_the_gaps_tables(self):
|
||||
problem = {"platform": "Batocera", "name": "x.bin", "status": "untested", "required": True,
|
||||
"reason": "expected md5 differs", "declared": {"md5": ["b" * 32]}, "path": ""}
|
||||
table = "\n".join(generate_site._render_problem_files([problem], db={"files": {}}))
|
||||
row = next(line for line in table.splitlines() if "x.bin" in line)
|
||||
self.assertIn(f'<code title="{"b" * 32}">{"b" * 12}</code>', row)
|
||||
self.assertIn('<span class="rb-reason">expected md5 differs</span>', row)
|
||||
|
||||
|
||||
class SourceLinksGoWhereTheFileIs(unittest.TestCase):
|
||||
"""8.14: trident cited a Panda3DS path and linked it into 3dsTrident."""
|
||||
|
||||
PROFILE = {
|
||||
"emulator": "trident", "type": "libretro",
|
||||
"source": "https://github.com/DanAlexMorton/3dsTrident",
|
||||
"upstream": "https://github.com/wheremyfoodat/Panda3DS",
|
||||
"source_commit": "b" * 40,
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
sitesources._tree_paths.cache_clear()
|
||||
|
||||
def _render(self, tree: frozenset[str] | None) -> str:
|
||||
original = sitesources._tree_paths
|
||||
sitesources._tree_paths = lambda repo, pin: tree # type: ignore[assignment]
|
||||
try:
|
||||
return sitesources._source_ref_markdown(
|
||||
self.PROFILE, "src/emulator.cpp:234; src/core/loader/ncch.cpp:151-164")
|
||||
finally:
|
||||
sitesources._tree_paths = original
|
||||
|
||||
def test_a_path_the_tree_lacks_is_not_linked(self):
|
||||
text = self._render(frozenset({"src/emulator.cpp"}))
|
||||
self.assertIn("[`src/emulator.cpp:234`](https://github.com/DanAlexMorton/3dsTrident/", text)
|
||||
self.assertIn("`src/core/loader/ncch.cpp:151-164`", text)
|
||||
self.assertNotIn("3dsTrident/blob/" + "b" * 40 + "/src/core/loader", text)
|
||||
|
||||
def test_without_a_cached_tree_the_declarations_decide(self):
|
||||
text = self._render(None)
|
||||
self.assertIn("3dsTrident/blob/" + "b" * 40 + "/src/core/loader/ncch.cpp", text)
|
||||
|
||||
def test_every_rendered_link_is_in_its_cached_tree(self):
|
||||
"""Offline: each link the pages render points at a path the cached
|
||||
tree of its repository holds."""
|
||||
if not sitesources.TREE_CACHE.is_dir():
|
||||
raise unittest.SkipTest("no cached tree listings")
|
||||
from common import load_emulator_profiles # noqa: PLC0415
|
||||
|
||||
link = re.compile(r"\(https://github\.com/([^/]+/[^/]+)/blob/([0-9a-f]{40})/([^#)]+)")
|
||||
checked, wrong = 0, []
|
||||
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"), skip_aliases=False)
|
||||
for key, profile in sorted(profiles.items()):
|
||||
for entry in profile.get("files") or []:
|
||||
if not isinstance(entry, dict) or not entry.get("source_ref"):
|
||||
continue
|
||||
for slug, pin, path in link.findall(
|
||||
sitesources._source_ref_markdown(profile, entry["source_ref"])):
|
||||
import upstream # noqa: PLC0415
|
||||
|
||||
repo = upstream.parse_repo(f"https://github.com/{slug}")
|
||||
held = sitesources.tree_holds(repo, pin, urllib.parse.unquote(path))
|
||||
if held is None:
|
||||
continue
|
||||
checked += 1
|
||||
if not held:
|
||||
wrong.append((key, slug, path))
|
||||
if not checked:
|
||||
raise unittest.SkipTest("no rendered link has a cached tree")
|
||||
self.assertEqual(wrong, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -224,7 +224,7 @@ class AnAliasTreeIsListedOnItsParentsPage(unittest.TestCase):
|
||||
members = {"freeintv-overlays": {"a.png": {"sha1": "02" * 20}}}
|
||||
items = generate_site.file_location_items({"files": {}}, self.PROFILES, members)
|
||||
self.assertEqual(items, [{"sha1": "02" * 20,
|
||||
"url": "emulators/freeintv/#sha1-020202020202"}])
|
||||
"url": "/retrobios/emulators/freeintv/#sha1-020202020202"}])
|
||||
|
||||
def test_the_parent_page_lists_the_tree(self):
|
||||
import generate_site
|
||||
|
||||
Reference in new issue
Block a user