mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
The start line keeps entries read for the section's own system, groups a section holding several, names files the way most setups do, links an Arcade-filed set the system requires and says when no file is needed. Cards of MAME and FBNeo sets name the releases whose recipe matches their members. Console headings lose their icon, Arcade gets its own group on the index, and install boxes count toward the weight that splits a heavy manufacturer page.
311 lines
15 KiB
Python
311 lines
15 KiB
Python
"""System pages and file cards (round 3, package 6).
|
|
|
|
The DS section opened on five DSi files, the Game Boy Advance line printed
|
|
BizHawk's file name where eight setups write another, the Neo Geo section
|
|
opened on a file one emulator reads, no arcade card said which release its
|
|
set matches, console headings carried an icon on 15 % of them, arcade boards
|
|
sat under "Consoles and computers", and every system page shared one card.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
import types
|
|
import unittest
|
|
import zipfile
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
|
|
import generate_site as gs # noqa: E402
|
|
import make_og_cards # noqa: E402
|
|
|
|
DIST = REPO_ROOT / "website" / "dist"
|
|
TMP = REPO_ROOT / "tmp"
|
|
|
|
|
|
def _file(name: str, char: str, path: str) -> dict:
|
|
return {"name": name, "sha1": char * 40, "md5": char * 32, "size": 4096, "path": path}
|
|
|
|
|
|
def _consumers(
|
|
readers: dict[str, list[tuple[str, dict]]],
|
|
db_files: list[dict] = (),
|
|
destinations: dict[str, dict[str, set[str]]] | None = None,
|
|
) -> types.SimpleNamespace:
|
|
"""Consumers where each sha1 is read by the given (emulator, entry)."""
|
|
files_of: dict[str, list] = {}
|
|
entries: dict[str, dict[str, int]] = {}
|
|
emulators: dict[str, set[str]] = {}
|
|
for sha1, read_by in readers.items():
|
|
for emulator, entry in read_by:
|
|
files_of.setdefault(emulator, []).append(entry)
|
|
entries.setdefault(sha1, {})[emulator] = len(files_of[emulator]) - 1
|
|
emulators.setdefault(sha1, set()).add(emulator)
|
|
return types.SimpleNamespace(
|
|
platforms={}, emulators=emulators, emulator_entries=entries,
|
|
emulator_files=files_of, installed={}, platform_systems={},
|
|
platform_destinations=destinations or {},
|
|
_db={"files": {f["sha1"]: f for f in db_files}},
|
|
)
|
|
|
|
|
|
DS = ("Nintendo", "Nintendo DS")
|
|
|
|
|
|
class TheSectionStartsWithItsOwnConsole(unittest.TestCase):
|
|
"""A file filed with the DS but read for the DSi is the DSi's."""
|
|
|
|
def setUp(self) -> None:
|
|
folder = "bios/Nintendo/Nintendo DS"
|
|
self.files = [_file("NDS_Bios7.bin", "7", f"{folder}/NDS_Bios7.bin"),
|
|
_file("NDS_Bios9.bin", "9", f"{folder}/NDS_Bios9.bin"),
|
|
_file("dsi_bios7.bin", "d", f"{folder}/dsi_bios7.bin")]
|
|
hle = {"required": False, "hle_fallback": True, "system": "nintendo-ds"}
|
|
self.consumers = _consumers({
|
|
"7" * 40: [("emu-a", {**hle, "name": "bios7.bin"}),
|
|
("emu-b", {**hle, "name": "bios7.bin"})],
|
|
"9" * 40: [("emu-a", {**hle, "name": "bios9.bin"})],
|
|
"d" * 40: [("emu-a", {"name": "dsi_bios7.bin", "required": True,
|
|
"system": "nintendo-dsi"})],
|
|
})
|
|
|
|
def test_no_dsi_file_on_the_ds_line(self) -> None:
|
|
text = "\n".join(gs._start_with(self.files, self.consumers, section=DS))
|
|
self.assertNotIn("dsi", text)
|
|
|
|
def test_a_console_its_emulators_run_without_says_so(self) -> None:
|
|
self.assertEqual(gs._start_with(self.files, self.consumers, section=DS), [
|
|
"**No file needed** with emu-a or emu-b. For full accuracy: "
|
|
"`bios7.bin`, `bios9.bin`.", ""])
|
|
|
|
def test_the_dsi_section_names_its_file(self) -> None:
|
|
lines = gs._start_with(self.files, self.consumers, section=("Nintendo", "Nintendo DSi"))
|
|
self.assertEqual(lines, ["**Start with**: `dsi_bios7.bin`.", ""])
|
|
|
|
def test_more_than_three_emulators_are_counted(self) -> None:
|
|
hle = {"required": False, "hle_fallback": True, "system": "nintendo-ds"}
|
|
consumers = _consumers({"7" * 40: [(f"emu-{n}", hle) for n in "abcd"]})
|
|
self.assertEqual(gs._start_with(self.files[:1], consumers, section=DS)[0],
|
|
"**No file needed**: none of its 4 emulators requires one. "
|
|
"For full accuracy: `NDS_Bios7.bin`.")
|
|
|
|
def test_a_shipped_file_is_no_start(self) -> None:
|
|
consumers = _consumers({"7" * 40: [("emu-a", {"required": True, "bundled": True,
|
|
"system": "nintendo-ds"})]})
|
|
self.assertEqual(gs._start_with(self.files[:1], consumers, section=DS), [])
|
|
|
|
|
|
class TheNameSetupsWrite(unittest.TestCase):
|
|
def test_most_setups_win_over_the_collection_name(self) -> None:
|
|
f = _file("GBA_bios.rom", "a", "bios/Nintendo/Game Boy Advance/GBA_bios.rom")
|
|
consumers = _consumers(
|
|
{"a" * 40: [("emu-a", {"name": "GBA_bios.rom", "required": True,
|
|
"system": "nintendo-gba"})]},
|
|
destinations={"a" * 40: {"p1": {"gba_bios.bin"}, "p2": {"bios/gba_bios.bin"},
|
|
"p3": {"GBA_bios.rom"}}})
|
|
self.assertEqual(gs._start_with([f], consumers, section=("Nintendo", "Game Boy Advance")),
|
|
["**Start with**: `gba_bios.bin`.", ""])
|
|
|
|
def test_no_setup_then_the_emulators_name(self) -> None:
|
|
f = _file("x.rom", "b", "bios/Nintendo/Game Boy Advance/x.rom")
|
|
consumers = _consumers({"b" * 40: [("emu-a", {"name": "gba_bios.bin", "required": True})]})
|
|
self.assertEqual(gs._setup_name(f, consumers, [("emu-a", {"name": "gba_bios.bin"})]),
|
|
"gba_bios.bin")
|
|
|
|
|
|
class ArcadeSetsAreNamedWhereverFiled(unittest.TestCase):
|
|
"""The Neo Geo BIOS is under Arcade: the Neo Geo section named
|
|
aes.zip, read by one emulator, and never neogeo.zip."""
|
|
|
|
def setUp(self) -> None:
|
|
self.aes = _file("aes.zip", "1", "bios/SNK/Neo Geo/aes.zip")
|
|
neogeo = _file("neogeo.zip", "2", "bios/Arcade/Arcade/neogeo.zip")
|
|
mega_cd = _file("bios_CD_U.bin", "3", "bios/Sega/Mega CD/bios_CD_U.bin")
|
|
shader = _file("tfx.glsl", "4", "bios/Other/emu/tfx.glsl")
|
|
mvs = {"required": True, "system": "snk-neogeo-mvs"}
|
|
self.consumers = _consumers({
|
|
"1" * 40: [("geolith", {"name": "aes.zip", "required": True,
|
|
"system": "snk-neogeo-aes"})],
|
|
"2" * 40: [(f"mame{n}", {**mvs, "name": "neogeo.zip"}) for n in range(3)],
|
|
"3" * 40: [("bizhawk", {"name": "bios_CD_U.bin", "required": True,
|
|
"system": "snk-neogeo-mvs"})],
|
|
"4" * 40: [("emu", {"name": "tfx.glsl", "required": True,
|
|
"system": "snk-neogeo-mvs"})],
|
|
}, db_files=[self.aes, neogeo, mega_cd, shader])
|
|
|
|
def test_one_line_per_system_and_the_arcade_set_linked(self) -> None:
|
|
lines = gs._start_with([self.aes], self.consumers, section=("SNK", "Neo Geo"))
|
|
self.assertEqual(lines[0], "**Start with** the files of your machine:")
|
|
link = f"[`neogeo.zip`](/retrobios/files/?sha1={'2' * 40})"
|
|
self.assertIn(f"- **SNK Neo Geo MVS**: {link}", lines)
|
|
self.assertIn("- **SNK Neo Geo AES**: `aes.zip`", lines)
|
|
|
|
def test_another_consoles_file_or_an_emulators_folder_is_left_out(self) -> None:
|
|
text = "\n".join(gs._start_with([self.aes], self.consumers, section=("SNK", "Neo Geo")))
|
|
self.assertNotIn("bios_CD_U.bin", text)
|
|
self.assertNotIn("tfx.glsl", text)
|
|
|
|
def test_the_card_read_by_most_emulators_comes_first(self) -> None:
|
|
one = _file("aes.zip", "1", "bios/SNK/Neo Geo/aes.zip")
|
|
many = _file("sp-s3.sp1", "5", "bios/SNK/Neo Geo/sp-s3.sp1")
|
|
consumers = _consumers({
|
|
"1" * 40: [("geolith", {"required": True})],
|
|
"5" * 40: [(f"emu{n}", {"required": True}) for n in range(3)],
|
|
})
|
|
cards = gs._console_cards("SNK", [one, many], consumers, set())
|
|
self.assertIn("sp-s3.sp1", cards[0][1][2])
|
|
|
|
def test_a_long_list_names_the_first_files(self) -> None:
|
|
files = [_file(f"rom{i}.bin", "abcdefgh"[i], f"bios/SNK/Neo Geo/rom{i}.bin")
|
|
for i in range(8)]
|
|
consumers = _consumers({f["sha1"]: [("emu", {"required": True})] for f in files})
|
|
self.assertEqual(gs._start_with(files, consumers, first_below=False)[0],
|
|
"**Start with**: `rom0.bin`, `rom1.bin`, `rom2.bin` and 5 more files.")
|
|
|
|
|
|
class CardsSayWhichReleaseTheSetMatches(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
TMP.mkdir(exist_ok=True)
|
|
self.path = TMP / "r3_systems_set.zip"
|
|
with zipfile.ZipFile(self.path, "w") as archive:
|
|
archive.writestr("sp-s2.sp1", b"bios")
|
|
crc = f"{zipfile.ZipFile(self.path).infolist()[0].CRC:08x}"
|
|
self.releases = {"set.zip": [
|
|
((("sp-s2.sp1", crc),), "mame", [(0, "0.190"), (1, "0.195"), (4, "0.220")]),
|
|
((("sp-s2.sp1", crc),), "fbneo", [(2, "1.0.0.03"), (7, "1.0.0.03")]),
|
|
((("other.bin", "00000000"),), "mame", [(9, "0.289")]),
|
|
]}
|
|
|
|
def tearDown(self) -> None:
|
|
self.path.unlink(missing_ok=True)
|
|
|
|
def card(self) -> str:
|
|
f = {"name": "set.zip", "sha1": "e" * 40, "md5": "e" * 32, "size": 4,
|
|
"path": str(self.path)}
|
|
with mock.patch.object(gs, "_romset_releases", return_value=self.releases):
|
|
return "\n".join(gs._system_file_card(f, _consumers({})))
|
|
|
|
def test_the_matching_releases_as_runs(self) -> None:
|
|
self.assertIn("<dt>Romset</dt><dd>MAME 0.190 to 0.195, 0.220, FBNeo 1.0.0.03</dd>",
|
|
self.card())
|
|
|
|
def test_no_match_names_what_was_compared(self) -> None:
|
|
self.releases["set.zip"] = self.releases["set.zip"][2:]
|
|
with mock.patch.object(gs, "_romset_coverage", return_value="MAME 0.190 to 0.289"):
|
|
self.assertIn("<dd>No match in MAME 0.190 to 0.289</dd>", self.card())
|
|
|
|
def test_no_recipe_no_line(self) -> None:
|
|
self.releases = {}
|
|
self.assertNotIn("Romset", self.card())
|
|
|
|
def test_the_snapshots_cover_mame_and_fbneo(self) -> None:
|
|
coverage = gs._romset_coverage()
|
|
self.assertRegex(coverage, r"^MAME 0\.\d+ to 0\.\d+ or FBNeo [\d.]+$")
|
|
|
|
def test_a_page_of_sets_links_the_version_answer(self) -> None:
|
|
f = {"name": "set.zip", "sha1": "e" * 40, "path": "bios/SNK/Neo Geo/set.zip"}
|
|
with mock.patch.object(gs, "_romset_releases", return_value=self.releases):
|
|
page = gs.generate_system_page("SNK", {"Neo Geo": [f]}, _consumers({}))
|
|
self.assertIn("MAME and FBNeo sets must match the emulator's version. [Which version "
|
|
f"each core reads]({gs.ROMSET_FAQ}).", page)
|
|
anchor = gs.ROMSET_FAQ.split("#")[1]
|
|
faq = (REPO_ROOT / "wiki" / "technical-faq.md").read_text(encoding="utf-8")
|
|
self.assertIn("## Which MAME version do the arcade BIOS sets match?", faq)
|
|
self.assertEqual(anchor, "which-mame-version-do-the-arcade-bios-sets-match")
|
|
|
|
|
|
class ArcadeIsItsOwnGroup(unittest.TestCase):
|
|
def test_arcade_rows_under_arcade(self) -> None:
|
|
f = {"name": "a.bin", "sha1": "a" * 40, "path": "bios/x/a.bin"}
|
|
index = gs.generate_systems_index({"Arcade": {"MAME": [f]}, "Sony": {"PlayStation": [f]}})
|
|
arcade = index.index("## Arcade")
|
|
self.assertLess(index.index("## Consoles and computers"), arcade)
|
|
self.assertGreater(index.index("[MAME]"), arcade)
|
|
self.assertLess(index.index("[Sony PlayStation]"), arcade)
|
|
|
|
|
|
class EachManufacturerHasItsCard(unittest.TestCase):
|
|
def test_a_tagline_holds_three_lines_and_counts_the_rest(self) -> None:
|
|
names = [f"Console number {n}" for n in range(40)]
|
|
text = make_og_cards.consoles_tagline(names)
|
|
self.assertRegex(text, r" and \d+ more$")
|
|
lines = make_og_cards.wrap(text, make_og_cards.TAGLINE_WIDTH, make_og_cards.TAGLINE_SIZE)
|
|
self.assertLessEqual(len(lines), make_og_cards.TAGLINE_LINES)
|
|
self.assertEqual(make_og_cards.consoles_tagline(["Saturn", "Dreamcast"]),
|
|
"BIOS files for Dreamcast and Saturn")
|
|
|
|
def test_every_manufacturer_page_has_a_committed_card(self) -> None:
|
|
cards = make_og_cards.system_cards()
|
|
missing = [key for key in cards
|
|
if not (REPO_ROOT / "docs_assets" / "og" / f"{key}.png").is_file()]
|
|
self.assertEqual(missing, [], "run python scripts/make_og_cards.py --only systems")
|
|
|
|
|
|
@unittest.skipUnless((DIST / "systems" / "nintendo" / "index.html").is_file(), "site not built")
|
|
class TheBuiltPages(unittest.TestCase):
|
|
def test_no_console_heading_carries_an_icon(self) -> None:
|
|
with_icon = [path.relative_to(DIST).as_posix() for path in DIST.glob("systems/**/index.html")
|
|
if re.search(r"<h2[^>]*>(?:(?!</h2>).)*<img", path.read_text(encoding="utf-8"), re.S)]
|
|
self.assertEqual(with_icon, [])
|
|
|
|
def test_the_ds_section_names_no_dsi_file(self) -> None:
|
|
page = (DIST / "systems" / "nintendo" / "index.html").read_text(encoding="utf-8")
|
|
start = page.index('id="nintendo-ds"')
|
|
section = page[start:page.index('id="nintendo-dsi"')]
|
|
opening = section[:section.index('class="rb-sys-file"')]
|
|
self.assertRegex(opening, r"No file needed|Start with")
|
|
self.assertNotRegex(opening.casefold(), r"dsi_|bios7i|bios9i")
|
|
|
|
def test_the_gba_line_prints_the_setups_name(self) -> None:
|
|
page = (DIST / "systems" / "nintendo" / "index.html").read_text(encoding="utf-8")
|
|
section = page[page.index('id="game-boy-advance"'):page.index('id="game-boy-color"')]
|
|
self.assertRegex(section[:section.index("class=\"rb-sys-file\"")], r"<code[^>]*>gba_bios.bin</code>")
|
|
|
|
def test_the_neo_geo_section_opens_on_the_neo_geo_bios(self) -> None:
|
|
page = (DIST / "systems" / "snk" / "index.html").read_text(encoding="utf-8")
|
|
section = page[page.index('id="neo-geo"'):]
|
|
opening = section[:section.index('class="rb-sys-file"')]
|
|
self.assertRegex(opening, r"<code[^>]*>neogeo.zip</code>")
|
|
self.assertIn("Which version each core reads", page)
|
|
|
|
def test_the_nintendo_summary_holds_its_budget(self) -> None:
|
|
size = (DIST / "systems" / "nintendo" / "index.html").stat().st_size
|
|
self.assertLess(size, 540_000)
|
|
|
|
|
|
class InstallBoxesCountTowardTheSummary(unittest.TestCase):
|
|
"""Ten install boxes were 295 KB of the 583 KB Nintendo page: the
|
|
weights that keep a section on its maker's summary counted cards only,
|
|
and the page passed its 600 KB budget once the DS files were refiled."""
|
|
|
|
def setUp(self) -> None:
|
|
import siterender
|
|
|
|
siterender.reset_console_pages()
|
|
self.addCleanup(siterender.reset_console_pages)
|
|
self.consoles = {
|
|
name: [_file(f"{name}{i}.bin", "0123456789"[index * 3 + i],
|
|
f"bios/Maker/{name}/{name}{i}.bin") for i in range(3)]
|
|
for index, name in enumerate(("Boxed", "Heavy", "Light"))
|
|
}
|
|
|
|
def pages(self, box: int) -> dict:
|
|
def install(console: str, _declared: list) -> list[str]:
|
|
return ["x" * box] if console == "Boxed" else []
|
|
|
|
with mock.patch.object(gs, "_console_install_block", side_effect=install), \
|
|
mock.patch.object(gs, "SYSTEM_SUMMARY_HTML", 50_000):
|
|
return gs.system_pages("Maker", self.consoles, _consumers({}), budget=30_000)
|
|
|
|
def test_a_section_with_a_heavy_install_box_moves(self) -> None:
|
|
pages = self.pages(20_000)
|
|
self.assertIn("maker-boxed", pages)
|
|
self.assertNotIn("maker-light", pages)
|
|
|
|
def test_a_light_install_box_stays(self) -> None:
|
|
self.assertEqual(list(self.pages(10)), ["maker"])
|