Files
libretro/tests/test_release_packs.py
T

391 lines
17 KiB
Python

"""Which platforms a release publishes a ZIP for, and how every surface says it.
The release carried a pack for every platform, 35 GB, while five of them
drew 83% of the downloads. `release_pack: true` in the registry is now the
list: the build, the pack checks, the release record and every download
table read it. The other platforms keep their install manifest, and each
page that would show a ZIP says there is none and gives the command.
Every release also deleted the previous tag, and with it each asset URL:
six of the eight links a community wiki had to our packs answered 404. Each
platform has a permanent page at get/<platform>/ instead.
"""
from __future__ import annotations
import contextlib
import io
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import common # noqa: E402
import release_record # noqa: E402
PLATFORMS = str(REPO_ROOT / "platforms")
TMP_ROOT = REPO_ROOT / "tmp"
SITE = "https://abdess.github.io/retrobios/"
# The owner's choice of 2026-10-23, from the download counts of v2026.10.10.
RELEASED = {"retroarch", "emudeck", "retrobat", "batocera", "romm", "misterfpga"}
def _write_registry(root: Path, platforms: dict) -> str:
platforms_dir = root / "platforms"
platforms_dir.mkdir()
for name in platforms:
(platforms_dir / f"{name}.yml").write_text(
yaml.dump({"platform": name.title(), "systems": {}}), encoding="utf-8"
)
registry = {name: {"config": f"{name}.yml", **entry} for name, entry in platforms.items()}
(platforms_dir / "_registry.yml").write_text(
yaml.dump({"platforms": registry}), encoding="utf-8"
)
return str(platforms_dir)
def _which_pack() -> str:
import generate_site
with contextlib.chdir(REPO_ROOT):
return generate_site.generate_which_pack()
class TheFieldIsTheList(unittest.TestCase):
def test_the_registry_marks_the_most_downloaded_setups(self):
self.assertEqual(
set(common.list_release_pack_platforms(PLATFORMS, include_archived=True)), RELEASED
)
def test_only_marked_platforms_are_listed_and_archived_ones_on_request(self):
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as tmp:
platforms_dir = _write_registry(Path(tmp), {
"big": {"status": "active", "release_pack": True},
"small": {"status": "active"},
"off": {"status": "active", "release_pack": False},
"old": {"status": "archived", "release_pack": True},
})
self.assertEqual(common.list_release_pack_platforms(platforms_dir), ["big"])
self.assertEqual(
common.list_release_pack_platforms(platforms_dir, include_archived=True),
["big", "old"],
)
self.assertEqual(
common.list_registered_platforms(platforms_dir), ["big", "off", "small"]
)
def test_the_schema_holds_the_field(self):
try:
from jsonschema import Draft202012Validator
except ImportError:
self.skipTest("jsonschema is a tooling dependency")
schema = json.loads((REPO_ROOT / "schemas" / "registry.schema.json").read_text())
validator = Draft202012Validator(schema)
registry = yaml.safe_load((REPO_ROOT / "platforms" / "_registry.yml").read_text())
self.assertEqual([e.message for e in validator.iter_errors(registry)], [])
registry["platforms"]["knulli"]["release_pack"] = "yes"
self.assertTrue(list(validator.iter_errors(registry)))
class TheBuildReadsTheField(unittest.TestCase):
def _groups(self, *argv: str) -> list[str]:
import generate_pack
built: list[str] = []
def capture(args, groups, *rest, **kw):
built.extend(name for group, _rep in groups for name in group)
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as out, \
contextlib.chdir(REPO_ROOT), \
mock.patch.object(generate_pack, "_run_platform_packs", capture), \
mock.patch.object(generate_pack, "_run_manifest_mode", capture), \
mock.patch.object(sys, "argv", ["generate_pack.py", *argv, "--output-dir", out]), \
contextlib.redirect_stdout(io.StringIO()):
generate_pack.main()
return built
def test_all_builds_the_release_packs_only(self):
self.assertEqual(set(self._groups("--all", "--offline")), RELEASED)
def test_archived_platforms_without_the_field_stay_out(self):
self.assertEqual(
set(self._groups("--all", "--include-archived", "--offline")), RELEASED
)
def test_manifests_cover_every_platform(self):
self.assertEqual(
set(self._groups("--all", "--manifest", "--offline")),
set(common.list_registered_platforms(PLATFORMS, include_archived=True)),
)
def test_the_release_owners_are_the_marked_platforms(self):
owners = release_record.release_owners(PLATFORMS)
served = {p for platforms in owners.values() for p in platforms}
self.assertEqual(served, RELEASED)
def test_the_list_marks_installer_only_platforms(self):
import subprocess
proc = subprocess.run(
[sys.executable, "scripts/generate_pack.py", "--list"],
capture_output=True, text=True, cwd=REPO_ROOT, check=True, timeout=120,
)
lines = proc.stdout.splitlines()
for name in RELEASED:
self.assertIn(name, lines)
self.assertIn("recalbox (installer only)", lines)
class APlatformWithoutAPackFailsNoCheck(unittest.TestCase):
VERIFY = (
"RetroArch: 10/10 OK [existence]\n"
"KNULLI: 5/5 OK [md5]\n"
)
PACK = (
"Generating pack for RetroArch...\n"
" dist/RetroArch_BIOS_Pack.zip: 10 files packed, 10/10 files OK [existence]\n"
)
def test_the_consistency_step_skips_a_setup_served_by_the_installer(self):
import pipeline
with contextlib.redirect_stdout(io.StringIO()) as out:
self.assertTrue(pipeline.check_consistency(
self.VERIFY, self.PACK, pipeline.release_pack_keys(PLATFORMS)))
self.assertIn("KNULLI: 5/5 (no release pack, served by the installer)", out.getvalue())
def test_a_released_platform_without_its_pack_still_fails(self):
import pipeline
verify = self.VERIFY + "Batocera: 3/3 OK [md5]\n"
with contextlib.redirect_stdout(io.StringIO()):
self.assertFalse(pipeline.check_consistency(
verify, self.PACK, pipeline.release_pack_keys(PLATFORMS)))
def test_the_pipeline_passes_the_release_platforms(self):
source = (REPO_ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8")
self.assertIn("release_pack_keys(include_archived=args.include_archived)", source)
def test_verify_packs_sweeps_the_release_packs_only(self):
import subprocess
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory:
proc = subprocess.run(
[sys.executable, "scripts/generate_pack.py", "--all", "--verify-packs",
"--output-dir", directory],
capture_output=True, text=True, cwd=REPO_ROOT, timeout=600,
)
swept = {line.split(":")[0].strip() for line in proc.stdout.splitlines()
if "SKIP (no pack in" in line}
self.assertEqual(swept, RELEASED)
class TheReleaseCheckReadsTheField(unittest.TestCase):
RECORD = {"tag": "v1", "packs": {
"A_BIOS_Pack.zip": {"download_size": 1, "assets": ["A_BIOS_Pack.zip"],
"platforms": ["retroarch"]},
"K_BIOS_Pack.zip": {"download_size": 1, "assets": ["K_BIOS_Pack.zip"],
"platforms": ["knulli"]},
}}
def test_a_missing_and_an_unlisted_pack_are_reported(self):
from check_release import Asset, compare_release
sums = {"A_BIOS_Pack.zip": "a" * 64, "K_BIOS_Pack.zip": "b" * 64}
assets = {name: Asset(1, digest) for name, digest in sums.items()}
assets["SHA256SUMS.txt"] = Asset(1, None)
findings = compare_release(self.RECORD, sums, assets, "v1", None,
{"retroarch", "batocera"})
kinds = {(f.kind, f.name) for f in findings}
self.assertIn(("no-pack", "batocera"), kinds)
self.assertIn(("not-released", "K_BIOS_Pack.zip"), kinds)
self.assertNotIn(("no-pack", "retroarch"), kinds)
class TheTablesFollowTheField(unittest.TestCase):
def test_a_pack_of_a_platform_without_the_field_is_not_advertised(self):
record = {"tag": "v1", "packs": {
"RetroArch_Lakka_BIOS_Pack.zip": {"platforms": ["lakka", "retroarch"],
"assets": ["x.zip"]},
"Recalbox_BIOS_Pack.zip": {"platforms": ["recalbox"], "assets": ["y.zip"]},
}}
shown = release_record.advertised(record, PLATFORMS)
self.assertEqual(list(shown["packs"]), ["RetroArch_Lakka_BIOS_Pack.zip"])
self.assertEqual(shown["packs"]["RetroArch_Lakka_BIOS_Pack.zip"]["platforms"],
["retroarch"])
self.assertIsNone(release_record.pack_for("lakka", shown))
self.assertEqual(release_record.advertised({}, PLATFORMS), {})
class EverySurfaceSaysWhy(unittest.TestCase):
def test_a_setup_page_says_no_zip_with_the_command_beside_it(self):
import generate_site
line = generate_site.no_zip_line("KNULLI")
self.assertEqual(line, "No ZIP for KNULLI. The command installs every file and checks it.")
with contextlib.chdir(REPO_ROOT):
block = generate_site._pack_buttons("knulli")
guide = generate_site._guide_download(
"knulli", "KNULLI", generate_site.GUIDES["knulli"], None, "## Download")
self.assertIn(line, block)
self.assertIn(generate_site.NO_ZIP_FAQ, block)
self.assertIn(line, "\n".join(guide))
self.assertNotIn("yet", block)
def test_a_built_setup_page_has_the_line(self):
page = REPO_ROOT / "website" / "src" / "content" / "docs" / "platforms" / "knulli.md"
if not page.is_file():
self.skipTest("the site is not generated")
text = page.read_text(encoding="utf-8")
self.assertIn("No ZIP for KNULLI. The command installs every file and checks it.", text)
install = text.index("## Install")
self.assertLess(install, text.index("No ZIP for KNULLI"))
def test_get_your_files_explains_the_rows(self):
page = _which_pack()
self.assertIn("No ZIP. [Use the command](platforms/knulli.md#install).", page)
self.assertIn("ZIPs are made for the setups most people download.", page)
self.assertIn("wiki/faq.md#why-is-there-no-zip-for-my-setup", page)
def test_the_readme_rows_read_use_the_command(self):
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
row = next(line for line in readme.splitlines() if line.startswith("| KNULLI |"))
self.assertIn(f"[Use the command]({SITE}get/knulli/)", row)
self.assertIn("ZIPs are made for the setups most people download.", readme)
def test_the_faq_answers_why(self):
faq = (REPO_ROOT / "wiki" / "faq.md").read_text(encoding="utf-8")
section = faq.split("## Why is there no ZIP for my setup?", 1)[1].split("\n## ", 1)[0]
self.assertIn("most people download", section)
self.assertIn("checks each one", section)
self.assertIn("--dry-run", section)
def test_the_release_process_documents_the_field_and_the_counts(self):
page = (REPO_ROOT / "wiki" / "release-process.md").read_text(encoding="utf-8")
self.assertIn("release_pack: true", page)
self.assertIn("download_count", page)
self.assertIn("part1of", page)
class TheReleaseNotesNameTheSetupsWithoutAZip(unittest.TestCase):
RECORD = {"tag": "v2026.10.24", "packs": {
"RomM_5_BIOS_Pack.zip": {
"files": 1, "extracted_size": 1, "download_size": 1,
"assets": ["RomM_5_BIOS_Pack.zip"], "platforms": ["romm"]},
}}
def test_the_paragraph_names_them_and_points_to_the_command(self):
import release_notes
text = release_notes.render(self.RECORD, PLATFORMS, "### What changed\n\n- x\n")
paragraph = next(line for line in text.splitlines() if line.startswith("No ZIP for "))
for name in ("KNULLI", "Recalbox", "RetroPie", "Lakka"):
self.assertIn(name, paragraph)
for name in ("RetroArch", "Batocera", "RomM"):
self.assertNotIn(f" {name},", paragraph)
self.assertIn("The command above", paragraph)
self.assertIn("--dry-run", paragraph)
self.assertLess(text.index(paragraph), text.index("### What changed"))
def test_the_rows_link_get_pages(self):
import release_notes
text = release_notes.render(self.RECORD, PLATFORMS, "x")
self.assertIn(f"[Download]({SITE}get/romm/)", text)
self.assertNotIn("/releases/download/", text)
class EveryPlatformHasAPermanentPage(unittest.TestCase):
def test_a_page_for_every_registered_platform(self):
import generate_site
record = release_record.load_advertised()
with contextlib.chdir(REPO_ROOT):
for name in common.list_registered_platforms(PLATFORMS, include_archived=True):
page = generate_site.generate_get_page(name)
pack = release_record.pack_for(name, record)
with self.subTest(platform=name):
self.assertRegex(page, r"\n# (.+ )?BIOS (pack|files)( for .+)?\n")
if name in RELEASED and pack:
for asset in pack["assets"]:
self.assertIn(
f"/releases/download/{record['tag']}/{asset}", page)
elif name not in RELEASED:
self.assertNotIn("_BIOS_Pack", page)
self.assertIn(f"install.sh | sh -s -- --platform {name}", page)
self.assertIn("No ZIP for ", page)
def test_the_generator_writes_and_sweeps_them(self):
import generate_site
self.assertIn(release_record.GET_DIR, generate_site.GENERATED_DIRS)
source = Path(generate_site.__file__).read_text(encoding="utf-8")
self.assertIn('write_if_changed(str(get_dir / f"{name}.md"), generate_get_page(name))',
source)
def test_built_pages_exist(self):
docs = REPO_ROOT / "website" / "src" / "content" / "docs" / release_record.GET_DIR
if not docs.is_dir():
self.skipTest("the site is not generated")
for name in common.list_registered_platforms(PLATFORMS, include_archived=True):
with self.subTest(platform=name):
self.assertTrue(any(docs.glob(f"{name}.md*")))
class TheSiteCheckResolvesEveryGetPage(unittest.TestCase):
def _site(self, root: Path, pages: dict[str, str]) -> Path:
site = root / "dist"
for rel, body in pages.items():
path = site / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(f"<!doctype html><html lang=en><body>{body}</body></html>",
encoding="utf-8")
return site
def test_missing_and_wrong_pages_are_reported(self):
import validate_site
record = {"tag": "v1", "packs": {"RomM_BIOS_Pack.zip": {
"platforms": ["romm"], "assets": ["RomM_BIOS_Pack.zip"],
"files": 1, "extracted_size": 1, "download_size": 1}}}
download = validate_site.RELEASE_DOWNLOAD
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as tmp:
root = Path(tmp)
record_path = root / "release.json"
record_path.write_text(json.dumps(record), encoding="utf-8")
pages = {
f"get/{name}/index.html": "install.sh"
for name in common.list_registered_platforms(PLATFORMS, include_archived=True)
}
pages["get/romm/index.html"] = f'<a href="{download}v1/RomM_BIOS_Pack.zip">x</a>'
pages["get/knulli/index.html"] = f'<a href="{download}v1/K_BIOS_Pack.zip">x</a>'
del pages["get/lakka/index.html"]
site = self._site(root, pages)
parsed = {p.resolve(): validate_site._parse_page(p) for p in site.rglob("*.html")}
issues = validate_site._check_get_pages(parsed, site.resolve(), Path(PLATFORMS),
record_path)
joined = "\n".join(issues)
self.assertIn("get/lakka/index.html: no download page", joined)
self.assertIn("get/knulli/index.html: links a pack", joined)
self.assertIn("get/knulli/index.html: gives no install command", joined)
self.assertNotIn("get/romm/", joined)
self.assertNotIn("get/retroarch/", joined)
def test_the_cli_runs_the_check(self):
source = (REPO_ROOT / "scripts" / "validate_site.py").read_text(encoding="utf-8")
self.assertIn("validate_site(Path(args.site_dir), args.base, args.platforms_dir, args.record)",
source)
if __name__ == "__main__":
unittest.main()