mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
The PlayStation section opened on psxonpsp660.bin under 'every region': since sets carry the requirement, ARMSX1's region-free member of its required BIOS set counts as required, and the world line came first, above the scph files most emulators read, and its card first. A file read for every region is a stand-in for each region's BIOS, so that line now follows the territories. The Apple II start now leads on BizHawk's and Mednafen's files, a tie of two readers with CLK's and POM2's apple2e.rom. The test named that one file. It now checks that every start file is a link, its intent.
1283 lines
65 KiB
Python
1283 lines
65 KiB
Python
"""System pages, boards and file cards.
|
|
|
|
The Sony page opened on LibPS.exe, its card titles read "scph5501.bin
|
|
permalink" in the site search, the two setup lines of a card were defined
|
|
only in a code comment, and the systems index named no console.
|
|
|
|
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.
|
|
|
|
The Game Boy, NES and SNES sections said "Start with" a file one core of
|
|
sixteen requires, under a home page saying they need none. The romset line
|
|
said no MAME release matched sets MAME loads. The Neo Geo opened on files
|
|
ares alone reads, and RetroArch had no install line. The Arcade page was
|
|
sorted by storage folder: NAOMI, PGM and ZN-1 had no section, and their
|
|
names were plain on every emulator page. Cards named emulators by id, part
|
|
pages had no parent and no name, and four paragraphs came before the first
|
|
console.
|
|
|
|
NAOMI opened on hod2bios.zip, a set 13 of the 19 emulators listing it read
|
|
for one game. Lines read other names for one file as files to get: "Start
|
|
with: fdsbios.zip, disksys.rom". Twelve neogeo.zip were on five pages, the
|
|
SNK ones closed under Variants. The Atom, two MZ computers, a sound chip and
|
|
a toy had board pages, and the NAOMI and Atomiswave sets under the
|
|
Dreamcast folder stayed off their boards' pages. ScummVM's files were on
|
|
three pages, one section named ".variants". Board pages said their name
|
|
three times before the first file.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
|
|
|
|
import re
|
|
import sys
|
|
import types
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
import zipfile
|
|
import json
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
|
|
import generate_site as gs # noqa: E402
|
|
import siterender # noqa: E402
|
|
from cross_reference import FileConsumers # noqa: E402
|
|
import make_og_cards # noqa: E402
|
|
from sitestarlight import toc_slug # noqa: E402
|
|
|
|
|
|
DIST = REPO_ROOT / "website" / "dist"
|
|
|
|
|
|
def _sony_file(name: str, char: str, path: str = "") -> dict:
|
|
return {"name": name, "sha1": char * 40, "md5": char * 32, "size": 524288,
|
|
"path": path or f"bios/Sony/PlayStation/{name}"}
|
|
|
|
|
|
def _one_reader_each(readers: dict[str, list[dict]], platforms: dict[str, set[str]] | None = None
|
|
) -> types.SimpleNamespace:
|
|
"""Consumers where each sha1 is read by the given profile entries, one
|
|
emulator per 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 number, entry in enumerate(read_by):
|
|
emulator = f"emu{number}-{sha1[:4]}"
|
|
files_of[emulator] = [entry]
|
|
entries.setdefault(sha1, {})[emulator] = 0
|
|
emulators.setdefault(sha1, set()).add(emulator)
|
|
return types.SimpleNamespace(
|
|
platforms=platforms or {}, emulators=emulators, emulator_entries=entries,
|
|
emulator_files=files_of, installed={}, platform_systems={},
|
|
platform_destinations={},
|
|
)
|
|
|
|
|
|
class TheConsoleBiosComesFirst(unittest.TestCase):
|
|
"""Cards were sorted by byte order: capitals first, so LibPS.exe and
|
|
PSX_DTLH2000.bin opened the PlayStation section and scph5501.bin was
|
|
28th."""
|
|
|
|
def setUp(self):
|
|
self.files = [
|
|
_sony_file("LibPS.exe", "1"), _sony_file("PSX_DTLH2000.bin", "2"),
|
|
_sony_file("dtlh1100.bin", "3"), _sony_file("scph5501.bin", "4"),
|
|
_sony_file("scph5500.bin", "5"), _sony_file("scph1001.bin", "6"),
|
|
_sony_file("openbios.bin", "7"),
|
|
]
|
|
self.consumers = _one_reader_each({
|
|
"4" * 40: [{"name": "scph5501.bin", "required": True, "priority": 2,
|
|
"region": ["north-america"]}],
|
|
"5" * 40: [{"name": "scph5500.bin", "required": True, "priority": 5,
|
|
"region": ["japan"]}],
|
|
"6" * 40: [{"name": "scph1001.bin", "required": False, "priority": 1}],
|
|
"2" * 40: [{"name": "PSX_DTLH2000.bin", "required": False}],
|
|
}, platforms={"6" * 40: {"a", "b"}, "3" * 40: {"a", "b", "c"}})
|
|
|
|
def names(self) -> list[str]:
|
|
cards = gs._console_cards("Sony", self.files, self.consumers, set())
|
|
return [re.search(r"<code>(.*?)</code>", card[2])[1] for _v, card, _s in cards]
|
|
|
|
def test_required_then_priority_then_setups_then_name(self):
|
|
self.assertEqual(self.names(), [
|
|
"scph5501.bin", "scph5500.bin", # required, by priority
|
|
"scph1001.bin", # optional with a priority
|
|
"dtlh1100.bin", # listed by three setups
|
|
"LibPS.exe", "openbios.bin", "PSX_DTLH2000.bin", # by name, any case
|
|
])
|
|
|
|
def test_a_console_cut_in_parts_goes_by_name(self):
|
|
cards = gs._console_cards("Sony", self.files, self.consumers, set(), by_name=True)
|
|
names = [re.search(r"<code>(.*?)</code>", card[2])[1] for _v, card, _s in cards]
|
|
self.assertEqual(names, sorted(names, key=str.casefold))
|
|
|
|
def required_only(self) -> types.SimpleNamespace:
|
|
"""The BIOS readers alone: an emulator reading only an optional
|
|
file runs without the BIOS, and two of four would say so."""
|
|
return _one_reader_each({
|
|
"4" * 40: [{"name": "scph5501.bin", "required": True, "priority": 2,
|
|
"region": ["north-america"]}],
|
|
"5" * 40: [{"name": "scph5500.bin", "required": True, "priority": 5,
|
|
"region": ["japan"]}],
|
|
})
|
|
|
|
def test_the_section_opens_on_the_bios_by_region(self):
|
|
lines = gs._start_with(self.files, self.required_only())
|
|
self.assertEqual(lines[0], "**Start with** the BIOS for the region of the games:")
|
|
self.assertIn("- **North America**: [`scph5501.bin`](#sha1-444444444444)", lines)
|
|
self.assertIn("- **Japan**: [`scph5500.bin`](#sha1-555555555555)", lines)
|
|
self.assertLess(lines.index("- **North America**: [`scph5501.bin`](#sha1-444444444444)"),
|
|
lines.index("- **Japan**: [`scph5500.bin`](#sha1-555555555555)"))
|
|
|
|
def test_regions_needing_the_same_file_share_a_line(self):
|
|
consumers = _one_reader_each({"4" * 40: [
|
|
{"required": True, "region": ["north-america", "latin-america", "brazil"]}]})
|
|
lines = gs._start_with([_sony_file("scph5501.bin", "4")], consumers)
|
|
# Brazil says nothing Latin America does not.
|
|
self.assertIn("- **North America, Latin America**: [`scph5501.bin`](#sha1-444444444444)", lines)
|
|
|
|
def test_a_long_list_names_the_first_and_counts_the_rest(self):
|
|
files = [_sony_file(f"rom{i}.bin", "abcdefgh"[i]) for i in range(8)]
|
|
# One emulator requires all eight: files each required by another
|
|
# emulator are other names for one slot.
|
|
consumers = _one_reader_each({f["sha1"]: [{"required": True}] for f in files})
|
|
consumers.emulator_files = {"emu": [{"required": True}] * len(files)}
|
|
consumers.emulator_entries = {f["sha1"]: {"emu": i} for i, f in enumerate(files)}
|
|
self.assertEqual(gs._start_with(files, consumers)[0],
|
|
"**Start with**: [`rom0.bin`](#sha1-aaaaaaaaaaaa), [`rom1.bin`](#sha1-bbbbbbbbbbbb), "
|
|
"[`rom2.bin`](#sha1-cccccccccccc) and 5 more files. "
|
|
"They come first below.")
|
|
|
|
def test_no_required_file_no_line(self):
|
|
self.assertEqual(gs._start_with([_sony_file("a.bin", "a")], _one_reader_each({})), [])
|
|
|
|
def test_the_page_carries_the_line_under_the_console(self):
|
|
# Sega keeps its consoles as sections. Sony gives each a page.
|
|
page = gs.generate_system_page("Sega", {"Saturn": self.files}, self.required_only())
|
|
heading = page.index("## Saturn")
|
|
self.assertLess(heading, page.index("**Start with**"))
|
|
self.assertLess(page.index("**Start with**"), page.index('class="rb-sys-file"'))
|
|
|
|
|
|
class ThePermalinkIsAnIcon(unittest.TestCase):
|
|
"""The word "permalink" was link text inside every card title: the
|
|
search showed "scph5501.bin permalink" and screen readers read it."""
|
|
|
|
def test_no_word_and_out_of_the_search(self):
|
|
lines = gs._system_file_card(_sony_file("scph5501.bin", "4"), FileConsumers({"files": {}}))
|
|
title = lines[2]
|
|
link = re.search(r'<a class="rb-permalink"[^>]*>(.*?)</a>', title)
|
|
self.assertNotIn("permalink", re.sub(r"<[^>]+>", "", title))
|
|
self.assertIn('aria-label="Permanent link to scph5501.bin"', link[0])
|
|
self.assertIn("data-pagefind-ignore", link[0])
|
|
self.assertIn('<use href="#rb-link-icon"/>', link[1])
|
|
|
|
def test_the_page_draws_the_icon_once(self):
|
|
page = gs.generate_system_page(
|
|
"Sega", {"Saturn": [_sony_file("a.bin", "a"), _sony_file("b.bin", "b")]},
|
|
FileConsumers({"files": {}}))
|
|
self.assertEqual(page.count('<symbol id="rb-link-icon"'), 1)
|
|
self.assertEqual(page.count('href="#rb-link-icon"'), 2)
|
|
|
|
|
|
class TheCardGivesTheSizeInBytes(unittest.TestCase):
|
|
"""The badge rounded to 0.1 KB and no line of a system card gave the
|
|
bytes an emulator compares."""
|
|
|
|
def test_the_badge_is_exact(self):
|
|
for size, shown in ((524288, "512 KB"), (44348015, "44,348,015 B"),
|
|
(2 * 1024 * 1024, "2 MB"), (2304, "2,304 B"), (10, "10 B")):
|
|
with self.subTest(size=size):
|
|
card = {**_sony_file("scph5501.bin", "4"), "size": size}
|
|
title = gs._system_file_card(card, FileConsumers({"files": {}}))[2]
|
|
self.assertIn(f'<span class="rb-badge rb-badge-muted">{shown}</span>', title)
|
|
|
|
|
|
class TheCardLinesSayWhatTheyMean(unittest.TestCase):
|
|
""""Platforms:" and "Also installed by:" listed different setups, as
|
|
lower-case ids, and only a code comment told them apart."""
|
|
|
|
def setUp(self):
|
|
sha1 = "4" * 40
|
|
self.consumers = types.SimpleNamespace(
|
|
platforms={sha1: {"batocera"}}, emulators={},
|
|
platform_systems={sha1: {"batocera": {"sony-playstation"}}},
|
|
platform_destinations={sha1: {"batocera": {"scph5501.bin"}}},
|
|
installed={sha1: {"retrobat": {"bios/scph5501.bin"}}},
|
|
)
|
|
self.lines = gs._system_file_card(_sony_file("scph5501.bin", "4"), self.consumers)
|
|
|
|
def test_labels_and_display_names(self):
|
|
text = "\n".join(self.lines)
|
|
# Grouped by where the setups put the file.
|
|
self.assertIn("<dt>On the setup's list</dt><dd><code>scph5501.bin</code>: <a href=\"../"
|
|
"platforms/batocera.md#sony-playstation\">Batocera</a></dd>", text)
|
|
self.assertIn("<dt>Packed for its emulators by</dt><dd><code>bios/scph5501.bin</code>: "
|
|
"<a href=\"../platforms/retrobat.md\">RetroBat</a></dd>", text)
|
|
self.assertNotIn("Platforms:", text)
|
|
self.assertNotIn("Also installed by", text)
|
|
|
|
def test_the_facts_are_out_of_the_search(self):
|
|
# The setup list made every card's excerpt a run of names and
|
|
# paths: the card's title is what the search reads.
|
|
# The facts shown open are one list, the others one fold, both
|
|
# out of the search.
|
|
shown = self.lines[:next(i for i, line in enumerate(self.lines) if line.startswith("<details"))]
|
|
lists = [line for line in shown if line.startswith("<dl")]
|
|
self.assertEqual(lists, ['<dl class="rb-facts" data-pagefind-ignore>'])
|
|
folds = [line for line in self.lines if line.startswith("<details")]
|
|
self.assertEqual(len(folds), 1)
|
|
self.assertEqual(folds, ["<details data-pagefind-ignore>"])
|
|
|
|
def test_the_page_defines_both_lines(self):
|
|
page = gs.generate_system_page("Sega", {"Saturn": [_sony_file("scph5501.bin", "4")]},
|
|
self.consumers)
|
|
key = page.index("**On the setup's list** names")
|
|
self.assertLess(key, page.index('class="rb-sys-file"'))
|
|
self.assertIn("**Packed for its emulators by** names", page)
|
|
|
|
def test_a_file_nothing_reads_says_so(self):
|
|
title = gs._system_file_card(_sony_file("GameIndex.yaml", "9"), FileConsumers({"files": {}}))[2]
|
|
self.assertIn(">No reader</span>", title)
|
|
self.assertNotIn("No reader", self.lines[2])
|
|
|
|
|
|
class FileFactsAreAGrid(unittest.TestCase):
|
|
"""Each card was two bullet lists of code chips."""
|
|
|
|
def test_one_value_cell_per_label_and_plain_hashes(self):
|
|
entry = {**_sony_file("scph5501.bin", "4"), "crc32": "8d8cb7e4",
|
|
"provenance": {"redump": {"name": "ps-30a.bin"},
|
|
"tosec": {"name": "SCPH-5501 (1996).bin"}}}
|
|
lines = gs._system_file_card(entry, FileConsumers({"files": {}}))
|
|
text = "\n".join(lines)
|
|
# One value cell after each label, in the open list and in the fold.
|
|
facts = re.findall(r"<dt>[^<]*</dt><dd[^>]*>.*?</dd>", text)
|
|
self.assertTrue(facts)
|
|
self.assertEqual(len(facts), text.count("<dt>"))
|
|
self.assertEqual(text.count("<dt>"), len(re.findall(r"<dd[ >]", text)))
|
|
# The hash is the cell's text, styled by the cell: no element around it.
|
|
self.assertIn('<dt>SHA1</dt><dd class="rb-hash">' + "4" * 40 + "</dd>", text)
|
|
self.assertNotIn("<code class=\"rb-hash\">", text)
|
|
self.assertNotIn('<span class="rb-hash">', text)
|
|
catalogued = next(fact for fact in facts if fact.startswith("<dt>Catalogued"))
|
|
self.assertIn("<br>", catalogued)
|
|
self.assertNotRegex(text, r"(?m)^- ")
|
|
|
|
def test_collection_path_keeps_its_chip(self):
|
|
text = "\n".join(gs._system_file_card(_sony_file("a.bin", "a"), FileConsumers({"files": {}})))
|
|
self.assertIn("<dt>Collection path</dt><dd><code>bios/Sony/PlayStation/a.bin</code>", text)
|
|
|
|
|
|
class ConsoleHeadingsStartWithTheName(unittest.TestCase):
|
|
"""The icon and a space came before the name: "Section titled
|
|
“ PlayStation”" on five of seven Sony headings. The icon is gone
|
|
altogether."""
|
|
|
|
def test_no_icon_even_when_one_exists(self):
|
|
icon = siterender._icon_name("Sony", "PlayStation")
|
|
with mock.patch.dict(siterender._icon_available, {icon: True}):
|
|
self.assertEqual(gs._console_heading("Sony", "PlayStation"),
|
|
"## PlayStation { #playstation }")
|
|
|
|
def test_no_icon_no_image(self):
|
|
icon = siterender._icon_name("Nobody", "Nothing")
|
|
with mock.patch.dict(siterender._icon_available, {icon: False}):
|
|
self.assertEqual(gs._console_heading("Nobody", "Nothing"), "## Nothing { #nothing }")
|
|
|
|
|
|
class TheSystemsIndexListsConsoles(unittest.TestCase):
|
|
"""The index listed 74 manufacturers and no console: PlayStation had to
|
|
be known to be under Sony."""
|
|
|
|
def test_one_row_per_console_by_its_name(self):
|
|
gs.reset_console_pages()
|
|
self.addCleanup(gs.reset_console_pages)
|
|
groups = {
|
|
"Sony": {"PlayStation": [_sony_file("a.bin", "a")], "PlayStation 2": [_sony_file("b.bin", "b")]},
|
|
"Other": {"ScummVM": [_sony_file("c.bin", "c")]},
|
|
"Game Engines": {"TheXTech": [_sony_file("d.png", "d")]},
|
|
}
|
|
page = gs.generate_systems_index(groups)
|
|
self.assertIn("| [Sony PlayStation](sony.md#playstation) | 1 | [Sony](sony.md) |", page)
|
|
self.assertIn("| [Sony PlayStation 2](sony.md#playstation-2) | 1 | [Sony](sony.md) |",
|
|
page)
|
|
# A program's folder under Other is named alone, in its own table.
|
|
consoles = page.split("## Emulator folders")[0]
|
|
self.assertNotIn("ScummVM", consoles)
|
|
self.assertIn("| [ScummVM](../emulators/scummvm.md) | [1 file](other.md#scummvm) |",
|
|
page.split("## Emulator folders")[1])
|
|
self.assertNotIn("TheXTech", consoles)
|
|
self.assertIn("| [TheXTech](game-engines.md#thextech)", page)
|
|
|
|
def test_rows_go_by_name_not_by_maker(self):
|
|
groups = {"Sega": {"Saturn": [_sony_file("a.bin", "a")]},
|
|
"Atari": {"Lynx": [_sony_file("b.bin", "b")]}}
|
|
page = gs.generate_systems_index(groups)
|
|
self.assertLess(page.index("Atari Lynx"), page.index("Sega Saturn"))
|
|
|
|
|
|
class FindAFileSaysWhatAHashIs(unittest.TestCase):
|
|
|
|
def test_the_sentence_above_the_box(self):
|
|
page = gs.generate_files_page()
|
|
intro = page.split('<form class="rb-file-lookup"')[0]
|
|
self.assertIn("A hash is a fingerprint of a file's contents", intro)
|
|
self.assertIn("(../wiki/glossary.md)", intro)
|
|
self.assertNotIn("**permalink**", page)
|
|
|
|
|
|
@unittest.skipUnless((DIST / "systems" / "sony" / "index.html").is_file(), "site not built")
|
|
class TheBuiltSonyPage(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.text = (DIST / "systems" / "sony" / "index.html").read_text(encoding="utf-8")
|
|
|
|
def test_no_card_title_says_permalink(self):
|
|
# Every Sony console has a page of its own: the cards are there.
|
|
own = (DIST / "systems" / "sony-playstation" / "index.html").read_text(encoding="utf-8")
|
|
titles = re.findall(r'<h3[^>]*class="rb-file-title".*?</h3>', own, re.S)
|
|
self.assertTrue(titles)
|
|
for title in titles:
|
|
self.assertNotIn("permalink", re.sub(r"<[^>]+>", "", title))
|
|
|
|
def test_playstation_opens_on_a_bios_it_needs(self):
|
|
section = self.text.split('id="playstation"', 1)[1].split("<h2", 1)[0]
|
|
# The opening line follows the profiles: "Start with" when most
|
|
# emulators require the BIOS, "No file needed with ... Only ...
|
|
# require" when most run without it. Either way it names a BIOS and
|
|
# links it, before the first card.
|
|
opening = section.split('class="rb-sys-file"', 1)[0]
|
|
self.assertRegex(opening, r'<a href="[^"]+"><code[^>]*>scph\d+\.bin</code></a>')
|
|
# PlayStation has a page of its own, and its files open there.
|
|
own = (DIST / "systems" / "sony-playstation" / "index.html").read_text(encoding="utf-8")
|
|
self.assertIn('href="/retrobios/systems/sony-playstation/"', section)
|
|
first = re.search(r'class="rb-file-title"[^>]*><code[^>]*>(.*?)</code>', own)[1]
|
|
self.assertNotIn(first, {"LibPS.exe", "PSX_DTLH2000.bin"})
|
|
self.assertRegex(first, r"^scph\d+")
|
|
|
|
def test_headings_have_no_leading_space(self):
|
|
self.assertNotRegex(self.text, r'<h2 id="playstation[^"]*"><img[^>]*>\s')
|
|
|
|
|
|
TMP = REPO_ROOT / "tmp"
|
|
|
|
|
|
def _file_at(name: str, char: str, path: str) -> dict:
|
|
return {"name": name, "sha1": char * 40, "md5": char * 32, "size": 4096, "path": path}
|
|
|
|
|
|
def _consumers_with_db(
|
|
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_at("NDS_Bios7.bin", "7", f"{folder}/NDS_Bios7.bin"),
|
|
_file_at("NDS_Bios9.bin", "9", f"{folder}/NDS_Bios9.bin"),
|
|
_file_at("dsi_bios7.bin", "d", f"{folder}/dsi_bios7.bin")]
|
|
hle = {"required": False, "hle_fallback": True, "system": "nintendo-ds"}
|
|
self.consumers = _consumers_with_db({
|
|
"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`](#sha1-777777777777), [`bios9.bin`](#sha1-999999999999).", ""])
|
|
|
|
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`](#sha1-dddddddddddd).", ""])
|
|
|
|
def test_more_than_three_emulators_are_counted(self) -> None:
|
|
hle = {"required": False, "hle_fallback": True, "system": "nintendo-ds"}
|
|
consumers = _consumers_with_db({"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`](#sha1-777777777777).")
|
|
|
|
def test_a_shipped_file_is_no_start(self) -> None:
|
|
consumers = _consumers_with_db({"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_at("GBA_bios.rom", "a", "bios/Nintendo/Game Boy Advance/GBA_bios.rom")
|
|
consumers = _consumers_with_db(
|
|
{"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`](#sha1-aaaaaaaaaaaa).", ""])
|
|
|
|
def test_no_setup_then_the_emulators_name(self) -> None:
|
|
f = _file_at("x.rom", "b", "bios/Nintendo/Game Boy Advance/x.rom")
|
|
consumers = _consumers_with_db({"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_at("aes.zip", "1", "bios/SNK/Neo Geo/aes.zip")
|
|
neogeo = _file_at("neogeo.zip", "2", "bios/Arcade/Arcade/neogeo.zip")
|
|
mega_cd = _file_at("bios_CD_U.bin", "3", "bios/Sega/Mega CD/bios_CD_U.bin")
|
|
shader = _file_at("tfx.glsl", "4", "bios/Other/emu/tfx.glsl")
|
|
mvs = {"required": True, "system": "snk-neogeo-mvs"}
|
|
self.consumers = _consumers_with_db({
|
|
"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_the_file_most_emulators_read_leads_and_the_arcade_set_is_linked(self) -> None:
|
|
lines = gs._start_with([self.aes], self.consumers, section=("SNK", "Neo Geo"))
|
|
link = f"[`neogeo.zip`](/retrobios/files/?sha1={'2' * 40})"
|
|
self.assertEqual(lines[0], f"**Start with** {link}. Keep it zipped. "
|
|
"mame0, mame1 and mame2 read it.")
|
|
self.assertIn("Geolith reads [`aes.zip`](#sha1-111111111111) instead.", 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_at("aes.zip", "1", "bios/SNK/Neo Geo/aes.zip")
|
|
many = _file_at("sp-s3.sp1", "5", "bios/SNK/Neo Geo/sp-s3.sp1")
|
|
consumers = _consumers_with_db({
|
|
"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_at(f"rom{i}.bin", "abcdefgh"[i], f"bios/SNK/Neo Geo/rom{i}.bin")
|
|
for i in range(8)]
|
|
consumers = _consumers_with_db({f["sha1"]: [("emu", {"required": True})] for f in files})
|
|
self.assertEqual(gs._start_with(files, consumers, first_below=False)[0],
|
|
"**Start with**: [`rom0.bin`](#sha1-aaaaaaaaaaaa), [`rom1.bin`](#sha1-bbbbbbbbbbbb), "
|
|
"[`rom2.bin`](#sha1-cccccccccccc) 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_with_db({})))
|
|
|
|
def test_the_matching_releases_as_runs(self) -> None:
|
|
self.assertIn("<dt>Romset</dt><dd>Same contents as MAME 0.190 to 0.195, 0.220 and "
|
|
"FBNeo 1.0.0.03</dd>", self.card())
|
|
|
|
def test_no_match_counts_what_is_missing(self) -> None:
|
|
self.releases["set.zip"] = self.releases["set.zip"][2:]
|
|
self.assertIn("<dd>No match: missing 1 ROM of MAME 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_with_db({}))
|
|
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")
|
|
# A card left behind by a page that went: oric.png outlived its page.
|
|
committed = (REPO_ROOT / "docs_assets" / "og" / "systems").glob("*.png")
|
|
stale = sorted(f"systems/{path.stem}" for path in committed
|
|
if f"systems/{path.stem}" not in cards)
|
|
self.assertEqual(stale, [])
|
|
|
|
|
|
@unittest.skipUnless((DIST / "systems" / "nintendo" / "index.html").is_file(), "site not built")
|
|
class TheBuiltSystemPages(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"')]
|
|
# The section may have moved to a page of its own: no card follows.
|
|
opening = section.split('class="rb-sys-file"')[0]
|
|
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_at(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_with_db({}), 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"])
|
|
|
|
|
|
CONTENT = REPO_ROOT / "website" / "src" / "content" / "docs"
|
|
|
|
|
|
def _unlinked(text: str) -> str:
|
|
"""A start line's wording: each file it names links to its card."""
|
|
return re.sub(r"\[(`[^`]*`)\]\([^)]*\)", r"\1", text)
|
|
|
|
|
|
def _game_boy_file(name: str, char: str, path: str = "") -> dict:
|
|
return {"name": name, "sha1": char * 40, "md5": char * 32, "size": 4096,
|
|
"path": path or f"bios/Nintendo/Game Boy/{name}"}
|
|
|
|
|
|
def _consumers_by_system(readers: dict[str, list[tuple[str, dict]]],
|
|
systems: 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=systems or {},
|
|
platform_destinations={}, _db={"files": {}},
|
|
)
|
|
|
|
|
|
def _built_page(path: Path) -> str:
|
|
if not path.is_file():
|
|
raise unittest.SkipTest(f"{path.relative_to(REPO_ROOT)} not built")
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def _opening(html: str, anchor: str) -> str:
|
|
"""The text of the first paragraph under a heading."""
|
|
start = html.index("<p>", html.index(f'id="{anchor}"'))
|
|
return re.sub(r"<[^>]+>", "", html[start:html.index("</p>", start)]).strip()
|
|
|
|
|
|
class AFileFewRequireIsNoStartFile(unittest.TestCase):
|
|
"""6.1: one requiring emulator decided the line for the console."""
|
|
|
|
def test_one_of_four_says_no_file_needed_and_names_it(self) -> None:
|
|
boot = _game_boy_file("dmg_boot.bin", "1")
|
|
optional = {"required": False, "system": "nintendo-gb"}
|
|
consumers = _consumers_by_system({"1" * 40: [
|
|
("emux_gb", {"name": "dmg_boot.bin", "required": True}),
|
|
("gambatte", optional), ("mgba", optional), ("sameboy", optional)]})
|
|
line = _unlinked(gs._start_with([boot], consumers)[0])
|
|
self.assertTrue(line.startswith("**No file needed** with "), line)
|
|
self.assertRegex(line, r"Only \S.* requires `dmg_boot.bin`\.$")
|
|
|
|
def test_most_requiring_keeps_the_start_line(self) -> None:
|
|
bios = _game_boy_file("scph5501.bin", "2")
|
|
required = {"name": "scph5501.bin", "required": True}
|
|
consumers = _consumers_by_system({"2" * 40: [
|
|
("a", required), ("b", required), ("c", {"required": False, "hle_fallback": True})]})
|
|
self.assertEqual(_unlinked(gs._start_with([bios], consumers)[0]), "**Start with**: `scph5501.bin`.")
|
|
|
|
def test_an_entry_read_for_a_system_with_no_section_is_dropped(self) -> None:
|
|
sample = _game_boy_file("ym2608_adpcm_rom.bin", "3", "bios/Nintendo/NES/ym2608_adpcm_rom.bin")
|
|
consumers = _consumers_by_system({"3" * 40: [
|
|
("fbneo", {"name": "ym2608_adpcm_rom.bin", "required": True,
|
|
"system": "no-such-system-anywhere"})]})
|
|
self.assertEqual(gs._start_with([sample], consumers, section=("Nintendo", "NES")), [])
|
|
|
|
def test_the_home_usually_not_consoles_open_on_no_file_needed(self) -> None:
|
|
home = _built_page(DIST / "index.html")
|
|
match = re.search(r"<strong>Usually not</strong> for ([^<.]+)", home)
|
|
self.assertIsNotNone(match, "the home page no longer says Usually not")
|
|
names = re.split(r", | and ", match[1])
|
|
sections = {"NES": ("nintendo", "nes"), "Super Nintendo": ("nintendo", "snes"),
|
|
"Mega Drive": ("sega", "mega-drive"), "Game Boy": ("nintendo", "game-boy"),
|
|
"Game Boy Advance": ("nintendo", "game-boy-advance")}
|
|
for name in names:
|
|
self.assertIn(name, sections, f"map {name!r} to its section")
|
|
page, anchor = sections[name]
|
|
html = _built_page(DIST / "systems" / page / "index.html")
|
|
self.assertTrue(_opening(html, anchor).startswith("No file needed"), name)
|
|
|
|
|
|
class TheRomsetLineMatchesByCrc(unittest.TestCase):
|
|
"""6.2: a set with an extra BIOS revision read "No match"."""
|
|
|
|
def test_the_main_neogeo_set_names_every_mame_release(self) -> None:
|
|
db = json.loads((REPO_ROOT / "database.json").read_text(encoding="utf-8"))
|
|
main = next((f for sha1, f in db["files"].items() if sha1.startswith("447e2375")), None)
|
|
if main is None or not (REPO_ROOT / main["path"]).is_file():
|
|
self.skipTest("the main neogeo.zip is not in this checkout")
|
|
line = gs._romset_line(main)
|
|
self.assertIn("Holds every ROM of MAME 0.177 to 0.289", line)
|
|
self.assertNotIn("No match", line)
|
|
|
|
|
|
class TheNeoGeoOpensOnItsSet(unittest.TestCase):
|
|
"""6.3: the set 26 emulators read leads, RetroArch gets its command."""
|
|
|
|
def test_the_lead_names_the_exceptions(self) -> None:
|
|
neogeo = _game_boy_file("neogeo.zip", "4", "bios/SNK/Neo Geo/neogeo.zip")
|
|
epo = _game_boy_file("neo-epo.sp1", "5", "bios/SNK/Neo Geo/neo-epo.sp1")
|
|
mvs = {"name": "neogeo.zip", "required": True, "system": "snk-neogeo-mvs"}
|
|
consumers = _consumers_by_system({
|
|
"4" * 40: [(f"mame{n}", mvs) for n in range(4)],
|
|
"5" * 40: [("ares", {"name": "neo-epo.sp1", "required": True,
|
|
"system": "snk-neogeo"})],
|
|
})
|
|
lines = [_unlinked(line) for line in gs._start_with([neogeo, epo], consumers, section=("SNK", "Neo Geo"))]
|
|
self.assertTrue(lines[0].startswith("**Start with** `neogeo.zip`. Keep it zipped. "))
|
|
self.assertIn("ares reads `neo-epo.sp1` instead.", lines)
|
|
cards = gs._console_cards("SNK", [epo, neogeo], consumers, set(),
|
|
start=gs._start_order([neogeo, epo], consumers,
|
|
("SNK", "Neo Geo")))
|
|
self.assertIn("neogeo.zip", cards[0][1][2])
|
|
|
|
def test_a_setup_filing_the_set_elsewhere_gets_its_command(self) -> None:
|
|
consumers = _consumers_by_system({}, systems={"a" * 40: {"retroarch": {"arcade"}}})
|
|
pairs = gs._install_pairs([("batocera", "snk-neogeo")], "a" * 40, consumers)
|
|
self.assertIn(("retroarch", "arcade"), pairs)
|
|
block = "\n".join(gs._console_install_block("Neo Geo", pairs))
|
|
self.assertIn("<li>RetroArch: <code", block)
|
|
self.assertIn("--platform retroarch --system arcade</code>", block)
|
|
|
|
def test_the_built_section(self) -> None:
|
|
html = _built_page(DIST / "systems" / "snk" / "index.html")
|
|
section = html[html.index('id="neo-geo"'):]
|
|
self.assertTrue(_opening(html, "neo-geo").startswith("Start with neogeo.zip"))
|
|
self.assertIn("--platform retroarch --system arcade", re.sub(r"<[^>]+>", "", section))
|
|
|
|
|
|
BOARDS = ("Sega NAOMI", "Sammy Atomiswave", "Capcom ZN-1", "IGS PolyGame Master",
|
|
"Nintendo PlayChoice-10", "SNK Hyper Neo Geo 64")
|
|
|
|
|
|
class EachArcadeBoardHasASection(unittest.TestCase):
|
|
"""6.4: the Arcade page was cut by storage folder."""
|
|
|
|
def test_each_board_heads_its_page(self) -> None:
|
|
# The page's title names the board, no h2 says it again.
|
|
for name in BOARDS:
|
|
slug = toc_slug(name)
|
|
html = _built_page(DIST / "systems" / f"arcade-{slug}" / "index.html")
|
|
self.assertRegex(html, rf"<h1[^>]*>Arcade: {re.escape(name)}</h1>", name)
|
|
self.assertNotRegex(html, rf"<h2[^>]*>{re.escape(name)}<", name)
|
|
|
|
def test_every_board_on_an_emulator_systems_row_is_a_link(self) -> None:
|
|
if not (DIST / "emulators").is_dir():
|
|
self.skipTest("site not built")
|
|
plain = []
|
|
for page in sorted((DIST / "emulators").glob("*/index.html")):
|
|
row = re.search(r"<td>Systems</td>\s*<td>(.*?)</td>", page.read_text(encoding="utf-8"),
|
|
re.S)
|
|
if not row:
|
|
continue
|
|
text = re.sub(r"<a [^>]*>.*?</a>", "", row[1])
|
|
plain += [(page.parent.name, name) for name in BOARDS if name in text]
|
|
self.assertEqual(plain, [])
|
|
|
|
def test_the_arcade_page_lists_the_boards_first(self) -> None:
|
|
md = _built_page(CONTENT / "systems" / "arcade.md")
|
|
headings = re.findall(r"^## (.+?) \{", md, re.M)
|
|
self.assertIn("Sega NAOMI", headings)
|
|
self.assertLess(headings.index("Sega NAOMI"), headings.index(gs.OTHER_ARCADE_LABEL))
|
|
|
|
|
|
class CardsNameEmulatorsByName(unittest.TestCase):
|
|
"""6.6: 19,499 card links read the profile id."""
|
|
|
|
def test_the_display_name_is_the_link_text(self) -> None:
|
|
sha1 = "6" * 40
|
|
consumers = _consumers_by_system({sha1: [("melonds_ds", {"name": "bios7.bin"})]})
|
|
link = gs._emulator_links(sha1, consumers)[0]
|
|
self.assertTrue(link.startswith("[melonDS DS]("), link)
|
|
|
|
def test_no_built_card_link_reads_an_id(self) -> None:
|
|
if not (DIST / "systems").is_dir():
|
|
self.skipTest("site not built")
|
|
profiles = gs._profiles_on_disk()
|
|
bad = set()
|
|
for page in (DIST / "systems").glob("*/index.html"):
|
|
for emulator, text in re.findall(
|
|
r'<a href="/retrobios/emulators/([^/"#]+)/[^"]*">([^<]+)</a>',
|
|
page.read_text(encoding="utf-8")):
|
|
display = str((profiles.get(emulator) or {}).get("emulator") or emulator)
|
|
if text == emulator and display != emulator:
|
|
bad.add(emulator)
|
|
self.assertEqual(sorted(bad), [])
|
|
|
|
|
|
class PartPagesKnowWhereTheyAre(unittest.TestCase):
|
|
def tearDown(self) -> None:
|
|
# system_pages records where consoles went, for every later page.
|
|
gs.reset_console_pages()
|
|
|
|
"""6.7: 128 system pages had no parent, parts were "part 3 of 5"."""
|
|
|
|
def test_every_part_has_a_parent_and_a_name(self) -> None:
|
|
if not (CONTENT / "systems").is_dir():
|
|
self.skipTest("pages not generated")
|
|
orphans, numbered = [], []
|
|
for page in sorted((CONTENT / "systems").glob("*.md*")):
|
|
text = page.read_text(encoding="utf-8")
|
|
# A part is titled "<maker>: <console>" or "<name> files a to b".
|
|
title = re.search(r"^title: (.*)$", text, re.M)
|
|
part = title and (": " in title[1] or re.search(r" files .+ to ", title[1]))
|
|
if part and "\nparent:" not in text.split("\n---\n", 1)[0]:
|
|
orphans.append(page.name)
|
|
if re.search(r"^title: .*part \d+ of \d+", text, re.M | re.I):
|
|
numbered.append(page.name)
|
|
self.assertEqual(orphans, [])
|
|
self.assertEqual(numbered, [])
|
|
|
|
def test_parts_are_even_and_linked_to_their_neighbours(self) -> None:
|
|
files = [_game_boy_file(f"rom{i:02}.bin", "0123456789"[i], f"bios/Sony/PlayStation 2/rom{i:02}.bin")
|
|
for i in range(10)]
|
|
consumers = _consumers_by_system({})
|
|
with gs.markup.using("mkdocs"):
|
|
one = sum(len(line) + 1 for line in gs._system_file_card(files[0], consumers))
|
|
pages = gs.system_pages("Sony", {"PlayStation 2": files}, consumers, budget=one * 7)
|
|
parts = [stem for stem in pages if stem != "sony"]
|
|
self.assertEqual(len(parts), 2)
|
|
counts = [pages[stem][0].count('class="rb-sys-file"') for stem in parts]
|
|
self.assertEqual(counts, [5, 5])
|
|
first = pages[parts[0]][0]
|
|
self.assertIn("parent:\n label: \"Sony\"\n link: \"/systems/sony/\"", first)
|
|
self.assertIn(f"next:\n label: \"PlayStation 2 files rom05.bin to rom09.bin\"\n"
|
|
f" link: \"/systems/{parts[1]}/\"", first)
|
|
self.assertIn("# PlayStation 2 files rom00.bin to rom04.bin", first)
|
|
self.assertIn(f"searchWeight: {gs._part_weight()}", first)
|
|
|
|
|
|
def _consoles(page: str) -> list[str]:
|
|
"""The consoles: front matter of a generated page."""
|
|
front = page.split("\n---\n", 1)[0] if page.startswith("---\n") else ""
|
|
block = re.search(r"^consoles:\n((?: - .*\n?)+)", front + "\n", re.M)
|
|
return [json.loads(line[4:]) for line in block[1].splitlines()] if block else []
|
|
|
|
|
|
class ConsolesFieldNamesMachinesOnce(unittest.TestCase):
|
|
def tearDown(self) -> None:
|
|
# system_pages records where consoles went, for every later page.
|
|
gs.reset_console_pages()
|
|
|
|
"""The consoles search field: only machine sections, and a console
|
|
with a page of its own is that page's, or "dolphin" found Other and
|
|
"playstation bios" found Sony before the PlayStation page."""
|
|
|
|
def test_no_emulator_folder_is_a_console(self) -> None:
|
|
for maker in ("Other", "Game Engines"):
|
|
page = gs.system_pages(maker, {"dolphin": [_game_boy_file("a.bin", "a")]}, _consumers_by_system({}))
|
|
self.assertEqual(_consoles(page[gs._slug(maker)][0]), [], maker)
|
|
|
|
def test_a_console_with_its_own_page_is_left_to_it(self) -> None:
|
|
big = [_game_boy_file(f"r{i:02}.bin", "0123456789"[i], f"bios/Sega/Saturn/r{i:02}.bin")
|
|
for i in range(10)]
|
|
small = [_game_boy_file("pico.bin", "p", "bios/Sega/Pico/pico.bin")]
|
|
consumers = _consumers_by_system({})
|
|
with gs.markup.using("mkdocs"):
|
|
one = sum(len(line) + 1 for line in gs._system_file_card(big[0], consumers))
|
|
# Sega splits by size. Every Sony console has a page whatever its size.
|
|
pages = gs.system_pages("Sega", {"Saturn": big, "Pico": small},
|
|
consumers, budget=one * 12)
|
|
self.assertIn("sega-saturn", pages)
|
|
self.assertEqual(_consoles(pages["sega"][0]), ["Pico"])
|
|
self.assertEqual(_consoles(pages["sega-saturn"][0]), ["Saturn"])
|
|
self.addCleanup(gs.reset_console_pages)
|
|
sony = gs.system_pages("Sony", {"PlayStation": big, "PocketStation": small},
|
|
consumers, budget=one * 12)
|
|
self.assertEqual(_consoles(sony["sony"][0]), [])
|
|
self.assertEqual(_consoles(sony["sony-pocketstation"][0]), ["PocketStation"])
|
|
|
|
|
|
class OneLineBeforeTheFirstConsole(unittest.TestCase):
|
|
"""6.8: four paragraphs, word for word on every maker's page."""
|
|
|
|
def test_the_intro_is_one_line_and_the_legend_closed(self) -> None:
|
|
page = gs.generate_system_page("SNK", {"Neo Geo": [_game_boy_file("a.bin", "a")]}, _consumers_by_system({}))
|
|
body = page.split("# SNK\n", 1)[1]
|
|
before = body[:body.index("## Neo Geo")]
|
|
self.assertTrue(before.lstrip().startswith(
|
|
"Every BIOS file for SNK machines, one card per file.\n"))
|
|
legend = before[before.index("<section data-pagefind-ignore>"):]
|
|
self.assertIn("<summary>How to read a card</summary>", legend)
|
|
self.assertNotIn(" open>", legend)
|
|
self.assertNotIn("JSON", before)
|
|
self.assertIn("as JSON](../api/v1/systems/snk.json)", page.split("## Next steps")[1])
|
|
|
|
|
|
class SetupsGroupedByPlace(unittest.TestCase):
|
|
"""6.9: each setup repeated the name it gives the file."""
|
|
|
|
def test_one_line_per_place(self) -> None:
|
|
places = [("Batocera", "b.md", ["scph5501.bin"]), ("KNULLI", "k.md", ["scph5501.bin"]),
|
|
("RetroDECK", "r.md", ["bios/scph5501.bin"]), ("EmuDeck", "e.md", [])]
|
|
self.assertEqual(gs._places_lines(places), [
|
|
"<code>scph5501.bin</code>: [Batocera](b.md), [KNULLI](k.md)",
|
|
"<code>bios/scph5501.bin</code>: [RetroDECK](r.md)",
|
|
"[EmuDeck](e.md)",
|
|
])
|
|
|
|
|
|
SYSTEMS = CONTENT / "systems"
|
|
|
|
|
|
def _naomi_file(name: str, char: str, path: str = "") -> dict:
|
|
return {"name": name, "sha1": char * 40, "md5": char * 32, "size": 4096,
|
|
"path": path or f"bios/Sega/NAOMI/{name}"}
|
|
|
|
|
|
def _consumers(reads: dict[str, list[tuple[str, dict]]]) -> types.SimpleNamespace:
|
|
"""Consumers where each sha1 is read by (emulator, entry) pairs, an
|
|
emulator's entries kept in one list as a profile keeps them."""
|
|
files_of: dict[str, list] = {}
|
|
entries: dict[str, dict[str, int]] = {}
|
|
emulators: dict[str, set[str]] = {}
|
|
for sha1, read_by in reads.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={}, _db={"files": {}},
|
|
)
|
|
|
|
|
|
def _system_page(stem: str) -> str:
|
|
path = SYSTEMS / f"{stem}.md"
|
|
if not path.is_file():
|
|
raise unittest.SkipTest(f"systems/{stem}.md not generated")
|
|
return _unlinked(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def _start_line(text: str) -> str:
|
|
match = re.search(r"^\*\*(Start with|No file needed)\*\*.*$", text, re.M)
|
|
return match[0] if match else ""
|
|
|
|
|
|
def _database() -> dict:
|
|
path = REPO_ROOT / "database.json"
|
|
if not path.is_file():
|
|
raise unittest.SkipTest("database.json is not built")
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
class AStartFileIsNoOnesExtra(unittest.TestCase):
|
|
"""6.1: a file most of whose readers take beside another one is no
|
|
start file, whoever requires it."""
|
|
|
|
def test_the_board_opens_on_its_bios_alone(self) -> None:
|
|
naomi, hod2 = _naomi_file("naomi.zip", "1"), _naomi_file("hod2bios.zip", "2")
|
|
both = [(f"flycast{n}", {"name": "hod2bios.zip", "required": True}) for n in range(2)]
|
|
extra = [(f"mame{n}", {"name": "hod2bios.zip", "required": False}) for n in range(3)]
|
|
consumers = _consumers({
|
|
"1" * 40: [(e, {"name": "naomi.zip", "required": True}) for e, _ in both + extra],
|
|
"2" * 40: both + extra,
|
|
})
|
|
self.assertEqual([_unlinked(line) for line in gs._start_with([naomi, hod2], consumers)],
|
|
["**Start with**: `naomi.zip`.", ""])
|
|
|
|
def test_the_naomi_page_opens_on_naomi_zip(self) -> None:
|
|
self.assertEqual(_start_line(_system_page("arcade-sega-naomi")), "**Start with**: `naomi.zip`.")
|
|
|
|
def test_a_file_several_require_beside_the_lead_stays(self) -> None:
|
|
bios7, bios9 = _naomi_file("bios7.bin", "3"), _naomi_file("bios9.bin", "4")
|
|
consumers = _consumers({
|
|
"3" * 40: [(e, {"name": "bios7.bin", "required": True}) for e in ("a", "b")],
|
|
"4" * 40: [(e, {"name": "bios9.bin", "required": True}) for e in ("a", "b")],
|
|
})
|
|
self.assertEqual(_unlinked(gs._start_with([bios7, bios9], consumers)[0]),
|
|
"**Start with**: `bios7.bin`, `bios9.bin`.")
|
|
|
|
def test_some_files_each_is_not_every_file_for_one(self) -> None:
|
|
"""The SNES said bsnes and four more require 22 files."""
|
|
files = [_naomi_file(f"chip{n}.rom", str(n)) for n in range(1, 4)]
|
|
reads = {f["sha1"]: [(f"free{k}", {"name": f["name"], "required": False,
|
|
"hle_fallback": True}) for k in range(5)]
|
|
for f in files}
|
|
reads["1" * 40].append(("bsnes", {"name": "chip1.rom", "required": True}))
|
|
reads["2" * 40].append(("jgenesis", {"name": "chip2.rom", "required": True}))
|
|
reads["3" * 40].append(("jgenesis", {"name": "chip3.rom", "required": True}))
|
|
line = gs._start_with(files, _consumers(reads))[0]
|
|
self.assertIn("require some of these files: ", line)
|
|
|
|
|
|
class OtherNamesForOneSlotReadAsAlternatives(unittest.TestCase):
|
|
"""6.2: "Start with: fdsbios.zip, disksys.rom" read as two files."""
|
|
|
|
def test_the_other_name_is_read_instead(self) -> None:
|
|
disk, fds = _naomi_file("disksys.rom", "5"), _naomi_file("fdsbios.zip", "6")
|
|
consumers = _consumers({
|
|
"5" * 40: [(e, {"name": "disksys.rom", "required": True})
|
|
for e in ("mesen", "nestopia", "fceumm")],
|
|
"6" * 40: [("fbneo", {"name": "fdsbios.zip", "required": True})],
|
|
})
|
|
self.assertEqual(_unlinked(gs._start_with([disk, fds], consumers)[0]),
|
|
"**Start with**: `disksys.rom`. FinalBurn Neo reads `fdsbios.zip` instead.")
|
|
|
|
def test_a_card_read_under_another_name_says_both(self) -> None:
|
|
disk, card = _naomi_file("disksys.rom", "5"), _naomi_file("long name (1986).bin", "7")
|
|
consumers = _consumers({
|
|
"5" * 40: [(e, {"name": "disksys.rom", "required": True}) for e in ("a", "b")],
|
|
"7" * 40: [("c", {"name": "short.rom", "required": True})],
|
|
})
|
|
self.assertIn("reads `long name (1986).bin` (as `short.rom`) instead.",
|
|
_unlinked(gs._start_with([disk, card], consumers)[0]))
|
|
|
|
def test_a_regional_line_names_one_file_per_reader(self) -> None:
|
|
mega_cd = _system_page("sega").split("{ #mega-cd }", 1)[1].split("\n## ", 1)[0]
|
|
regions = [line for line in mega_cd.splitlines() if line.startswith("- **")]
|
|
self.assertTrue(regions)
|
|
for line in regions:
|
|
lead = line.split(". ", 1)[0]
|
|
self.assertEqual(len(re.findall(r"`[^`]+`", lead)), 1, line)
|
|
|
|
def test_the_fds_line(self) -> None:
|
|
fds = _system_page("nintendo").split("{ #famicom-disk-system }", 1)[1]
|
|
self.assertRegex(_start_line(fds), r"^\*\*Start with\*\*: `disksys.rom`\. FinalBurn Neo "
|
|
r"reads \[?`fdsbios.zip`.* instead\.$")
|
|
|
|
|
|
class TheNeoGeoSetsSitWithTheNeoGeo(unittest.TestCase):
|
|
"""6.3: every neogeo.zip has its card on SNK's pages, side by side and
|
|
open, and the ares sentence names the card."""
|
|
|
|
def test_every_neogeo_zip_is_on_snk(self) -> None:
|
|
db = _database()
|
|
sha1s = [sha1 for sha1, f in db["files"].items() if f["name"] == "neogeo.zip"]
|
|
held = "".join(path.read_text(encoding="utf-8") for path in SYSTEMS.glob("snk*.md"))
|
|
if not held:
|
|
self.skipTest("SNK pages not generated")
|
|
self.assertEqual([s for s in sha1s if f'id="sha1-{s[:12]}"' not in held], [])
|
|
others = "".join(path.read_text(encoding="utf-8")
|
|
for path in SYSTEMS.glob("arcade*.md"))
|
|
self.assertEqual([s for s in sha1s if f'id="sha1-{s[:12]}"' in others], [])
|
|
|
|
def test_the_sets_are_open_and_together(self) -> None:
|
|
section = _system_page("snk").split("{ #neo-geo }", 1)[1].split("\n## ", 1)[0]
|
|
titles = re.findall(r'class="rb-file-title"[^>]*><code>([^<]+)</code>', section)
|
|
sets = [n for n, name in enumerate(titles) if name == "neogeo.zip"]
|
|
self.assertGreater(len(sets), 1)
|
|
self.assertEqual(sets, list(range(len(sets))))
|
|
variants = section.split("Variants (", 1)[1] if "Variants (" in section else ""
|
|
self.assertNotIn("<code>neogeo.zip</code>", variants)
|
|
|
|
def test_ares_is_told_the_card_it_reads(self) -> None:
|
|
self.assertIn("ares reads `neo-epo.bin` and `sp-45.sp1` instead.", _system_page("snk"))
|
|
|
|
|
|
class ABoardIsAnArcadeMachine(unittest.TestCase):
|
|
"""6.4: computers, a sound chip and a toy had board pages."""
|
|
|
|
def test_no_board_has_a_parent_outside_arcade(self) -> None:
|
|
wrong = sorted(board for board in siterender.arcade_boards()
|
|
if board in siterender.SYSTEM_PARENTS)
|
|
self.assertEqual(wrong, [])
|
|
|
|
def test_no_board_page_for_a_machine_filed_elsewhere(self) -> None:
|
|
if not SYSTEMS.is_dir():
|
|
self.skipTest("pages not generated")
|
|
for stem in ("arcade-acorn-atom", "arcade-sharp-mz-2000", "arcade-sharp-mz-2500",
|
|
"arcade-yamaha-ym2608", "arcade-miuchiz"):
|
|
self.assertFalse((SYSTEMS / f"{stem}.md").exists(), stem)
|
|
|
|
def test_the_computers_sets_are_on_their_makers_page(self) -> None:
|
|
acorn = _system_page("acorn")
|
|
self.assertIn("{ #atom }", acorn)
|
|
self.assertIn("<code>atom_gdos.zip</code>", acorn)
|
|
cards = re.findall(r'class="rb-file-title"[^>]*><code>([^<]+)</code>', _system_page("sharp"))
|
|
self.assertEqual(cards.count("mz2000.zip"), 2)
|
|
|
|
|
|
class TheDreamcastBoardsAreFiledByBoard(unittest.TestCase):
|
|
"""6.5: the NAOMI and Atomiswave sets under the Dreamcast folder."""
|
|
|
|
def test_the_board_sets_left_the_dreamcast(self) -> None:
|
|
sega = _system_page("sega")
|
|
dreamcast = sega.split("{ #dreamcast }", 1)[1].split("\n## ", 1)[0]
|
|
for name in ("naomi.zip", "awbios.zip", "naomi2.zip"):
|
|
self.assertNotIn(f"<code>{name}</code>", dreamcast)
|
|
self.assertIn("(/retrobios/systems/arcade-sega-naomi/)", dreamcast)
|
|
self.assertIn("(/retrobios/systems/arcade-sammy-atomiswave/)", dreamcast)
|
|
|
|
def test_every_naomi_set_is_on_its_board(self) -> None:
|
|
db = _database()
|
|
page = _system_page("arcade-sega-naomi")
|
|
missing = [sha1 for sha1, f in db["files"].items()
|
|
if f["name"] == "naomi.zip" and f'id="sha1-{sha1[:12]}"' not in page]
|
|
self.assertEqual(missing, [])
|
|
|
|
def test_the_dreamcast_arcade_id_leads_to_naomi(self) -> None:
|
|
siterender.reset_console_pages()
|
|
for board in siterender.arcade_boards():
|
|
siterender.record_board_section(board)
|
|
try:
|
|
self.assertEqual(siterender.system_section("sega-dreamcast-arcade"),
|
|
("arcade", "Sega NAOMI"))
|
|
finally:
|
|
siterender.reset_console_pages()
|
|
|
|
|
|
class EnginesGoByEngine(unittest.TestCase):
|
|
"""6.6: no section named after a folder of variants or a file."""
|
|
|
|
def test_no_section_or_row_is_a_folder_or_a_file(self) -> None:
|
|
db = _database()
|
|
names = set(db["indexes"]["by_name"])
|
|
index = _system_page("index")
|
|
rows = re.findall(r"^\| \[([^\]]+)\]\(", index, re.M)
|
|
headings = [h.strip() for path in SYSTEMS.glob("*.md")
|
|
for h in re.findall(r"^## ([^{\n]+)", path.read_text(encoding="utf-8"), re.M)]
|
|
wrong = sorted({n for n in rows + headings if n.startswith(".") or n in names})
|
|
self.assertEqual(wrong, [])
|
|
|
|
def test_scummvm_has_one_page(self) -> None:
|
|
self.assertFalse((SYSTEMS / "other-scummvm.md").exists())
|
|
self.assertIn("## ScummVM {", _system_page("scummvm"))
|
|
self.assertNotIn("](/retrobios/systems/other/#scummvm)", _system_page("index"))
|
|
|
|
def test_a_folder_named_by_a_profile_key_reads_the_engine(self) -> None:
|
|
index = _system_page("index")
|
|
self.assertIn("| [EasyRPG Player](/retrobios/systems/rpg-maker/#easyrpg) |", index)
|
|
self.assertIn("| [SDLPAL](", index)
|
|
|
|
def test_moved_pages_redirect(self) -> None:
|
|
if not SYSTEMS.is_dir():
|
|
self.skipTest("pages not generated")
|
|
for old, target in gs.SYSTEM_PAGES_MOVED.items():
|
|
new = target.partition("#")[0]
|
|
self.assertFalse((CONTENT / old).exists() or (CONTENT / old).with_suffix(".mdx").exists(), old)
|
|
# A page with tabs is written as MDX.
|
|
self.assertTrue((CONTENT / new).exists() or (CONTENT / new).with_suffix(".mdx").exists(), new)
|
|
redirects = CONTENT.parents[1] / "generated" / "redirects.json"
|
|
if redirects.is_file():
|
|
table = json.loads(redirects.read_text(encoding="utf-8"))
|
|
self.assertEqual(table.get("/systems/other-scummvm/"), "/systems/scummvm/")
|
|
self.assertEqual(table.get("/systems/oric/"), "/systems/tangerine/#oric")
|
|
|
|
|
|
class OneLabelOnceOnABoardPage(unittest.TestCase):
|
|
"""6.7, 6.8, 6.9, 6.10: the words a board or part page repeats."""
|
|
|
|
def test_a_label_naming_files_installs_these_files(self) -> None:
|
|
block = "\n".join(gs._console_install_block("Other arcade files", [("mame", "arcade")]))
|
|
self.assertIn("<summary>Install these files</summary>", block)
|
|
block = "\n".join(gs._console_install_block("Sega NAOMI", [("mame", "arcade")]))
|
|
self.assertIn("<summary>Install the files of Sega NAOMI</summary>", block)
|
|
|
|
def test_a_board_page_has_no_heading_of_its_name(self) -> None:
|
|
page = _system_page("arcade-sega-naomi")
|
|
self.assertNotRegex(page, r"^## Sega NAOMI", )
|
|
self.assertNotIn("](/retrobios/systems/arcade-sega-naomi/#sega-naomi)",
|
|
_system_page("index"))
|
|
|
|
def test_no_part_says_where_it_sits(self) -> None:
|
|
if not SYSTEMS.is_dir():
|
|
self.skipTest("pages not generated")
|
|
said = sorted(p.name for p in SYSTEMS.glob("*.md")
|
|
if "\nPart of [" in p.read_text(encoding="utf-8"))
|
|
self.assertEqual(said, [])
|
|
|
|
def test_a_part_goes_by_the_emulators_name(self) -> None:
|
|
titles = [re.search(r"^title: (.*)$", p.read_text(encoding="utf-8"), re.M)[1].strip("'")
|
|
for p in SYSTEMS.glob("other-mame*.md")]
|
|
if not titles:
|
|
self.skipTest("pages not generated")
|
|
self.assertEqual([t for t in titles if not t.startswith(("MAME files", "Other: MAME"))],
|
|
[])
|
|
|
|
|
|
class EachFileDownloadsFromItsCard(unittest.TestCase):
|
|
"""The Apple II page named eight start files and gave no visible way to
|
|
get one: the download was a "(download)" after the collection path,
|
|
fourth of a card's facts."""
|
|
|
|
def test_the_card_title_holds_the_download(self) -> None:
|
|
from cross_reference import FileConsumers # noqa: PLC0415
|
|
|
|
f = _naomi_file("apple2-asoft-auto.rom", "3", "bios/Apple/Apple II/apple2-asoft-auto.rom")
|
|
title = next(line for line in gs._system_file_card(f, FileConsumers({"files": {}}))
|
|
if "rb-file-title" in line)
|
|
self.assertIn('class="rb-download" href="https://raw.githubusercontent.com/Abdess/'
|
|
'retrobios/main/bios/Apple/Apple%20II/apple2-asoft-auto.rom"', title)
|
|
self.assertIn('aria-label="Download apple2-asoft-auto.rom"', title)
|
|
facts = "\n".join(gs._system_file_card(f, FileConsumers({"files": {}})))
|
|
self.assertNotIn("(download)", facts)
|
|
|
|
def test_a_start_name_leads_to_its_card(self) -> None:
|
|
line = gs._link_held_names("- **Apple II**: `a.rom`, `b.rom`.",
|
|
{"a.rom": "a" * 40, "b.rom": "b" * 40}, {"a" * 40})
|
|
self.assertEqual(line, "- **Apple II**: [`a.rom`](#sha1-aaaaaaaaaaaa), `b.rom`.")
|
|
|
|
def test_a_card_on_another_part_is_reached_by_its_permalink(self) -> None:
|
|
full = next(iter(json.loads((REPO_ROOT / "database.json").read_text())["files"]))
|
|
here = f'[`x.rom`](#sha1-{full[:12]})'
|
|
away = gs._cards_elsewhere(here)
|
|
self.assertEqual(away, f"[`x.rom`](/retrobios/files/?sha1={full})")
|
|
held = here + f'\n<h3 id="sha1-{full[:12]}" class="rb-file-title">'
|
|
self.assertEqual(gs._cards_elsewhere(held), held)
|
|
|
|
def test_every_start_name_on_the_built_apple_page_links(self) -> None:
|
|
page = next(SYSTEMS.glob("apple.md*"), None)
|
|
if page is None:
|
|
self.skipTest("site not generated")
|
|
section = page.read_text(encoding="utf-8").split("## Apple II {", 1)[1]
|
|
start = section.split("<section", 1)[0]
|
|
line = next(row for row in start.splitlines() if row.startswith("- **Apple II**: "))
|
|
# The files to start with come before the first sentence ("MAME MESS
|
|
# reads ... instead"), which names what other emulators read.
|
|
named = line.removeprefix("- **Apple II**: ").split(". ", 1)[0].rstrip(".")
|
|
names = [part.strip() for part in named.split(", ")]
|
|
self.assertTrue(names)
|
|
for name in names:
|
|
self.assertRegex(name, r"^\[`[^`]+`\]\(#sha1-[0-9a-f]{12}\)$")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|