Files
libretro/tests/test_site_r3_p0.py
T
Abdessamad Derraz 4dd3800f11 fix: file system cards by board, machine, engine
Every board set sits on its board page whatever folder holds it, a set a
reviewed machine owns joins that machine (neogeo.zip under SNK, the Atom
under Acorn, the MZ-2000 under Sharp), and a game engine's files form one
section named after it. Board pages drop the heading that repeated their
title, parts drop the Part of line their crumbs already say, and pages
that moved redirect.
2026-10-24 01:19:56 +02:00

310 lines
14 KiB
Python

"""Every system id with a section leads to it (rounds 3 and 4, package P0).
260 of 636 system ids linked: a variant (WonderSwan Color, Neo Geo MVS)
never matched a heading, twin folders (DS and Nintendo DS) made the exact
name ambiguous, and every Arcade heading was refused. siterender now reads
a reviewed SYSTEM_PARENTS table after the id's own section, accepts an
Arcade heading that reads the name exactly, and leads an arcade board
(NAOMI, ZN-1, PGM) to its own section. An id counts as holding files
through a set archive (``category: bios_zip``) its profile declares for it,
not only through a file's own hash. An id whose files the collection holds
and that stays plain is named below, with the reason.
Reads the repository: skips when the database is absent.
"""
from __future__ import annotations
import json
import os
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import siterender # noqa: E402
# Ids whose files the collection holds and that lead to no section.
UNLINKED: dict[str, str] = {
# Files only under a program's folder (Other/<emulator>, Arcade/<core>,
# Game Engines/<engine>, a top named after the program), a folder of
# several systems' files or not reviewed as the system's own. The
# heading names the program, not the system.
"capcom-cps3": "Arcade > FBNeo",
"cps3": "Arcade > Arcade holds one CPS-3 set among every board's",
"commanderx16": "Arcade > Arcade",
"dark-forces-ii": "Game Engines > OpenJKDF2",
"fpinball": "Other > future-pinball-fploader",
"fruitmachine": "Other > mfme",
"mysteries-of-the-sith": "Game Engines > OpenJKDF2",
"namco-na1": "Arcade > FBNeo",
"namco-nb1": "Arcade > FBNeo",
"omicron": "Other > omicron",
"rpgmaker": "RPG Maker > easyrpg",
"sdlpal": "the SDLPAL section under its own top, named after the program",
"sega-system16b": "Arcade > FBNeo, and isgsm.zip is also the ISG Selection Master's",
"smbx": "Game Engines > TheXTech",
"thomson-mo6": "Other > Clock Signal",
"vgmplay": "qsound.zip, ym2413.zip, ym2608.zip: sound chips, no board",
# Files filed with another machine: a link would name the wrong one.
"commodore-c64-supercpu": "VICE's tree under the C128",
"emerson-arcadia-2001": "ar_bios.zip is filed with the Saturn",
"spectravideo": "the SVI ROMs are filed with the MSX",
"taito-type-x": "mame2010 files taitogn.zip, the G-NET BIOS, under this id",
"vvvvvv": "SDL's controller database",
# Files of distinct machines or games, one section each.
"catacomb-adventure": "three games, three sections",
"sinclair-zx80-81": "ZX 80 and ZX 81 sections",
}
def _accepts(fe: dict, local: str | None, status: str) -> bool:
"""A resolution that proves the entry's content or its full path."""
from common import resolution_is_hash_exact
if not local:
return False
if resolution_is_hash_exact(status) or status == "path_exact":
return True
path = fe.get("path")
return bool(path) and local.endswith("/" + str(path))
def _archives_held(fe: dict, db: dict) -> set[str]:
"""The copies of a set archive (``category: bios_zip``) the collection
holds: by its name, kept when it holds one CRC32 its ``contents:``
lists. MAME hashes the members, never the archive, so the entry has no
hash of its own and a name match is the only way to it."""
from ziptools import zip_member_crcs
crcs = {str(m["crc32"]).lower() for m in fe.get("contents") or []
if isinstance(m, dict) and m.get("crc32")}
names = {str(fe.get("name", ""))} | {str(a) for a in fe.get("aliases") or []}
held: set[str] = set()
for name in names:
for sha1 in db["indexes"]["by_name"].get(name.rsplit("/", 1)[-1], []):
members = zip_member_crcs(str(REPO_ROOT / db["files"][sha1]["path"]))
if not crcs or not members or members & crcs:
held.add(sha1)
return held
@unittest.skipUnless((REPO_ROOT / "database.json").exists(), "database.json is not built")
class SystemLinks(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
previous = os.getcwd()
os.chdir(REPO_ROOT)
try:
from common import (
list_registered_platforms,
load_emulator_profiles,
load_platform_config,
resolve_local_file,
)
from generate_site import _group_by_manufacturer, arcade_board_sections
with open("database.json", encoding="utf-8") as handle:
db = json.load(handle)
cls.manufacturers = _group_by_manufacturer(db)
profiles = load_emulator_profiles("emulators", skip_aliases=False)
siterender.set_console_sections(
cls.manufacturers, siterender.program_names(profiles))
# The system pages give every arcade board a section of its own
# (round 4, 6.4) and record it. The built pages are checked by
# the round 4 gate.
siterender.reset_console_pages()
# The sections as the system pages hold them: each board, the
# sets a reviewed machine owns under that machine.
cls.sections, _installs = arcade_board_sections(cls.manufacturers, {})
platforms = [
load_platform_config(name, "platforms")
for name in list_registered_platforms("platforms", include_archived=True)
]
cls.ids = siterender.system_ids(profiles, platforms)
entries: dict[str, list[dict]] = {}
for config in platforms:
for sys_id, system in config.get("systems", {}).items():
entries.setdefault(sys_id, []).extend(system.get("files") or [])
for profile in profiles.values():
systems = [str(s) for s in profile.get("systems") or []]
for fe in profile.get("files") or []:
if not isinstance(fe, dict):
continue
owners = {str(fe["system"])} if fe.get("system") else set()
owners.update(str(s) for s in fe.get("systems") or [])
if not owners and len(systems) == 1:
owners = set(systems)
for owner in owners:
entries.setdefault(owner, []).append(fe)
# Where each content has a card.
places: dict[str, set[tuple[str, str, str]]] = {}
for mfr, consoles in cls.sections.items():
for console, files in consoles.items():
for f in files:
places.setdefault(f["sha1"], set()).add((mfr, console, f["name"]))
by_path = {entry.get("path"): sha1 for sha1, entry in db["files"].items()}
# Id -> the (manufacturer, console) sections holding its files.
cls.held: dict[str, set[tuple[str, str]]] = {}
for sys_id in cls.ids:
sections: set[tuple[str, str]] = set()
for fe in entries.get(sys_id, []):
if fe.get("category") == "bios_zip":
for sha1 in _archives_held(fe, db):
sections.update((m, c) for m, c, _ in places.get(sha1, set()))
continue
local, status = resolve_local_file(fe, db)
if not _accepts(fe, local, status):
continue
sha1 = by_path.get(local)
if sha1 is not None:
sections.update((m, c) for m, c, _ in places.get(sha1, set()))
if sections:
cls.held[sys_id] = sections
finally:
os.chdir(previous)
@classmethod
def tearDownClass(cls) -> None:
siterender.reset_console_pages()
def test_every_id_with_files_links(self) -> None:
linked, total = siterender.link_coverage(self.ids)
with_files = set(self.held)
print(f"\n {linked} of {total} system ids linked, "
f"{len(with_files - set(UNLINKED))} of {len(with_files)} with files, "
f"{len(self.ids - with_files)} with no file")
plain = sorted(
sys_id for sys_id in with_files - set(UNLINKED)
if siterender.system_section(sys_id) is None
)
self.assertEqual(plain, [], "link these or name them in UNLINKED with the reason")
def test_unlinked_list_is_current(self) -> None:
stale = sorted(
sys_id for sys_id in UNLINKED
if sys_id not in self.held or siterender.system_section(sys_id) is not None
)
self.assertEqual(stale, [])
def test_a_matching_heading_is_linked(self) -> None:
"""An id whose name reads one heading, outside Other, leads there."""
by_heading: dict[str, set[tuple[str, str]]] = {}
for mfr, consoles in self.manufacturers.items():
for console in consoles:
for name in {siterender._heading_name(mfr, console), console}:
by_heading.setdefault(name.casefold(), set()).add((mfr, console))
missed = []
for sys_id in sorted(self.ids):
places = by_heading.get(siterender.system_name(sys_id).casefold(), set())
if len(places) != 1:
continue
mfr, console = next(iter(places))
if mfr == "Other" or (
mfr == "Arcade" and console.casefold() in siterender._program_names
):
continue
if siterender.system_section(sys_id) is None:
missed.append(sys_id)
self.assertEqual(missed, [])
def test_parents_are_sections(self) -> None:
sections = {
(mfr, console)
for mfr, consoles in self.sections.items() for console in consoles
}
self.assertEqual(
sorted(k for k, v in siterender.SYSTEM_PARENTS.items() if v not in sections), [])
def test_parents_hold_the_files(self) -> None:
"""A parent holds a file of the id, or of an id of the same name.
An entry its own section shadows is never read and is not judged."""
by_name: dict[str, set[tuple[str, str]]] = {}
for sys_id, sections in self.held.items():
by_name.setdefault(siterender.system_name(sys_id), set()).update(sections)
wrong = []
for sys_id, parent in sorted(siterender.SYSTEM_PARENTS.items()):
if siterender._own_section(sys_id) is not None:
continue
held = by_name.get(siterender.system_name(sys_id))
if held is not None and parent not in held:
wrong.append((sys_id, parent))
self.assertEqual(wrong, [])
def test_variants_lead_to_their_family(self) -> None:
expect = {
"wonderswan-color": ("bandai", "WonderSwan"),
"bandai-wswanc": ("bandai", "WonderSwan"),
"ngpc": ("snk", "Neo Geo Pocket"),
"snk-neogeo-mvs": ("snk", "Neo Geo"),
"snk-neogeo-aes": ("snk", "Neo Geo"),
"nintendo-ds": ("nintendo", "Nintendo DS"),
"nintendo-64dd": ("nintendo", "Nintendo 64DD"),
"zx-spectrum-128": ("sinclair", "ZX Spectrum"),
"pc-engine-ld": ("pioneer", "LaserActive"),
}
for sys_id, section in expect.items():
with self.subTest(sys_id=sys_id):
self.assertEqual(siterender.system_section(sys_id), section)
def test_arcade_heading_by_exact_name(self) -> None:
self.assertEqual(
siterender.system_section("namco-system246"), ("arcade", "Namco System 246"))
# A board leads to its own section, never a folder of copies a core
# loads (Arcade > FBNeo).
self.assertEqual(siterender.system_section("sega-naomi"), ("arcade", "Sega NAOMI"))
self.assertEqual(
siterender.system_section("igs-pgm"), ("arcade", "IGS PolyGame Master"))
def test_folder_votes_stay_out(self) -> None:
"""A copy filed with another machine names no section: the Arcadia
stays plain, NAOMI 2 goes to its board, never to the Dreamcast."""
self.assertIsNone(siterender.system_section("emerson-arcadia-2001"))
self.assertEqual(siterender.system_section("naomi2"), ("arcade", "Sega NAOMI 2"))
for sys_id in ("emerson-arcadia-2001", "naomi2"):
with self.subTest(sys_id=sys_id):
self.assertNotIn(sys_id, siterender.SYSTEM_PARENTS)
def test_emulator_rows_link_the_variant(self) -> None:
from generate_site import _named_system_link
self.assertEqual(
_named_system_link("wonderswan-color", "../"),
"[Bandai WonderSwan Color](../systems/bandai.md#wonderswan)")
class SearchAliases(unittest.TestCase):
def test_platform_names(self) -> None:
self.assertIn("psx", siterender.system_search_aliases("sony-playstation"))
self.assertIn("dc", siterender.system_search_aliases("sega-dreamcast"))
self.assertIn("gba", siterender.system_search_aliases("nintendo-gba"))
def test_one_typed_word_per_name(self) -> None:
siterender.system_search_aliases("sony-playstation")
for sys_id, names in siterender._search_aliases.items():
with self.subTest(sys_id=sys_id):
self.assertNotIn(sys_id, names)
self.assertEqual(len({n.casefold() for n in names}), len(names))
for name in names:
self.assertNotRegex(name, r"[\s,]")
def test_unknown_id(self) -> None:
self.assertEqual(siterender.system_search_aliases("no-such-system"), [])
class BuildCount(unittest.TestCase):
def test_generator_prints_the_count(self) -> None:
source = (REPO_ROOT / "scripts" / "generate_site.py").read_text(encoding="utf-8")
self.assertIn("link_coverage(", source)
self.assertIn("system ids linked", source)
if __name__ == "__main__":
unittest.main()