mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 04:39:06 -05:00
305 lines
14 KiB
Python
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()
|