Files
libretro/tests/test_site_r3_p0.py
T

305 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": "Other > sdlpal",
"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",
"sega-dreamcast-arcade": "NAOMI, NAOMI 2 and Atomiswave sets, three boards",
"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 _copy_places, _group_by_manufacturer
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()
for board in siterender.arcade_boards():
siterender.record_board_section(board)
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)
places = _copy_places(db)
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.manufacturers.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()