mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
feat: link system ids to their files' section
A reviewed SYSTEM_PARENTS table leads variants (WonderSwan Color, Neo Geo MVS, MSX2) to the section holding their files, an Arcade heading that reads the name exactly is accepted, and system_search_aliases() gives the platforms' native ids for search. 260 of 636 ids linked before, 338 after.
This commit is contained in:
1 parent
d83a086355
commit
fad947e9d0
3 files changed
+484
-7
No files matched your search
+190
-6
@@ -223,14 +223,20 @@ def emulator_card_page(emulator: str, anchor: str) -> str:
|
||||
|
||||
|
||||
def system_section(sys_id: str) -> tuple[str, str] | None:
|
||||
"""The console section a system id is named after, as (manufacturer
|
||||
slug, console): its own section, else the one section whose heading
|
||||
reads system_name(). The folder a system's files sit in is no answer:
|
||||
Emerson's Arcadia 2001 led to the Saturn and Sega's NAOMI to the
|
||||
Dreamcast."""
|
||||
"""The console section a system id leads to, as (manufacturer slug,
|
||||
console): its own section, else the parent SYSTEM_PARENTS names, else
|
||||
the one section whose heading reads system_name(). The folder a
|
||||
system's files sit in is no answer by itself: Emerson's Arcadia 2001
|
||||
led to the Saturn and Sega's NAOMI to the Dreamcast.
|
||||
|
||||
A heading under Arcade counts when it reads the name exactly (Namco
|
||||
System 246). One under Other never does, its headings name programs."""
|
||||
own = _own_section(sys_id)
|
||||
if own is not None:
|
||||
return _manufacturer_slug(own[0]), own[1]
|
||||
parent = SYSTEM_PARENTS.get(sys_id)
|
||||
if parent is not None and parent in _section_pairs:
|
||||
return _manufacturer_slug(parent[0]), parent[1]
|
||||
try:
|
||||
name = system_name(sys_id)
|
||||
except KeyError:
|
||||
@@ -239,7 +245,9 @@ def system_section(sys_id: str) -> tuple[str, str] | None:
|
||||
if len(places) != 1:
|
||||
return None
|
||||
mfr, console = next(iter(places))
|
||||
if mfr in _PROGRAM_TOPS:
|
||||
if mfr in _NAMELESS_TOPS or (
|
||||
mfr in _PROGRAM_TOPS and console.casefold() in _program_names
|
||||
):
|
||||
return None
|
||||
return _manufacturer_slug(mfr), console
|
||||
|
||||
@@ -359,7 +367,12 @@ def _by_mode(value: str | dict, mode: str) -> str:
|
||||
# Folders that file copies by the emulator or board that loads them. Their
|
||||
# headings name a program, not a system.
|
||||
_PROGRAM_TOPS = frozenset({"Other", "Arcade"})
|
||||
# Of those, the top whose headings never name a system, even read whole.
|
||||
# An Arcade heading can (Namco System 246), an Other one cannot.
|
||||
_NAMELESS_TOPS = frozenset({"Other"})
|
||||
|
||||
# Every (manufacturer, console) section the system pages carry.
|
||||
_section_pairs: set[tuple[str, str]] = set()
|
||||
# Normalized id -> the console sections spelled that way.
|
||||
_console_sections: dict[str, set[tuple[str, str]]] = {}
|
||||
# A console section by the name its heading reads (Bandai WonderSwan).
|
||||
@@ -402,6 +415,8 @@ def set_console_sections(
|
||||
_console_sections.clear()
|
||||
_sections_by_name.clear()
|
||||
_console_anchors.clear()
|
||||
_section_pairs.clear()
|
||||
_section_pairs.update(pairs)
|
||||
by_maker: dict[str, list[str]] = {}
|
||||
for mfr, console in pairs:
|
||||
by_maker.setdefault(mfr, []).append(console)
|
||||
@@ -927,3 +942,172 @@ SYSTEM_NAMES: dict[str, str] = {
|
||||
"zx-spectrum": "Sinclair ZX Spectrum",
|
||||
"zx-spectrum-128": "Sinclair ZX Spectrum 128",
|
||||
}
|
||||
|
||||
|
||||
# A system id with no section of its own -> the (manufacturer, console)
|
||||
# section holding its files: a model, a regional name or an add-on of the
|
||||
# machine that section covers, or a game whose data that section files.
|
||||
# Reviewed entry by entry against the files the profiles and platforms
|
||||
# declare for the id, never decided by a vote of the folders those files
|
||||
# sit in. A section that is not the machine stays out of this table even
|
||||
# when it holds a copy: NAOMI's naomi2.zip filed with the Dreamcast, the
|
||||
# PET's ROMs in VICE's tree under the C128, the Arcadia's under the Saturn.
|
||||
SYSTEM_PARENTS: dict[str, tuple[str, str]] = {
|
||||
# Nintendo. The twin folders (DS and Nintendo DS, 64DD and Nintendo
|
||||
# 64DD) gave each name two headings and no link.
|
||||
"64dd": ("Nintendo", "Nintendo 64DD"),
|
||||
"nintendo-64dd": ("Nintendo", "Nintendo 64DD"),
|
||||
"nintendo-ds": ("Nintendo", "Nintendo DS"),
|
||||
"nintendo-dsi": ("Nintendo", "Nintendo DS"),
|
||||
"famicom": ("Nintendo", "NES"),
|
||||
"super-famicom": ("Nintendo", "SNES"),
|
||||
# Bandai: the Color boot ROMs, and those of Benesse's WonderSwan-based
|
||||
# Pocket Challenge V2, sit with the WonderSwan.
|
||||
"wonderswan-color": ("Bandai", "WonderSwan"),
|
||||
"bandai-wswanc": ("Bandai", "WonderSwan"),
|
||||
"pocket-challenge-v2": ("Bandai", "WonderSwan"),
|
||||
# SNK
|
||||
"ngpc": ("SNK", "Neo Geo Pocket"),
|
||||
"snk-ngpc": ("SNK", "Neo Geo Pocket"),
|
||||
"neo-geo-pocket-color": ("SNK", "Neo Geo Pocket"),
|
||||
"snk-neogeo-aes": ("SNK", "Neo Geo"),
|
||||
"snk-neogeo-mvs": ("SNK", "Neo Geo"),
|
||||
# Sega. The VMU is the Dreamcast's memory card, its images sit there.
|
||||
# Sega's arcade section holds the OutRun board ROMs Cannonball reads.
|
||||
"sega-genesis": ("Sega", "Mega Drive"),
|
||||
"mega-cd-32x": ("Sega", "Mega CD"),
|
||||
"sega-vmu": ("Sega", "Dreamcast"),
|
||||
"sega-outrun": ("Sega", "Arcade"),
|
||||
# NEC and Pioneer: both LaserActive packs (LD-ROM2 and Mega LD) file
|
||||
# their BIOS under the LaserActive.
|
||||
"nec-supergrafx": ("NEC", "PC Engine"),
|
||||
"supergrafx": ("NEC", "PC Engine"),
|
||||
"supergrafx-cd": ("NEC", "PC Engine CD"),
|
||||
"pc-engine-ld": ("Pioneer", "LaserActive"),
|
||||
"mega-ld": ("Pioneer", "LaserActive"),
|
||||
# Microsoft MSX generations
|
||||
"microsoft-msx2": ("Microsoft", "MSX"),
|
||||
"msx2": ("Microsoft", "MSX"),
|
||||
"msx2plus": ("Microsoft", "MSX"),
|
||||
"msx2+": ("Microsoft", "MSX"),
|
||||
"msxturboR": ("Microsoft", "MSX"),
|
||||
"msxturbor": ("Microsoft", "MSX"),
|
||||
# Sinclair: 128-0.rom and 128-1.rom, and Timex's tc2048.rom.
|
||||
"zx-spectrum-128": ("Sinclair", "ZX Spectrum"),
|
||||
"timex-tc2048": ("Sinclair", "ZX Spectrum"),
|
||||
# Acorn: os12.rom and basic2.rom for the Model B and B+, and the
|
||||
# Master Compact's bbcmc.zip.
|
||||
"bbc-micro-b": ("Acorn", "BBC Micro"),
|
||||
"bbc-micro-bplus": ("Acorn", "BBC Micro"),
|
||||
"bbc-master-compact": ("Acorn", "BBC Micro"),
|
||||
# Atari: falcon.img, ste.img and tt.img sit with the ST TOS images.
|
||||
"atari-ste": ("Atari", "ST"),
|
||||
"atari-tt": ("Atari", "ST"),
|
||||
"atari-falcon": ("Atari", "ST"),
|
||||
# Home computers filed under their family
|
||||
"apple-iie": ("Apple", "Apple II"),
|
||||
"apf-m1000": ("APF", "Imagination Machine"),
|
||||
"apfm1000": ("APF", "Imagination Machine"),
|
||||
"bbk-longman-4980": ("BBK", "A4980"),
|
||||
"bbk-longman-4988": ("BBK", "A4988"),
|
||||
"coco3": ("Tandy", "CoCo"),
|
||||
"commodore-cdtv": ("Commodore", "Amiga"),
|
||||
"dragon32": ("Dragon", "Dragon"),
|
||||
"dragon64": ("Dragon", "Dragon"),
|
||||
"enterprise-64": ("Enterprise", "64-128"),
|
||||
"enterprise-128": ("Enterprise", "64-128"),
|
||||
"fm77av": ("Fujitsu", "FM-7"),
|
||||
"gamepock": ("Epoch", "Game Pocket"),
|
||||
"ibm-pcjr": ("IBM", "PC"),
|
||||
"ibm-xt": ("IBM", "PC"),
|
||||
"marty": ("Fujitsu", "FM Towns"),
|
||||
"mz700": ("Sharp", "MZ"),
|
||||
"mz80k": ("Sharp", "MZ"),
|
||||
"oricatmos": ("Oric", "Oric"),
|
||||
"palm-os": ("Palm", "Palm"),
|
||||
"philips-vg5k": ("Philips", "VG5000"),
|
||||
"vg5k": ("Philips", "VG5000"),
|
||||
"videobrain": ("VideoBrain", "Family Computer"),
|
||||
# Files kept at the maker's root, so each file is a heading of its own
|
||||
# on a page that is the machine's: the first one leads there.
|
||||
"galaksija": ("Galaksija", "CHRGEN.BIN"),
|
||||
"vircon32": ("Vircon", "Vircon32Bios.v32"),
|
||||
# Games, filed with the game whose folder holds their data
|
||||
"dinothawr": ("Dinothawr", "Dinothawr"),
|
||||
"doom": ("Id Software", "Doom"),
|
||||
"ctf": ("Id Software", "Quake II"),
|
||||
"teamarena": ("Id Software", "Quake III Arena"),
|
||||
"enemyterritory": ("Id Software", "Wolfenstein Enemy Territory"),
|
||||
"gearbox": ("Valve", "Half-Life"),
|
||||
"dmc": ("Valve", "Half-Life"),
|
||||
"ricochet": ("Valve", "Half-Life"),
|
||||
"tfc": ("Valve", "Half-Life"),
|
||||
"mohaa": ("Electronic Arts", "Medal of Honor Allied Assault"),
|
||||
"mohaa-breakthrough": ("Electronic Arts", "Medal of Honor Allied Assault"),
|
||||
"mohaa-spearhead": ("Electronic Arts", "Medal of Honor Allied Assault"),
|
||||
"ur-quan-masters": ("Toys for Bob", "The Ur-Quan Masters"),
|
||||
"star-control-2": ("Toys for Bob", "The Ur-Quan Masters"),
|
||||
# The collection's general arcade section, where naomi.zip, neogeo.zip
|
||||
# and the other board sets sit.
|
||||
"arcade": ("Arcade", "Arcade"),
|
||||
}
|
||||
|
||||
|
||||
def system_ids(profiles: dict[str, dict], platforms: list[dict]) -> set[str]:
|
||||
"""Every system id a page can name: the platforms' and the profiles'."""
|
||||
ids = {str(sys_id) for config in platforms for sys_id in config.get("systems", {})}
|
||||
for profile in profiles.values():
|
||||
ids.update(str(sys_id) for sys_id in profile.get("systems") or [])
|
||||
for entry in profile.get("files") or []:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
if entry.get("system"):
|
||||
ids.add(str(entry["system"]))
|
||||
ids.update(str(sys_id) for sys_id in entry.get("systems") or [])
|
||||
return ids
|
||||
|
||||
|
||||
def link_coverage(ids: set[str]) -> tuple[int, int]:
|
||||
"""How many of ``ids`` lead to a section, and how many there are."""
|
||||
return sum(1 for sys_id in ids if system_section(sys_id)), len(ids)
|
||||
|
||||
|
||||
# A system id -> the names the platforms' own files give it, built once.
|
||||
_search_aliases: dict[str, list[str]] = {}
|
||||
_search_aliases_built = [False]
|
||||
# A native name as a person types it: no space, no comma. RetroArch's
|
||||
# native names are No-Intro titles ("Sony - PlayStation") and stay out.
|
||||
_ALIAS_SHAPE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.+-]*")
|
||||
|
||||
|
||||
def system_search_aliases(sys_id: str) -> list[str]:
|
||||
"""The native system ids the platform YAMLs file ``sys_id`` under
|
||||
(psx, dreamcast, gba), sorted, for search. Read from each platform's
|
||||
own entries by common.platform_system_names, never typed by hand."""
|
||||
if not _search_aliases_built[0]:
|
||||
from common import (
|
||||
list_registered_platforms,
|
||||
load_platform_config,
|
||||
platform_system_names,
|
||||
)
|
||||
|
||||
platforms_dir = str(_REPO_ROOT / "platforms")
|
||||
index: dict[str, set[str]] = {}
|
||||
for platform in list_registered_platforms(platforms_dir, include_archived=True):
|
||||
config = load_platform_config(platform, platforms_dir)
|
||||
names = platform_system_names(config, list(config.get("systems", {})))
|
||||
for owner, spelled in names.items():
|
||||
index.setdefault(owner, set()).update(
|
||||
name for name in spelled if _ALIAS_SHAPE.fullmatch(name)
|
||||
)
|
||||
# Search ignores case: one spelling per word, the lower-case one
|
||||
# when a platform writes it (psx, not PSX).
|
||||
_search_aliases.clear()
|
||||
for owner, names in index.items():
|
||||
spellings: dict[str, str] = {}
|
||||
for name in sorted(names, key=lambda n: (n != n.lower(), n)):
|
||||
spellings.setdefault(name.casefold(), name)
|
||||
if spellings:
|
||||
_search_aliases[owner] = sorted(spellings.values(), key=str.casefold)
|
||||
_search_aliases_built[0] = True
|
||||
return list(_search_aliases.get(str(sys_id), []))
|
||||
@@ -0,0 +1,293 @@
|
||||
"""Every system id with a section leads to it (round 3, 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, and accepts an
|
||||
Arcade heading that reads the name exactly. 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). The heading
|
||||
# names the program, not the system.
|
||||
"atari-system1": "Other > fbneo",
|
||||
"bbc-master-128": "Other > beebem",
|
||||
"bbc-master-et": "Other > beebem",
|
||||
"capcom-cps3": "Arcade > FBNeo",
|
||||
"cave-story": "Other > NXEngine",
|
||||
"cps3": "Arcade > Arcade holds one CPS-3 set among every board's",
|
||||
"commanderx16": "Arcade > Arcade",
|
||||
"dark-forces-ii": "Game Engines > OpenJKDF2",
|
||||
"deco-cassette": "Arcade > FBNeo",
|
||||
"dos": "DOSBox, Roland and Other folders",
|
||||
"fpinball": "Other > future-pinball-fploader",
|
||||
"fruitmachine": "Other > mfme",
|
||||
"igs-pgm": "Arcade > FBNeo",
|
||||
"konami-bubsys": "Arcade > FBNeo",
|
||||
"microsoft-xbox-360": "Other > xenia-edge",
|
||||
"midway-mcr": "Arcade > FBNeo",
|
||||
"mysteries-of-the-sith": "Game Engines > OpenJKDF2",
|
||||
"mz2000": "Arcade > Arcade, not the Sharp MZ section",
|
||||
"mz2500": "Arcade > Arcade, not the Sharp MZ section",
|
||||
"mz800": "Arcade > Arcade, not the Sharp MZ section",
|
||||
"namco-na1": "Arcade > FBNeo",
|
||||
"namco-nb1": "Arcade > FBNeo",
|
||||
"nmk16": "Arcade > FBNeo",
|
||||
"omicron": "Other > omicron",
|
||||
"pegasus": "Arcade > MAME",
|
||||
"rpgmaker": "RPG Maker > easyrpg",
|
||||
"scummvm": "Other > ScummVM",
|
||||
"sdlpal": "Other > sdlpal",
|
||||
"sega-system16b": "Arcade > FBNeo",
|
||||
"sinclair-zx-spectrum-next": "Other > cspect",
|
||||
"smbx": "Game Engines > TheXTech",
|
||||
"solarus": "Game Engines > Solarus",
|
||||
"sunplus-spmp8000": "Other > spmp8000emu",
|
||||
"super-kaneko-nova": "Arcade > FBNeo",
|
||||
"taito-cchip": "Arcade > FBNeo",
|
||||
"thomson-mo6": "Other > Clock Signal",
|
||||
"tomb-raider-1": "Game Engines > TRX",
|
||||
"vgmplay": "Arcade > Arcade and MAME",
|
||||
"vm-labs-nuon": "Other > nuance",
|
||||
"vpinball": "Arcade > PinMAME",
|
||||
"xrick": "Other > xrick",
|
||||
"zc210": "ZC > ZC210, the emulator's folder",
|
||||
# Files filed with another machine: a link would name the wrong one.
|
||||
"atom": "atom.zip is filed with the BBC Micro",
|
||||
"commodore-c64-supercpu": "VICE's tree under the C128",
|
||||
"commodore-cbm-ii": "VICE's tree under the C128",
|
||||
"commodore-pet": "VICE's tree under the C128",
|
||||
"emerson-arcadia-2001": "ar_bios.zip is filed with the Saturn",
|
||||
"naomi2": "naomi2.zip is filed with the Dreamcast",
|
||||
"neogeo64": "the platform declares the Neo Geo's neogeo.zip",
|
||||
"sega-dreamcast-arcade": "naomi.zip and awbios.zip, Arcade and Dreamcast",
|
||||
"spectravideo": "the SVI ROMs are filed with the MSX",
|
||||
"vvvvvv": "SDL's controller database",
|
||||
"ym2608": "an FBNeo sound ROM, also filed with the NES",
|
||||
# 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))
|
||||
|
||||
|
||||
@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))
|
||||
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, []):
|
||||
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)
|
||||
|
||||
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")
|
||||
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"))
|
||||
# No heading reads NAOMI: its sets are cards of the general section.
|
||||
self.assertIsNone(siterender.system_section("sega-naomi"))
|
||||
# Never a folder of copies a core loads.
|
||||
self.assertIsNone(siterender.system_section("igs-pgm"))
|
||||
|
||||
def test_folder_votes_stay_out(self) -> None:
|
||||
for sys_id in ("emerson-arcadia-2001", "naomi2", "commodore-pet"):
|
||||
with self.subTest(sys_id=sys_id):
|
||||
self.assertIsNone(siterender.system_section(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()
|
||||
@@ -54,7 +54,7 @@ Every module in `tests/`, by the area it guards.
|
||||
| Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_offline`, `test_install_selection` |
|
||||
| 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_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `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_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `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_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
|
||||
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `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_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `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_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_p0`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
|
||||
|
||||
## Test architecture
|
||||
|
||||
|
||||
Reference in new issue
Block a user