mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 12:49:03 -05:00
314 lines
12 KiB
Python
314 lines
12 KiB
Python
"""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 contextlib
|
|
import re
|
|
import sys
|
|
import unittest
|
|
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
|
|
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."""
|
|
|
|
def test_every_detected_system_lists_the_platform(self):
|
|
import yaml # noqa: PLC0415
|
|
|
|
registry = yaml.safe_load(_read("platforms/_registry.yml"))
|
|
names = {"retroarch": "RetroArch", "retrobat": "RetroBat", "emudeck": "EmuDeck",
|
|
"bizhawk": "BizHawk"}
|
|
page = _which_pack()
|
|
for os_name, heading in (("windows", "### Windows"), ("darwin", "### macOS")):
|
|
table = _section(page, heading)
|
|
for platform, entry in registry["platforms"].items():
|
|
systems = {rule["os"] for rule in (entry.get("install") or {}).get("detect", [])}
|
|
if os_name in systems:
|
|
with self.subTest(os=os_name, platform=platform):
|
|
self.assertIn(f"[{names[platform]}](", table)
|
|
|
|
|
|
class DownloadLinksNameTheFile(unittest.TestCase):
|
|
def test_a_whole_pack_links_its_asset(self):
|
|
self.assertEqual(
|
|
download_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 = download_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_buttons_carry_their_attributes_on_each_part(self):
|
|
cell = download_links("batocera", SPLIT_RECORD, REPO, "Pack", " { .x }")
|
|
self.assertEqual(cell.count("{ .x }"), 2)
|
|
|
|
def test_without_a_pack_the_link_is_the_release_page(self):
|
|
self.assertEqual(
|
|
download_links("bizhawk", SPLIT_RECORD, "../..", "Download"),
|
|
"[Download](../../releases/latest)",
|
|
)
|
|
|
|
def test_the_readme_table_links_each_recorded_pack(self):
|
|
coverages = {
|
|
"batocera": {"platform": "Batocera"},
|
|
"romm": {"platform": "RomM"},
|
|
}
|
|
rows = download_table(coverages, set(), {}, SPLIT_RECORD)[2:]
|
|
self.assertEqual(len(rows), 2)
|
|
for row in rows:
|
|
self.assertNotIn("releases/latest", row)
|
|
self.assertIn("releases/download/v2000.01.01/", row)
|
|
|
|
def test_the_generated_readme_links_every_released_asset(self):
|
|
if not RECORD.get("packs"):
|
|
self.skipTest("release.json is not written")
|
|
readme = _read("README.md")
|
|
for pack in RECORD["packs"].values():
|
|
for asset in pack["assets"]:
|
|
with self.subTest(asset=asset):
|
|
self.assertIn(
|
|
f"../../releases/download/{RECORD['tag']}/{asset})", readme
|
|
)
|
|
|
|
def test_the_download_page_rows_link_assets(self):
|
|
if not RECORD.get("packs"):
|
|
self.skipTest("release.json is not written")
|
|
page = _which_pack()
|
|
rows = [line for line in page.splitlines() if line.startswith("| ")]
|
|
self.assertTrue(rows)
|
|
for row in rows:
|
|
self.assertNotIn("releases/latest", row)
|
|
for pack in RECORD["packs"].values():
|
|
for asset in pack["assets"]:
|
|
self.assertIn(f"/releases/download/{RECORD['tag']}/{asset})", page)
|
|
|
|
|
|
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/getting-started.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 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", "## Game won't start / black screen"),
|
|
("wiki/faq.md", "## My game shows a black screen"),
|
|
("wiki/faq.md", "## How do I know which BIOS I need?"),
|
|
("wiki/getting-started.md", "## Checking what you installed"),
|
|
)
|
|
|
|
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("install.sh | 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/getting-started.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}"'),
|
|
(r"^ Core files: \d+ in pack, \d+ required missing, \d+ optional missing$", '" Core files: {core_in_pack} in pack'),
|
|
(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/troubleshooting.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_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"),
|
|
)
|
|
|
|
|
|
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, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|