"""System pages and file cards (round 2, package 6).
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.
"""
from __future__ import annotations
import re
import sys
import types
import unittest
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 siterender # noqa: E402
from cross_reference import FileConsumers # noqa: E402
DIST = REPO_ROOT / "website" / "dist"
def _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 _consumers(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 = [
_file("LibPS.exe", "1"), _file("PSX_DTLH2000.bin", "2"),
_file("dtlh1100.bin", "3"), _file("scph5501.bin", "4"),
_file("scph5500.bin", "5"), _file("scph1001.bin", "6"),
_file("openbios.bin", "7"),
]
self.consumers = _consumers({
"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"(.*?)", 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"(.*?)", 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 _consumers({
"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 your games:")
self.assertIn("- **North America**: `scph5501.bin`", lines)
self.assertIn("- **Japan**: `scph5500.bin`", lines)
self.assertLess(lines.index("- **North America**: `scph5501.bin`"),
lines.index("- **Japan**: `scph5500.bin`"))
def test_regions_needing_the_same_file_share_a_line(self):
consumers = _consumers({"4" * 40: [
{"required": True, "region": ["north-america", "latin-america", "brazil"]}]})
lines = gs._start_with([_file("scph5501.bin", "4")], consumers)
# Brazil says nothing Latin America does not.
self.assertIn("- **North America, Latin America**: `scph5501.bin`", lines)
def test_a_long_list_names_the_first_and_counts_the_rest(self):
files = [_file(f"rom{i}.bin", "abcdefgh"[i]) for i in range(8)]
consumers = _consumers({f["sha1"]: [{"required": True}] for f in files})
self.assertEqual(gs._start_with(files, consumers)[0],
"**Start with**: `rom0.bin`, `rom1.bin`, `rom2.bin` and 5 more files. "
"They come first below.")
def test_no_required_file_no_line(self):
self.assertEqual(gs._start_with([_file("a.bin", "a")], _consumers({})), [])
def test_the_page_carries_the_line_under_the_console(self):
page = gs.generate_system_page("Sony", {"PlayStation": self.files}, self.required_only())
heading = page.index("## PlayStation")
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(_file("scph5501.bin", "4"), FileConsumers({"files": {}}))
title = lines[2]
link = re.search(r']*>(.*?)', 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('', link[1])
def test_the_page_draws_the_icon_once(self):
page = gs.generate_system_page(
"Sony", {"PlayStation": [_file("a.bin", "a"), _file("b.bin", "b")]},
FileConsumers({"files": {}}))
self.assertEqual(page.count('On the setup's listscph5501.bin: Batocera", text)
self.assertIn("Packed for its emulators bybios/scph5501.bin: "
"RetroBat", 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 (round 4, V6): the card's title is what the search reads.
lists = [line for line in self.lines if line.startswith("'])
def test_the_page_defines_both_lines(self):
page = gs.generate_system_page("Sony", {"PlayStation": [_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(_file("GameIndex.yaml", "9"), FileConsumers({"files": {}}))[2]
self.assertIn(">No reader", 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 = {**_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": {}}))
facts = [line for line in lines if line.startswith("- ")]
self.assertTrue(facts)
for line in facts:
self.assertEqual(line.count("
- "), 1, line)
self.assertEqual(line.count("
- "), 1, line)
text = "\n".join(lines)
self.assertIn('
- SHA1
- ' + "4" * 40 + "
", text)
self.assertNotIn("", text)
catalogued = next(line for line in facts if line.startswith("- Catalogued"))
self.assertIn("
", catalogued)
self.assertNotRegex(text, r"(?m)^- ")
def test_collection_path_keeps_its_chip(self):
text = "\n".join(gs._system_file_card(_file("a.bin", "a"), FileConsumers({"files": {}})))
self.assertIn("- Collection path
bios/Sony/PlayStation/a.bin", text)
class ConsoleHeadingsStartWithTheName(unittest.TestCase):
"""The icon and a space came before the name: "Section titled
“ PlayStation”" on five of seven Sony headings. Round 3 removed the
icon 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):
groups = {
"Sony": {"PlayStation": [_file("a.bin", "a")], "PlayStation 2": [_file("b.bin", "b")]},
"Other": {"ScummVM": [_file("c.bin", "c")]},
"Game Engines": {"TheXTech": [_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": [_file("a.bin", "a")]},
"Atari": {"Lynx": [_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('