"""What the README, the site and the wiki tell an end user. Someone with a game that does not boot runs the one-line installer, not a clone of this repository: `scripts/verify.py` reads the collection, never their BIOS folder. A download link names the file to fetch, parts included, from the release the table describes. Output shown on a page is output the tool prints, and a size is the one the release record holds. """ from __future__ import annotations import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache import ast import contextlib import re import sys import unittest from functools import cache from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO_ROOT / "scripts")) import release_record # noqa: E402 from generate_readme import ( # noqa: E402 SITE_URL, asset_links, contributing_text, download_links, download_table, format_size, ) RECORD = release_record.load_record(REPO_ROOT / "release.json") REPO = "https://github.com/Abdess/retrobios" SPLIT_RECORD = { "tag": "v2000.01.01", "packs": { "Batocera_1_BIOS_Pack.zip": { "files": 3, "extracted_size": 3 * 1024**3, "download_size": 2 * 1024**3, "assets": [ "Batocera_1_BIOS_Pack.part1of2.zip", "Batocera_1_BIOS_Pack.part2of2.zip", ], "platforms": ["batocera"], }, "RomM_1_BIOS_Pack.zip": { "files": 1, "extracted_size": 1024**2, "download_size": 1024**2, "assets": ["RomM_1_BIOS_Pack.zip"], "platforms": ["romm"], }, }, } def _read(relative: str) -> str: return (REPO_ROOT / relative).read_text(encoding="utf-8") def _section(text: str, heading: str) -> str: """The text under a markdown heading, up to the next heading of its level.""" level = len(heading) - len(heading.lstrip("#")) start = text.index(heading + "\n") rest = text[start + len(heading) + 1:] stop = re.search(rf"^#{{1,{level}}} ", rest, re.MULTILINE) return rest[: stop.start()] if stop else rest def _which_pack() -> str: import generate_site with contextlib.chdir(REPO_ROOT): return generate_site.generate_which_pack() class TheDownloadTablesFollowTheRegistry(unittest.TestCase): """EmuDeck has a Windows edition the installer detects, and the Windows table of the download page left it out. The page is grouped by device now: the row of each platform names every system it is detected on.""" def test_every_detected_system_lists_the_platform(self): import yaml # noqa: PLC0415 from common import load_platform_config # noqa: PLC0415 registry = yaml.safe_load(_read("platforms/_registry.yml")) page = _which_pack() for os_name, label in (("windows", "Windows"), ("darwin", "macOS")): for platform, entry in registry["platforms"].items(): systems = {rule["os"] for rule in (entry.get("install") or {}).get("detect", [])} if os_name in systems: name = load_platform_config(platform, "platforms")["platform"] row = next(line for line in page.splitlines() if line.startswith(f"| [{name}](")) with self.subTest(os=os_name, platform=platform): self.assertIn(label, row.split(" | ")[1]) class AssetLinksNameTheFile(unittest.TestCase): """The get/ page of a platform links the files of its pack.""" def test_a_whole_pack_links_its_asset(self): self.assertEqual( asset_links("romm", SPLIT_RECORD, "../..", "Download"), "[Download](../../releases/download/v2000.01.01/RomM_1_BIOS_Pack.zip)", ) def test_a_split_pack_links_every_part(self): cell = asset_links("batocera", SPLIT_RECORD, "../..", "Download") for part in ("part1of2", "part2of2"): self.assertIn( f"releases/download/v2000.01.01/Batocera_1_BIOS_Pack.{part}.zip", cell ) self.assertIn("part 1 of 2", cell) def test_a_split_pack_says_how_many_files_to_download(self): """One link taken for the pack left half of it out of the folder.""" self.assertIn( "Download, 2 files, all needed: ", asset_links("batocera", SPLIT_RECORD, "../..", "Download"), ) buttons = asset_links("batocera", SPLIT_RECORD, REPO, "Pack", " { .x }") self.assertTrue(buttons.startswith("2 files to download, all needed: "), buttons) def test_buttons_carry_their_attributes_on_each_part(self): cell = asset_links("batocera", SPLIT_RECORD, REPO, "Pack", " { .x }") self.assertEqual(cell.count("{ .x }"), 2) def test_without_a_recorded_release_the_link_is_the_release_page(self): self.assertEqual( asset_links("romm", {}, "../..", "Download"), "[Download](../../releases/latest)", ) class DownloadLinksKeepTheirAddress(unittest.TestCase): """Every release deletes the previous tag and its asset URLs: six of the eight links a community wiki had to our packs answered 404. A download cell links the platform's get/ page, which outlives the release.""" def test_a_released_pack_links_its_get_page(self): self.assertEqual( download_links("romm", SPLIT_RECORD, "Download", site=SITE_URL), f"[Download]({SITE_URL}get/romm/)", ) self.assertEqual( download_links("batocera", SPLIT_RECORD, "Download", site=SITE_URL), f"[Download]({SITE_URL}get/batocera/)", ) def test_a_setup_without_a_release_pack_reads_use_the_command(self): self.assertEqual( download_links("bizhawk", SPLIT_RECORD, "Download", site=SITE_URL), f"[Use the command]({SITE_URL}get/bizhawk/)", ) def test_a_marked_setup_the_release_lacks_has_no_zip_yet(self): self.assertEqual( download_links("retroarch", SPLIT_RECORD, "Download", site=SITE_URL), f"No ZIP yet: [use the command]({SITE_URL}get/retroarch/)", ) def test_without_a_recorded_release_the_get_page_answers(self): self.assertEqual( download_links("romm", {}, "Download", site=SITE_URL), f"[Download]({SITE_URL}get/romm/)", ) def test_the_readme_table_links_get_pages(self): coverages = { "batocera": {"platform": "Batocera"}, "bizhawk": {"platform": "BizHawk"}, "romm": {"platform": "RomM"}, } rows = download_table(coverages, {}, SPLIT_RECORD)[2:] self.assertEqual(len(rows), 3) for row in rows: self.assertNotIn("/releases/", row) self.assertIn(f"{SITE_URL}get/", row) self.assertIn("[Use the command]", next(r for r in rows if "BizHawk" in r)) def test_the_generated_readme_links_every_get_page(self): if not RECORD.get("packs"): self.skipTest("release.json is not written") from common import list_registered_platforms, list_release_pack_platforms readme = _read("README.md") table = _section(readme, "## Or download a ZIP") self.assertNotIn("/releases/download/", table) released = set(list_release_pack_platforms("platforms")) rows = [line for line in table.splitlines() if line.startswith("| ")] for key in list_registered_platforms("platforms"): with self.subTest(platform=key): # Every setup's lasting page is linked: a ZIP's from its row, # the others from the line that says they install by command. self.assertIn(f"]({SITE_URL}get/{key}/)", table) in_rows = any(f"get/{key}/)" in row for row in rows) self.assertEqual(in_rows, key in released) def test_the_download_page_rows_link_get_pages(self): page = _which_pack() rows = [line for line in page.splitlines() if line.startswith("| ")] self.assertTrue(rows) for row in rows: self.assertNotIn("/releases/", row) def test_a_platform_without_a_zip_offers_the_command(self): """KNULLI, AmberELEC and EmuELEC were registered after the recorded release, and the platforms without release_pack never get a ZIP: a row for them offers the command.""" from common import list_registered_platforms, load_platform_config import generate_site record = release_record.load_advertised() page = _which_pack() for key in list_registered_platforms("platforms"): if release_record.pack_for(key, record) is not None: continue label = load_platform_config(key, "platforms").get("platform", key) row = next(line for line in page.splitlines() if line.startswith(f"| [{label}](")) state = "No ZIP yet." if generate_site.has_release_pack(key) else "No ZIP." with self.subTest(platform=key): self.assertNotIn("/releases/", row) self.assertIn(f"{state} [Use the command](platforms/{key}.md#install).", row) class SlicesLeaveWithTheirRelease(unittest.TestCase): """Parts named `.zip.001` were cut by v2026.09.04, deleted since. A page explaining a layout no release serves buries the one the user has.""" PAGES = ("README.md", "wiki/troubleshooting.md") def setUp(self): assets = [a for p in RECORD.get("packs", {}).values() for a in p["assets"]] if any(re.search(r"\.zip\.\d{3}$", a) for a in assets): self.skipTest("the recorded release is published in slices") def test_user_pages_name_one_layout(self): for page in self.PAGES: with self.subTest(page=page): self.assertNotIn(".zip.001", _read(page)) self.assertNotIn("copy /b", _read(page)) def test_the_download_page_names_one_layout(self): self.assertNotIn(".zip.001", _which_pack()) class TheReadmeLeadsBeginnersToTheirPages(unittest.TestCase): """The README linked the installer reference and the FAQ's no-ZIP anchor only: a user stopped by an install error or a BIOS-not-found message had no link to the first install or troubleshooting page.""" def test_quick_install_links_the_start_pages(self): # The code block holds `#` comment lines, so the section runs to # the next level-two heading. section = _read("README.md").split("## Install in one line", 1)[1].split("\n## ", 1)[0] for page in ("first-install", "troubleshooting", "faq"): with self.subTest(page=page): self.assertIn(f"wiki/{page}/)", section) self.assertTrue((REPO_ROOT / "wiki" / f"{page}.md").is_file()) class EndUsersCheckTheirOwnFolder(unittest.TestCase): """verify.py reads database.json and bios/. Advice about a user's own files points at the installer's --check, which hashes their folder.""" USER_SECTIONS = ( ("wiki/troubleshooting.md", "## 1. Check the BIOS folder { #checking-what-you-installed }"), ("wiki/troubleshooting.md", "## 2. Fix the files"), ("wiki/faq.md", "## A game shows a black screen { #my-game-shows-a-black-screen }"), ("wiki/faq.md", "## How to tell which BIOS is needed? { #how-do-i-know-which-bios-i-need }"), ) def test_user_sections_never_send_to_verify(self): for page, heading in self.USER_SECTIONS: with self.subTest(page=page, heading=heading): section = _section(_read(page), heading) self.assertNotIn("verify.py", section) self.assertNotIn("python install.py --check", section) def test_the_download_page_gives_the_one_liner_check(self): page = _which_pack() self.assertNotIn("from a checkout", page) self.assertIn("/i | sh -s -- --check", page) def test_the_readme_does_not_call_verify_a_check_of_your_bios(self): self.assertNotIn("Verify your BIOS collection", _read("README.md")) class DestIsTheBiosFolder(unittest.TestCase): """--dest is used as the BIOS folder as given (install.py), so the root of a card puts every file beside the folder the frontend reads.""" PAGES = ( "README.md", "wiki/installer.md", "wiki/faq.md", "wiki/first-install.md", ) def test_no_example_installs_to_a_card_root(self): for page in self.PAGES: for dest in re.findall(r"--dest\s+(\S+)", _read(page)): with self.subTest(page=page, dest=dest): self.assertFalse(dest.rstrip("/").endswith("sdcard"), dest) class ShownOutputIsPrintedOutput(unittest.TestCase): """Every line in the troubleshooting page's verify samples has a shape verify.py prints, and the shapes are read from its source.""" SHAPES = ( (r"^\S.*: \d+/\d+ (OK|present)\b.*\[(md5|sha1|existence)\]$", "}: {', '.join(parts)} [{mode}]"), (r"^ (OK|UNTESTED|MISSING) \((required|optional)[^)]*\): \S+/\S+", '" UNTESTED ({req}{hle}): {key}: {reason}"'), (r"^ DISCREPANCY: \S+/\S+: ", '" DISCREPANCY: {key}: {disc}"'), # A count of optional files missing is followed by where to list them: # the sample once showed "1 optional missing" with no such suffix. (r"^ Core files: \d+ in pack, \d+ required missing, " r"(0 optional missing|\d+ optional missing \(--verbose lists them\))$", '" Core files: {core_in_pack} in pack'), (r"^ Core files held but not packed: \d+ \(.+\)$", '" Core files held but not packed: '), (r"^ Bundled with their emulator, not collected: \d+( \(.+\))?$", '" Bundled with their emulator, not collected: {len(bundled)}"'), (r"^ Game data: \d+ in pack, (0 missing|\d+ missing \(--verbose lists them\))$", '" Game data: {gd_packed} in pack'), (r"^ Game data held but not packed: \d+ \(.+\)$", '" Game data held but not packed: '), (r"^ No external files: \d+ cores \(--verbose lists them\)$", '" No external files: {len(exclusions)} cores (--verbose lists them)"'), (r"^ Ground truth: \d+/\d+ files have emulator validation \(\d+%\)$", '" Ground truth: {gt_cov[\'with_validation\']}/{gt_cov[\'total\']} files have emulator validation'), (r"^ \d+ platform-only \(no emulator profile\)$", '" {no_profile} platform-only (no emulator profile)"'), (r"^ \S", None), ) def test_the_shapes_are_verify_output(self): source = _read("scripts/verify.py") for _pattern, literal in self.SHAPES: if literal: with self.subTest(literal=literal): self.assertIn(literal, source) def test_samples_match_a_shape(self): section = _section(_read("wiki/reading-reports.md"), "### Reading the output") blocks = re.findall(r"```\n(.*?)```", section, re.DOTALL) self.assertTrue(blocks) for block in blocks: for line in block.splitlines(): with self.subTest(line=line): self.assertTrue( any(re.match(pattern, line) for pattern, _ in self.SHAPES), line ) def test_an_optional_count_without_its_suffix_is_refused(self): line = " Core files: 1495 in pack, 0 required missing, 1 optional missing" self.assertFalse(any(re.match(pattern, line) for pattern, _ in self.SHAPES)) def test_no_severity_column(self): self.assertNotIn("[CRITICAL]", _read("wiki/troubleshooting.md")) class SizesComeFromTheRecord(unittest.TestCase): def test_the_quoted_pack_sizes_are_the_recorded_ones(self): text = _read("wiki/release-process.md") quoted = re.search( r"in (v[\d.]+),\s+(\w+) downloaded as ([\d.]+ GB) and extracted to" r"\s+([\d.]+ GB)", text, ) self.assertIsNotNone(quoted, "the sizes are quoted with their release tag") tag, platform, download, extracted = quoted.groups() if tag != RECORD.get("tag"): self.skipTest(f"release.json records {RECORD.get('tag')}, not {tag}") pack = release_record.pack_for(platform.lower(), RECORD) self.assertEqual(download, format_size(pack["download_size"])) self.assertEqual(extracted, format_size(pack["extracted_size"])) class ContributorChecksMatchCi(unittest.TestCase): """validate.yml fetches the data directories before the suite: the tests that build packs exit non-zero without them.""" def test_the_pr_checklist_fetches_data_directories_before_the_tests(self): for text in ( contributing_text(lambda page: page, "# C", "here"), _read("CONTRIBUTING.md"), ): block = _section(text, "## Before opening a PR") self.assertIn("python scripts/refresh_data_dirs.py", block) self.assertLess( block.index("refresh_data_dirs.py"), block.index("unittest discover") ) def test_ci_does_the_same(self): workflow = _read(".github/workflows/validate.yml") self.assertLess( workflow.index("python scripts/refresh_data_dirs.py"), workflow.index("python -m unittest discover"), ) def test_the_pr_checklist_generates_the_site_before_the_tests(self): # validate.yml generates the pages before the suite. Without them # the 228 tests that read the gitignored pages fail on a clone. workflow = _read(".github/workflows/validate.yml") self.assertLess( workflow.index("run: python scripts/generate_site.py"), workflow.index("python -m unittest discover"), ) for text in ( contributing_text(lambda page: page, "# C", "here"), _read("CONTRIBUTING.md"), ): block = _section(text, "## Before opening a PR") self.assertIn("python scripts/generate_site.py", block) self.assertLess( block.index("generate_site.py"), block.index("unittest discover") ) class PagesOpenOnTheirContent(unittest.TestCase): """The download page said "This page picks the right pack for a setup" before saying anything about a pack.""" def test_no_paragraph_announces_its_page(self): import generate_site # noqa: PLC0415 text = generate_site.generate_which_pack() announcing = [ line for line in text.splitlines() if re.match(r"(This|The) (page|section) ", line) ] self.assertEqual(announcing, []) class CoreCountsReadFromTheData(unittest.TestCase): """The cores page said "623 cores across 18 platforms": that was every profile, and 91 of them no platform runs.""" def test_the_run_count_is_what_the_platforms_resolve(self): import yaml from common import load_emulator_profiles, load_platform_config, resolve_platform_cores from tests import sitepages sitepages.require() text = sitepages.page("cross-reference.md").read_text(encoding="utf-8") profiles = load_emulator_profiles(str(REPO_ROOT / "emulators")) # A test core says so in role, its type staying operational. unique = {k: v for k, v in profiles.items() if v.get("type") != "alias" and v.get("role") != "test"} registry = yaml.safe_load((REPO_ROOT / "platforms" / "_registry.yml").read_text( encoding="utf-8"))["platforms"] run = set() for name in registry: config = load_platform_config(name, str(REPO_ROOT / "platforms")) run |= set(resolve_platform_cores(config, unique)) said = re.search(r"([\d,]+) cores are profiled, ([\d,]+) of them run by the (\d+) platforms", text) self.assertIsNotNone(said, "the page says how many profiled cores the platforms run") self.assertEqual(int(said[1].replace(",", "")), len(unique)) self.assertEqual(int(said[2].replace(",", "")), len(run)) self.assertEqual(int(said[3]), len(registry)) # What the generators write must not bring back what the project retired. # The generated pages, README.md, CONTRIBUTING.md and the site's components # are read for a retired command, address or feature term, each listed with # the reason it was retired, and for every `--option` a generated command # passes: the script it names must declare it in its parser. The pages copied # from wiki/ are left to their own tests: the installer page keeps the raw # one-liner as the documented fallback. def _sitepages(): from tests import sitepages return sitepages # (string, why it is retired). Compared case-sensitively against the text. RETIRED = ( ("raw.githubusercontent.com/Abdess/retrobios/main/install.sh", "the site serves the one-liner at https://abdess.github.io/retrobios/i " "since 2026-10-28, pinned to the commit the site was built from"), ("raw.githubusercontent.com/Abdess/retrobios/main/install.ps1", "served at https://abdess.github.io/retrobios/i.ps1 since 2026-10-28"), ("Abdess/retroarch_system", "the repository was renamed retrobios in March 2026"), ("mkdocs serve", "the MkDocs site was replaced by Astro Starlight on 2026-10-22"), ("mkdocs build", "the MkDocs site was replaced by Astro Starlight on 2026-10-22"), ("pip install mkdocs", "the site is built by Node from website/package-lock.json"), ('class="rb-tested', "the Tested blocks were removed on 2026-10-28: " "only Proof lines and the Measurements page remain"), ("rb-region-runs", "region outcomes left the system pages on 2026-10-28"), ("## Campaign", "the campaign table left the Measurements page on 2026-10-28"), ("| Tested by |", "the coverage table left the Measurements page on 2026-10-28"), ("ms per frame", "benchmark figures stay in the records and SQLite files, " "never on a page, since 2026-10-28"), ("with this BIOS folder as", "the Tested block of platform pages, removed on 2026-10-28"), ("regenerated in its native format", "the export patches each platform's own " "file and never regenerates it (scripts/exporter/base_exporter.py needs_original)"), ) COMMAND = re.compile( r"(?:python3? (?P