mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 12:49:03 -05:00
Five tabs from Start here to Contribute, a level on every page, a device picker on Get your files that lists every registered platform, and platform pages that open on where the files go, the command, the pack and how to check. FAQ and troubleshooting keep the questions asked before installing, the technical ones move to their own page, and getting started points to the pages that took its content.
343 lines
13 KiB
Python
343 lines
13 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. 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(" | ")[2])
|
|
|
|
|
|
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_a_release_without_the_pack_names_the_installer(self):
|
|
self.assertEqual(
|
|
download_links("bizhawk", SPLIT_RECORD, "../..", "Download"),
|
|
"Not in v2000.01.01: "
|
|
"[use the installer](https://abdess.github.io/retrobios/wiki/installer/)",
|
|
)
|
|
|
|
def test_without_a_recorded_release_the_link_is_the_release_page(self):
|
|
self.assertEqual(
|
|
download_links("bizhawk", {}, "../..", "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)
|
|
|
|
def test_a_platform_the_release_lacks_links_no_release(self):
|
|
"""KNULLI, AmberELEC and EmuELEC were registered after the recorded
|
|
release: a row for them linked a release page holding no such pack.
|
|
Every platform is on the picker now, so the row offers the command."""
|
|
if not RECORD.get("packs"):
|
|
self.skipTest("release.json is not written")
|
|
from common import list_registered_platforms, load_platform_config
|
|
from release_record import pack_for
|
|
|
|
page = _which_pack()
|
|
for key in list_registered_platforms("platforms", include_archived=True):
|
|
if 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}]("))
|
|
with self.subTest(platform=key):
|
|
self.assertNotIn("/releases/", row)
|
|
self.assertIn(f"Not in {RECORD['tag']}: [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/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/troubleshooting.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/verification-modes.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()
|