From 3b137cd266bc6a7a710c27621baeabf24659b8c3 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:44:31 +0100 Subject: [PATCH] fix: name a missing folder in the folder check --- scripts/check_platform_folder.py | 18 +++++++++--------- scripts/generate_site.py | 2 +- tests/test_site_reference.py | 25 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/scripts/check_platform_folder.py b/scripts/check_platform_folder.py index 4c5401a44..b2d845b5d 100644 --- a/scripts/check_platform_folder.py +++ b/scripts/check_platform_folder.py @@ -1,22 +1,19 @@ -"""Check a BIOS folder against one platform's list, from the published data only: -python check_platform_folder.py batocera path/to/bios. Exit 1 on a file wrong or missing.""" +"""Usage: python check_platform_folder.py batocera path/to/bios. Exit 1 on a file wrong or missing.""" from __future__ import annotations import hashlib import json import sys -import urllib.error import urllib.request import zipfile from pathlib import Path API = "https://abdess.github.io/retrobios/api/v1/platforms/" -DIGESTS = {"md5": "md5", "sha1": "sha1"} # without one (existence), presence passes def accepts(entry: dict, path: Path, mode: str) -> bool: """Whether the platform's own check passes the file at path.""" - digest, raw = DIGESTS.get(mode), entry.get(mode) or "" + digest, raw = mode if mode in ("md5", "sha1") else None, entry.get(mode) or "" # existence: no digest values = [v.strip().lower() for v in (raw if isinstance(raw, list) else str(raw).split(",")) if v.strip()] if digest is None or not values: return path.is_file() # nothing to compare: the platform checks presence @@ -29,7 +26,7 @@ def accepts(entry: dict, path: Path, mode: str) -> bool: actual = hashlib.new(digest, path.read_bytes()).hexdigest() if entry.get("zip_digest") != "composite" and any(actual.startswith(v) for v in values): return True # a truncated md5 (Batocera) compares by its prefix - if digest == DIGESTS["md5"] and path.suffix.lower() == ".zip": # Recalbox: members in name order + if digest == "md5" and path.suffix.lower() == ".zip": # Recalbox: members in name order with zipfile.ZipFile(path) as archive: names = sorted(name for name in archive.namelist() if not name.endswith("/")) return hashlib.md5(b"".join(map(archive.read, names))).hexdigest() in values @@ -42,8 +39,7 @@ def check(platform: str, root: Path, api: str = API) -> list[tuple[str, str]]: """(destination, ok | wrong | missing) per entry. With no destination (EmuDeck), any file with its hash passes.""" with urllib.request.urlopen(f"{api}{platform}.json") as response: contract = json.load(response)["items"][0]["contract"] - mode, results = contract["verification_mode"], [] - loose = [path for path in root.iterdir() if path.is_file()] if root.is_dir() else [] + mode, results, loose = contract["verification_mode"], [], [p for p in root.iterdir() if p.is_file()] for system_id, system in contract["systems"].items(): listed = [e for e in system.get("files") or [] if not e.get("destination")] for entry in (e for e in system.get("files") or [] if e.get("destination")): @@ -57,12 +53,16 @@ def check(platform: str, root: Path, api: str = API) -> list[tuple[str, str]]: if __name__ == "__main__": + if len(sys.argv) != 3: + sys.exit(__doc__) try: found = check(sys.argv[1], Path(sys.argv[2])) - except urllib.error.HTTPError as error: # a 404: no platform has that name + except urllib.request.HTTPError as error: # a 404: no platform has that name with urllib.request.urlopen(API.replace("platforms/", "catalog.json")) as response: known = sorted(i["url"][10:-5] for i in json.load(response)["items"] if i["url"].startswith("manifests/")) sys.exit(f"unknown platform: {sys.argv[1]}. Known: {', '.join(known)}" if error.code == 404 else error) + except OSError as error: # no such folder, or no network (URLError) + sys.exit(f"error: {error}") for verdict in ("ok", "wrong", "missing"): names = sorted({dest for dest, seen in found if seen == verdict}) print(f"{verdict}: {len(names)}" + "".join(f"\n {name}" for name in names if verdict != "ok")) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 22ac647b6..a49eb98ea 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -2330,7 +2330,7 @@ def generate_data_page(exports: list[dict], measurements_sqlite: Path | None = N f"({REPO_URL}/blob/main/scripts/check_platform_folder.py). A test runs it " "against the verifier on a sample folder, so the two give the same answer. " "It exits 1 when a file is wrong or missing. For an unknown platform it " - "prints the known ones.", + "prints the known ones, and it names a folder that does not exist.", "", "`install.py --check` answers another question by default: whether the folder " "holds what the pack installs, the manifest above. With `--declared-only`, or " diff --git a/tests/test_site_reference.py b/tests/test_site_reference.py index 36f130fde..4f46676af 100644 --- a/tests/test_site_reference.py +++ b/tests/test_site_reference.py @@ -1464,6 +1464,31 @@ class TheFolderCheckExitCode(unittest.TestCase): "nosuch", Path(folder), {"/nosuch.json": None, "/catalog.json": catalog}) self.assertEqual(code, "unknown platform: nosuch. Known: batocera") + def test_a_folder_that_is_not_there_is_named(self): + """A mistyped folder read as every file missing, exit 1.""" + with tempfile.TemporaryDirectory() as folder: + code, _err = self.run_script("p", Path(folder) / "bois", {"/p.json": self.CONTRACT}) + self.assertIsInstance(code, str) + self.assertIn("bois", code) + self.assertIn("No such file or directory", code) + + def test_a_missing_argument_prints_the_usage(self): + err = io.StringIO() + with mock.patch.object(sys, "argv", ["check", "batocera"]), \ + mock.patch("sys.stderr", err), self.assertRaises(SystemExit) as raised: + runpy.run_path(self.SCRIPT, run_name="__main__") + self.assertTrue(str(raised.exception.code).startswith("Usage: ")) + + def test_no_network_is_a_message(self): + def offline(url: str, *args, **kwargs): + raise urllib.error.URLError("Name or service not known") + with tempfile.TemporaryDirectory() as folder, \ + mock.patch("urllib.request.urlopen", offline), \ + mock.patch.object(sys, "argv", ["check", "p", folder]), \ + self.assertRaises(SystemExit) as raised: + runpy.run_path(self.SCRIPT, run_name="__main__") + self.assertEqual(raised.exception.code, "error: ") + def test_the_page_shows_the_script(self): script = Path(self.SCRIPT).read_text(encoding="utf-8") self.assertIn("```python\n" + script.rstrip("\n") + "\n```",