From b23f5b04ad9b45ea7ae2b09ba8f6a336fc974bd1 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:39:27 +0200 Subject: [PATCH] fix: refuse a --bios-dir that does not exist --- scripts/auto_fetch.py | 2 ++ scripts/common.py | 11 +++++++++++ scripts/dedup.py | 3 ++- scripts/generate_pack.py | 2 ++ scripts/identity.py | 3 +++ scripts/romset_recipes.py | 2 ++ tests/test_system_selection.py | 35 ++++++++++++++++++++++++++++++++++ 7 files changed, 57 insertions(+), 1 deletion(-) diff --git a/scripts/auto_fetch.py b/scripts/auto_fetch.py index 61d258a7..4f3dbf69 100644 --- a/scripts/auto_fetch.py +++ b/scripts/auto_fetch.py @@ -29,6 +29,7 @@ from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from common import ( + require_directory, list_registered_platforms, load_database, load_platform_config, @@ -429,6 +430,7 @@ def main() -> None: "--create-issues", action="store_true", help="Output GitHub Issue bodies" ) args = parser.parse_args() + require_directory(args.bios_dir) if not os.path.exists(args.db): print(f"Error: {args.db} not found. Run generate_db.py first.", file=sys.stderr) diff --git a/scripts/common.py b/scripts/common.py index 8a114964..dd9daac7 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -31,6 +31,17 @@ except ImportError: +def require_directory(path: str, flag: str = "--bios-dir") -> None: + """Stop on a folder that does not exist. + + identity --strict, dedup, auto_fetch, romset_recipes and generate_pack + read a mistyped --bios-dir as an empty collection and passed: 0 + conflicts, 0 duplicates, a pack of nothing. + """ + if not os.path.isdir(path): + raise SystemExit(f"ERROR: {flag} {path} is not a directory") + + class DatabaseUnreadable(FileNotFoundError): """The database a command was pointed at does not exist or is not JSON.""" diff --git a/scripts/dedup.py b/scripts/dedup.py index 44fb1b88..7005bdbb 100644 --- a/scripts/dedup.py +++ b/scripts/dedup.py @@ -28,7 +28,7 @@ from collections import defaultdict from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) -from common import compute_hashes, write_text_atomic +from common import compute_hashes, require_directory, write_text_atomic DEFAULT_BIOS_DIR = "bios" @@ -319,6 +319,7 @@ def main() -> None: ) parser.add_argument("--bios-dir", default=DEFAULT_BIOS_DIR) args = parser.parse_args() + require_directory(args.bios_dir) print(f"Scanning {args.bios_dir}/ for duplicates...") if args.dry_run: diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 60489646..d1320b53 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -33,6 +33,7 @@ from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) from common import resolve_platform_cores as _platform_cores from common import ( + require_directory, target_listing_line, suggest_names, require_platform, @@ -2977,6 +2978,7 @@ def _main(held: contextlib.ExitStack) -> None: packresolve.set_offline(bool(args.offline)) if args.platform: require_platform(args.platform, args.platforms_dir) + require_directory(args.bios_dir) _refuse_unapplied_flags(args, parser) # Quick-exit modes: --verify-packs alone = verify existing packs only diff --git a/scripts/identity.py b/scripts/identity.py index 381ed84d..fd6572a5 100644 --- a/scripts/identity.py +++ b/scripts/identity.py @@ -151,6 +151,9 @@ def main() -> int: "--strict", action="store_true", help="exit non-zero when any is found" ) args = parser.parse_args() + from common import require_directory # noqa: PLC0415 + + require_directory(args.bios_dir) duplicates = scan(args.bios_dir) if args.json: diff --git a/scripts/romset_recipes.py b/scripts/romset_recipes.py index fe9a3bb1..0870f0cc 100644 --- a/scripts/romset_recipes.py +++ b/scripts/romset_recipes.py @@ -35,6 +35,7 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from common import ( + require_directory, load_database, load_emulator_profiles, load_platform_config, @@ -471,6 +472,7 @@ def main() -> int: help="recipe snapshot file, or a directory of them", ) args = parser.parse_args() + require_directory(args.bios_dir) _resolve_modes(args) db = load_database(args.db) diff --git a/tests/test_system_selection.py b/tests/test_system_selection.py index 57adb608..00559e43 100644 --- a/tests/test_system_selection.py +++ b/tests/test_system_selection.py @@ -208,6 +208,41 @@ class AMissingDatabaseIsNamed(unittest.TestCase): self.assertIn("nosuch.json", proc.stderr) +class AMissingBiosDirIsRefused(unittest.TestCase): + """identity --strict passed on a mistyped --bios-dir with "0 contents", + dedup saved 0 MB, and generate_pack wrote a pack of nothing: a folder + that does not exist read as an empty collection.""" + + EXTRA = { + "auto_fetch.py": ["--platform", "batocera", "--dry-run"], + "generate_pack.py": ["--platform", "batocera", "--offline", "--output-dir", "{out}"], + "dedup.py": ["--dry-run"], + "identity.py": ["--strict"], + } + + def test_every_script_taking_a_bios_dir_refuses_a_missing_one(self): + scratch = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") + self.addCleanup(scratch.cleanup) + out = str(Path(scratch.name) / "out") + taking = sorted( + path.name for path in (REPO_ROOT / "scripts").glob("*.py") + if re.search(r'add_argument\(\s*"--bios-dir"', path.read_text(encoding="utf-8")) + and path.name != "generate_db.py" + ) + self.assertGreater(len(taking), 4) + for script in taking: + extra = [arg.format(out=out) for arg in self.EXTRA.get(script, [])] + with self.subTest(script=script): + proc = subprocess.run( + [sys.executable, f"scripts/{script}", "--bios-dir", "nosuch", *extra], + capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=600, + check=False, + ) + self.assertNotEqual(proc.returncode, 0) + self.assertIn("--bios-dir nosuch is not a directory", proc.stderr) + self.assertFalse(Path(out).exists()) + + class StrictVerifyFailsOnACriticalMiss(unittest.TestCase): """verify exited 0 with a required file missing on an md5 platform, so the pipeline printed "verify OK" over a pack the frontend rejects."""