diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0e57bda38..8a75a5dee 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -32,6 +32,9 @@ on: - "retired.json" - "measurements/**" - "website/**" + # The type check's configuration and baseline. + - "pyproject.toml" + - "mypy-baseline.txt" # Spelled out twice because the workflow parser reads no YAML anchor. A test # holds the two lists equal. push: @@ -58,6 +61,8 @@ on: - "retired.json" - "measurements/**" - "website/**" + - "pyproject.toml" + - "mypy-baseline.txt" permissions: contents: read @@ -163,6 +168,17 @@ jobs: - name: Validate platform configs and emulator profiles run: python scripts/validate_schemas.py --source-only + # The annotations are checked against mypy-baseline.txt, the errors the + # code carried when the check started: a new error fails, and so does + # an entry that no longer occurs until the baseline is synced. mypy + # reads no installed package (no_site_packages in pyproject.toml), so + # what this job has installed does not change the result. + - name: Type-check the scripts and the installer + run: | + pip install "mypy==1.18.2" "mypy-baseline==0.7.4" + mypy > "$RUNNER_TEMP/mypy.txt" || true + mypy-baseline filter --no-colors < "$RUNNER_TEMP/mypy.txt" + # A profile written from a clone can cite lines that do not hold the value # it declares, and schemas and tests pass it. Each profile a change adds is # confronted with its upstream, both roads into main. An edited profile is diff --git a/install/targets/emudeck.json b/install/targets/emudeck.json index 4ef016714..995b97a60 100644 --- a/install/targets/emudeck.json +++ b/install/targets/emudeck.json @@ -342,7 +342,6 @@ "freechaf", "freeintv", "frodo", - "fsuae", "fuse", "galaksija", "gam4980", @@ -424,7 +423,6 @@ "nxengine", "o2em", "oberon", - "omicron", "openlara", "opera", "panda3ds", @@ -505,7 +503,6 @@ "vice_xplus4", "vice_xscpu64", "vice_xvic", - "video_processor", "vircon32", "virtualjaguar", "virtualxt", diff --git a/measurements/pack-probes.jsonl b/measurements/pack-probes.jsonl index 4f4d63da7..b485648dc 100644 --- a/measurements/pack-probes.jsonl +++ b/measurements/pack-probes.jsonl @@ -13,20 +13,20 @@ {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_psx_hw_libretro.so.zip","file":"mednafen_psx_hw_libretro.so","last_modified":"Tue, 27 Oct 2026 05:06:00 GMT","sha256":"d16c889c9f12bd3265edc3c9d4b0e6e24c1f244d1d8d475235f6a74e65e5b10b"},"emulator":"beetle_psx","error":null,"files_opened":[{"created":false,"entry":"scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-7117d9fe809f","install":"pack","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:54:23+00:00","records":["2026-10-27-436b1708ef9b"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2"},"staged":24,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/pcsx_rearmed_libretro.so.zip","file":"pcsx_rearmed_libretro.so","last_modified":"Sun, 25 Oct 2026 06:43:04 GMT","sha256":"bfda3f667f8ea8c2f5cc939b970628174b6f491ad32d7f6b143728322df46af6"},"emulator":"pcsx_rearmed","error":null,"files_opened":[{"created":false,"entry":"scph5500.bin","path":"scph5500.bin","sha1":"b05def971d8ec59f346f2d9ac21fb742e3eb6917"},{"created":false,"entry":"scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"},{"created":false,"entry":"scph5502.bin","path":"scph5502.bin","sha1":"f6bc2d1f5eb6593de7d089c425ac681d6fffd3f0"}],"findings":[],"id":"2026-10-27-f215251f1fbe","install":"pack","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:55:00+00:00","records":["2026-10-27-dcc3253dd29c"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2"},"staged":22,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/swanstation_libretro.so.zip","file":"swanstation_libretro.so","last_modified":"Tue, 27 Oct 2026 05:20:50 GMT","sha256":"fc65bb2d662c3ad762b611a62ac578f78efd260431d3b8c090f0eb40887a77ea"},"emulator":"swanstation","error":null,"files_opened":[{"created":false,"entry":"scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-e0de25b19c5d","install":"pack","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:55:04+00:00","records":["2026-10-27-4b1fae82aa24"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2"},"staged":160,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/blastem_libretro.so.zip","file":"blastem_libretro.so","last_modified":"Tue, 27 Oct 2026 06:40:24 GMT","sha256":"1901fcbccef88c088328a18e680e11f3626175ea4cc36ba31674286afdc7ac9f"},"emulator":"blastem","error":null,"files_opened":[{"created":false,"entry":"files:bios_MD.bin","path":"bios_MD.bin","sha1":"3f50b76b0529db7f79c396b5e808cc0786ffc311"},{"created":false,"entry":"files:bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"f4f315adcef9b8feb0364c21ab7f0eaf5457f3ed"}],"findings":[{"declared_by":["tmss.md"],"kind":"missing_declared","path":"tmss.md","paths":[]}],"id":"2026-10-27-999ee89554ae","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T14:55:53+00:00","records":["2026-10-27-20cd02176c4b"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":9,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/clownmdemu_libretro.so.zip","file":"clownmdemu_libretro.so","last_modified":"Mon, 26 Oct 2026 05:34:34 GMT","sha256":"0d85101878d347a420cf67bf35d9fa339ac8bbe0a636064a5a66af43eb9cec2a"},"emulator":"clownmdemu","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-401e733ca21d","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T14:56:45+00:00","records":["2026-10-27-1642d5598a5b"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":3,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/genesis_plus_gx_libretro.so.zip","file":"genesis_plus_gx_libretro.so","last_modified":"Sat, 24 Oct 2026 07:22:46 GMT","sha256":"90b3b66fa998b7769e0c56c6aa288f891eec33c3cd1cf328ccbddb2d628a1522"},"emulator":"genesis_plus_gx","error":null,"files_opened":[{"created":false,"entry":"files:bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"f4f315adcef9b8feb0364c21ab7f0eaf5457f3ed"}],"findings":[],"id":"2026-10-27-ac147338e3a8","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:17+00:00","records":["2026-10-27-1510af952e4f"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":12,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/genesis_plus_gx_wide_libretro.so.zip","file":"genesis_plus_gx_wide_libretro.so","last_modified":"Sun, 25 Oct 2026 06:32:50 GMT","sha256":"03e157b15bd9faadab5a79dde1986c90ba7da9c11b5b44d4243b60c931e23d78"},"emulator":"genesis_plus_gx_wide","error":null,"files_opened":[{"created":false,"entry":"files:bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"f4f315adcef9b8feb0364c21ab7f0eaf5457f3ed"}],"findings":[],"id":"2026-10-27-7b9744e164c6","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:19+00:00","records":["2026-10-27-d192c31e4bbc"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":12,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/picodrive_libretro.so.zip","file":"picodrive_libretro.so","last_modified":"Sun, 25 Oct 2026 05:53:32 GMT","sha256":"fd4010afa91f69d3c61be49c95e14a5c37117c4d0899d3e593ccc4339c1417d5"},"emulator":"picodrive","error":null,"files_opened":[{"created":false,"entry":"files:us_scd2_9306.bin","path":"us_scd2_9306.bin","sha1":"5adb6c3af218c60868e6b723ec47e36bbdf5e6f0"},{"created":false,"entry":"files:carthw.cfg","path":"carthw.cfg","sha1":"d3fe0b958705e4cb5eeb7822e60d474204df4bd3"}],"findings":[],"id":"2026-10-27-019d891b68e9","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:20+00:00","records":["2026-10-27-568e6ae9560c"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":16,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_saturn_libretro.so.zip","file":"mednafen_saturn_libretro.so","last_modified":"Tue, 27 Oct 2026 07:34:40 GMT","sha256":"2cef16a3444d45032ad0ae0d523cbff0e5655f4bc15eb33406c81212f54dc9c1"},"emulator":"beetle_saturn","error":null,"files_opened":[{"created":false,"entry":"files:sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"}],"findings":[],"id":"2026-10-27-6207be7f6fa2","install":"installer","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:31+00:00","records":["2026-10-27-323214feeed8"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":19,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/kronos_libretro.so.zip","file":"kronos_libretro.so","last_modified":"Tue, 27 Oct 2026 06:30:48 GMT","sha256":"ca7d0655cb4283b1749ddd2751450d2ec0fd2ea3a3686bde5c4b626cb99e06ac"},"emulator":"kronos","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-c2f209b271ee","install":"installer","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:32+00:00","records":["2026-10-27-f9b964643090"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":18,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabasanshiro_libretro.so.zip","file":"yabasanshiro_libretro.so","last_modified":"Tue, 27 Oct 2026 06:26:02 GMT","sha256":"d75ae811201373f0b3db69b4258f5d19ee42bfb81231af8abeb6a8389a8417ec"},"emulator":"yabasanshiro","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-f39c67d20831","install":"installer","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:32+00:00","records":["2026-10-27-a9e89d04a77f"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":13,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabause_libretro.so.zip","file":"yabause_libretro.so","last_modified":"Tue, 27 Oct 2026 06:42:50 GMT","sha256":"974a4cc700ed7eef80b8a33abd33ed07ab317f2123a1014b78c57e043b923296"},"emulator":"yabause","error":null,"files_opened":[{"created":false,"entry":"files:saturn_bios.bin","path":"saturn_bios.bin","sha1":"2b8cb4f87580683eb4d760e4ed210813d667f0a2"}],"findings":[],"id":"2026-10-27-00c60e795b5f","install":"installer","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:39+00:00","records":["2026-10-27-6ca3d0d1f0ec"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":13,"system":"sega-saturn","target":"linux-x86_64","verdict":"hang","verdict_evidence":{"detail":"1 of 1 repetitions: the last 183 sampled frames are one colour each","repetitions":["hang"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/ymir_libretro.so.zip","file":"ymir_libretro.so","last_modified":"Tue, 27 Oct 2026 05:34:56 GMT","sha256":"237d154fa6ef5caccc1d2899e0a2444947e4391a81bd241803932d1319832712"},"emulator":"ymir","error":null,"files_opened":[{"created":false,"entry":"files:sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"created":false,"entry":"files:mpr-17933.bin","path":"mpr-17933.bin","sha1":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3"},{"created":false,"entry":"files:saturn_bios.bin","path":"saturn_bios.bin","sha1":"2b8cb4f87580683eb4d760e4ed210813d667f0a2"},{"created":false,"entry":"files:cdb/cdb_rom.bin","path":"cdb/cdb_rom.bin","sha1":"b3c20fbe57cd2eb595e9edac86817e5948dccae4"}],"findings":[],"id":"2026-10-27-da1992edf668","install":"installer","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:51+00:00","records":["2026-10-27-0cfeae5767b2"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":14,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_psx_libretro.so.zip","file":"mednafen_psx_libretro.so","last_modified":"Tue, 27 Oct 2026 05:05:54 GMT","sha256":"a89200443c0afa594e85e5e77630921d5e5b04ebf9c499597f3e6f1c947b0c87"},"emulator":"beetle_psx","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-400f6875c7b5","install":"installer","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:59+00:00","records":["2026-10-27-953506afe46d"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":24,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_psx_hw_libretro.so.zip","file":"mednafen_psx_hw_libretro.so","last_modified":"Tue, 27 Oct 2026 05:06:00 GMT","sha256":"d16c889c9f12bd3265edc3c9d4b0e6e24c1f244d1d8d475235f6a74e65e5b10b"},"emulator":"beetle_psx","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-3ac99e860c4f","install":"installer","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:58:05+00:00","records":["2026-10-27-01ee781f441e"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":24,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/pcsx_rearmed_libretro.so.zip","file":"pcsx_rearmed_libretro.so","last_modified":"Sun, 25 Oct 2026 06:43:04 GMT","sha256":"bfda3f667f8ea8c2f5cc939b970628174b6f491ad32d7f6b143728322df46af6"},"emulator":"pcsx_rearmed","error":null,"files_opened":[{"created":false,"entry":"files:scph5500.bin","path":"scph5500.bin","sha1":"b05def971d8ec59f346f2d9ac21fb742e3eb6917"},{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"},{"created":false,"entry":"files:scph5502.bin","path":"scph5502.bin","sha1":"f6bc2d1f5eb6593de7d089c425ac681d6fffd3f0"}],"findings":[],"id":"2026-10-27-5305f9a96888","install":"installer","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:58:13+00:00","records":["2026-10-27-ebaf456445fd"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":22,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/swanstation_libretro.so.zip","file":"swanstation_libretro.so","last_modified":"Tue, 27 Oct 2026 05:20:50 GMT","sha256":"fc65bb2d662c3ad762b611a62ac578f78efd260431d3b8c090f0eb40887a77ea"},"emulator":"swanstation","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-5bd8c6d9541c","install":"installer","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:58:16+00:00","records":["2026-10-27-9165bec33048"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":160,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/blastem_libretro.so.zip","file":"blastem_libretro.so","last_modified":"Tue, 27 Oct 2026 06:40:24 GMT","sha256":"1901fcbccef88c088328a18e680e11f3626175ea4cc36ba31674286afdc7ac9f"},"emulator":"blastem","error":null,"files_opened":[{"created":false,"entry":"files:bios_MD.bin","path":"bios_MD.bin","sha1":"3f50b76b0529db7f79c396b5e808cc0786ffc311"},{"created":false,"entry":"files:bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"f4f315adcef9b8feb0364c21ab7f0eaf5457f3ed"}],"findings":[{"declared_by":["tmss.md"],"kind":"missing_declared","path":"tmss.md","paths":[]}],"id":"2026-10-27-6d9bf0105e6b","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T14:55:53+00:00","records":["2026-10-27-20cd02176c4b"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":9,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/clownmdemu_libretro.so.zip","file":"clownmdemu_libretro.so","last_modified":"Mon, 26 Oct 2026 05:34:34 GMT","sha256":"0d85101878d347a420cf67bf35d9fa339ac8bbe0a636064a5a66af43eb9cec2a"},"emulator":"clownmdemu","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-57ca2c364da2","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T14:56:45+00:00","records":["2026-10-27-1642d5598a5b"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":3,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/genesis_plus_gx_libretro.so.zip","file":"genesis_plus_gx_libretro.so","last_modified":"Sat, 24 Oct 2026 07:22:46 GMT","sha256":"90b3b66fa998b7769e0c56c6aa288f891eec33c3cd1cf328ccbddb2d628a1522"},"emulator":"genesis_plus_gx","error":null,"files_opened":[{"created":false,"entry":"files:bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"f4f315adcef9b8feb0364c21ab7f0eaf5457f3ed"}],"findings":[],"id":"2026-10-27-7fdea1d8d23a","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:17+00:00","records":["2026-10-27-1510af952e4f"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":12,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/genesis_plus_gx_wide_libretro.so.zip","file":"genesis_plus_gx_wide_libretro.so","last_modified":"Sun, 25 Oct 2026 06:32:50 GMT","sha256":"03e157b15bd9faadab5a79dde1986c90ba7da9c11b5b44d4243b60c931e23d78"},"emulator":"genesis_plus_gx_wide","error":null,"files_opened":[{"created":false,"entry":"files:bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"f4f315adcef9b8feb0364c21ab7f0eaf5457f3ed"}],"findings":[],"id":"2026-10-27-63b36da4e56d","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:19+00:00","records":["2026-10-27-d192c31e4bbc"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":12,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/picodrive_libretro.so.zip","file":"picodrive_libretro.so","last_modified":"Sun, 25 Oct 2026 05:53:32 GMT","sha256":"fd4010afa91f69d3c61be49c95e14a5c37117c4d0899d3e593ccc4339c1417d5"},"emulator":"picodrive","error":null,"files_opened":[{"created":false,"entry":"files:us_scd2_9306.bin","path":"us_scd2_9306.bin","sha1":"5adb6c3af218c60868e6b723ec47e36bbdf5e6f0"},{"created":false,"entry":"files:carthw.cfg","path":"carthw.cfg","sha1":"d3fe0b958705e4cb5eeb7822e60d474204df4bd3"}],"findings":[],"id":"2026-10-27-fff0117b43b1","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:20+00:00","records":["2026-10-27-568e6ae9560c"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":16,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_saturn_libretro.so.zip","file":"mednafen_saturn_libretro.so","last_modified":"Tue, 27 Oct 2026 07:34:40 GMT","sha256":"2cef16a3444d45032ad0ae0d523cbff0e5655f4bc15eb33406c81212f54dc9c1"},"emulator":"beetle_saturn","error":null,"files_opened":[{"created":false,"entry":"files:sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"}],"findings":[],"id":"2026-10-27-cba4ec471283","install":"installer","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:31+00:00","records":["2026-10-27-323214feeed8"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":19,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/kronos_libretro.so.zip","file":"kronos_libretro.so","last_modified":"Tue, 27 Oct 2026 06:30:48 GMT","sha256":"ca7d0655cb4283b1749ddd2751450d2ec0fd2ea3a3686bde5c4b626cb99e06ac"},"emulator":"kronos","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-52f7110f988c","install":"installer","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:32+00:00","records":["2026-10-27-f9b964643090"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":18,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabasanshiro_libretro.so.zip","file":"yabasanshiro_libretro.so","last_modified":"Tue, 27 Oct 2026 06:26:02 GMT","sha256":"d75ae811201373f0b3db69b4258f5d19ee42bfb81231af8abeb6a8389a8417ec"},"emulator":"yabasanshiro","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-4801da4eab2a","install":"installer","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:32+00:00","records":["2026-10-27-a9e89d04a77f"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":13,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabause_libretro.so.zip","file":"yabause_libretro.so","last_modified":"Tue, 27 Oct 2026 06:42:50 GMT","sha256":"974a4cc700ed7eef80b8a33abd33ed07ab317f2123a1014b78c57e043b923296"},"emulator":"yabause","error":null,"files_opened":[{"created":false,"entry":"files:saturn_bios.bin","path":"saturn_bios.bin","sha1":"2b8cb4f87580683eb4d760e4ed210813d667f0a2"}],"findings":[],"id":"2026-10-27-1f998c82b1ae","install":"installer","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:39+00:00","records":["2026-10-27-6ca3d0d1f0ec"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":13,"system":"sega-saturn","target":"linux-x86_64","verdict":"hang","verdict_evidence":{"detail":"1 of 1 repetitions: the last 183 sampled frames are one colour each","repetitions":["hang"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/ymir_libretro.so.zip","file":"ymir_libretro.so","last_modified":"Tue, 27 Oct 2026 05:34:56 GMT","sha256":"237d154fa6ef5caccc1d2899e0a2444947e4391a81bd241803932d1319832712"},"emulator":"ymir","error":null,"files_opened":[{"created":false,"entry":"files:sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"created":false,"entry":"files:mpr-17933.bin","path":"mpr-17933.bin","sha1":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3"},{"created":false,"entry":"files:saturn_bios.bin","path":"saturn_bios.bin","sha1":"2b8cb4f87580683eb4d760e4ed210813d667f0a2"},{"created":false,"entry":"files:cdb/cdb_rom.bin","path":"cdb/cdb_rom.bin","sha1":"b3c20fbe57cd2eb595e9edac86817e5948dccae4"}],"findings":[],"id":"2026-10-27-b2819fb679eb","install":"installer","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:51+00:00","records":["2026-10-27-0cfeae5767b2"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":14,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_psx_libretro.so.zip","file":"mednafen_psx_libretro.so","last_modified":"Tue, 27 Oct 2026 05:05:54 GMT","sha256":"a89200443c0afa594e85e5e77630921d5e5b04ebf9c499597f3e6f1c947b0c87"},"emulator":"beetle_psx","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-65efad62e33f","install":"installer","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:57:59+00:00","records":["2026-10-27-953506afe46d"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":24,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_psx_hw_libretro.so.zip","file":"mednafen_psx_hw_libretro.so","last_modified":"Tue, 27 Oct 2026 05:06:00 GMT","sha256":"d16c889c9f12bd3265edc3c9d4b0e6e24c1f244d1d8d475235f6a74e65e5b10b"},"emulator":"beetle_psx","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-fc5fd143e6e9","install":"installer","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:58:05+00:00","records":["2026-10-27-01ee781f441e"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":24,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/pcsx_rearmed_libretro.so.zip","file":"pcsx_rearmed_libretro.so","last_modified":"Sun, 25 Oct 2026 06:43:04 GMT","sha256":"bfda3f667f8ea8c2f5cc939b970628174b6f491ad32d7f6b143728322df46af6"},"emulator":"pcsx_rearmed","error":null,"files_opened":[{"created":false,"entry":"files:scph5500.bin","path":"scph5500.bin","sha1":"b05def971d8ec59f346f2d9ac21fb742e3eb6917"},{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"},{"created":false,"entry":"files:scph5502.bin","path":"scph5502.bin","sha1":"f6bc2d1f5eb6593de7d089c425ac681d6fffd3f0"}],"findings":[],"id":"2026-10-27-500d09e316a3","install":"installer","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:58:13+00:00","records":["2026-10-27-ebaf456445fd"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":22,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/swanstation_libretro.so.zip","file":"swanstation_libretro.so","last_modified":"Tue, 27 Oct 2026 05:20:50 GMT","sha256":"fc65bb2d662c3ad762b611a62ac578f78efd260431d3b8c090f0eb40887a77ea"},"emulator":"swanstation","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-b5d57c2a3d85","install":"installer","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T14:58:16+00:00","records":["2026-10-27-9165bec33048"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"49b47355b9504dbefcb0261513b18e9f15dc20a2","save_dir_files":106},"staged":160,"system":"sony-playstation","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/blastem_libretro.so.zip","file":"blastem_libretro.so","last_modified":"Tue, 27 Oct 2026 06:40:24 GMT","sha256":"1901fcbccef88c088328a18e680e11f3626175ea4cc36ba31674286afdc7ac9f"},"emulator":"blastem","error":null,"files_opened":[{"created":false,"entry":"tmss.md","path":"tmss.md","sha1":"3f50b76b0529db7f79c396b5e808cc0786ffc311"},{"created":false,"entry":"bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"5adb6c3af218c60868e6b723ec47e36bbdf5e6f0"}],"findings":[],"id":"2026-10-27-2b3b2b6e67b7","install":"pack","platform":"batocera","platform_system":"sega-mega-cd","platform_version":"43.1","probed_at":"2026-10-27T17:07:16+00:00","records":["2026-10-27-788859c63e16"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"052986c92351077a88cba47b5314727cb3984f82"},"staged":10,"system":"sega-mega-cd","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/genesis_plus_gx_libretro.so.zip","file":"genesis_plus_gx_libretro.so","last_modified":"Sat, 24 Oct 2026 07:22:46 GMT","sha256":"90b3b66fa998b7769e0c56c6aa288f891eec33c3cd1cf328ccbddb2d628a1522"},"emulator":"genesis_plus_gx","error":null,"files_opened":[{"created":false,"entry":"bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"5adb6c3af218c60868e6b723ec47e36bbdf5e6f0"}],"findings":[],"id":"2026-10-27-2745e259a3bd","install":"pack","platform":"batocera","platform_system":"sega-mega-cd","platform_version":"43.1","probed_at":"2026-10-27T17:07:18+00:00","records":["2026-10-27-34ef09cf10e7"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"052986c92351077a88cba47b5314727cb3984f82"},"staged":12,"system":"sega-mega-cd","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/genesis_plus_gx_wide_libretro.so.zip","file":"genesis_plus_gx_wide_libretro.so","last_modified":"Sun, 25 Oct 2026 06:32:50 GMT","sha256":"03e157b15bd9faadab5a79dde1986c90ba7da9c11b5b44d4243b60c931e23d78"},"emulator":"genesis_plus_gx_wide","error":null,"files_opened":[{"created":false,"entry":"bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"5adb6c3af218c60868e6b723ec47e36bbdf5e6f0"}],"findings":[],"id":"2026-10-27-6275db21bd63","install":"pack","platform":"batocera","platform_system":"sega-mega-cd","platform_version":"43.1","probed_at":"2026-10-27T17:07:20+00:00","records":["2026-10-27-e63bf0cea0fa"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"052986c92351077a88cba47b5314727cb3984f82"},"staged":12,"system":"sega-mega-cd","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} @@ -68,7 +68,7 @@ {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/pcsx_rearmed_libretro.so.zip","file":"pcsx_rearmed_libretro.so","last_modified":"Sun, 25 Oct 2026 06:43:04 GMT","sha256":"bfda3f667f8ea8c2f5cc939b970628174b6f491ad32d7f6b143728322df46af6"},"emulator":"pcsx_rearmed","error":null,"files_opened":[{"created":false,"entry":"files:scph5500.bin","path":"scph5500.bin","sha1":"b05def971d8ec59f346f2d9ac21fb742e3eb6917"},{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"},{"created":false,"entry":"files:scph5502.bin","path":"scph5502.bin","sha1":"f6bc2d1f5eb6593de7d089c425ac681d6fffd3f0"}],"findings":[],"id":"2026-10-27-98b9d80687bc","install":"installer","platform":"retrobat","platform_system":"sony-playstation","platform_version":"8.3.1","probed_at":"2026-10-27T21:46:41+00:00","records":["2026-10-27-c6d5fc766117"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/pak000.pk4, base/pak002.pk4, base/pak003.pk4, base/pak004.pk4 return-to-castle-wolfenstein (realrtcw): main/sp_pak1.pk3 sonic-mania (sonic-mania): Data.rsdk sonic3-air (sonic3-air): Sonic_Knuckles_wSonic3.bin starship (starship): baserom.us.rev1.z64 Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":5561,"path":"install/retrobat.json","sha256":"94a79556469df55169be485274bfb6c6defb7a52a85319440112d0c8f89df1fb"},"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"7433d49666b848062b67cf3a16975be907705ab2","save_dir_files":41},"staged":7,"system":"sony-playstation","target":"windows","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 530 shows Now Loading with the yellow bubble","frame":530,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"e5af1f2023d0d729ce7aabf52d8e03f75bb8d230","stages":["bios","game"]}} {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/swanstation_libretro.so.zip","file":"swanstation_libretro.so","last_modified":"Tue, 27 Oct 2026 05:20:50 GMT","sha256":"fc65bb2d662c3ad762b611a62ac578f78efd260431d3b8c090f0eb40887a77ea"},"emulator":"swanstation","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-c0cd95f153a0","install":"installer","platform":"retrobat","platform_system":"sony-playstation","platform_version":"8.3.1","probed_at":"2026-10-27T21:48:52+00:00","records":["2026-10-27-feb3fc839667"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/pak000.pk4, base/pak002.pk4, base/pak003.pk4, base/pak004.pk4 return-to-castle-wolfenstein (realrtcw): main/sp_pak1.pk3 sonic-mania (sonic-mania): Data.rsdk sonic3-air (sonic3-air): Sonic_Knuckles_wSonic3.bin starship (starship): baserom.us.rev1.z64 Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":5561,"path":"install/retrobat.json","sha256":"94a79556469df55169be485274bfb6c6defb7a52a85319440112d0c8f89df1fb"},"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"7433d49666b848062b67cf3a16975be907705ab2","save_dir_files":41},"staged":307,"system":"sony-playstation","target":"windows","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 1300 shows Now Loading with the yellow bubble","frame":1300,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"e5af1f2023d0d729ce7aabf52d8e03f75bb8d230","stages":["bios","game"]}} {"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/blastem_libretro.so.zip","file":"blastem_libretro.so","last_modified":"Tue, 27 Oct 2026 06:40:24 GMT","sha256":"1901fcbccef88c088328a18e680e11f3626175ea4cc36ba31674286afdc7ac9f"},"emulator":"blastem","error":null,"files_opened":[{"created":false,"entry":"bios_MD.bin","path":"bios_MD.bin","sha1":"3f50b76b0529db7f79c396b5e808cc0786ffc311"},{"created":false,"entry":"bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"f4f315adcef9b8feb0364c21ab7f0eaf5457f3ed"}],"findings":[],"id":"2026-10-27-fd43eb05c3ce","install":"pack","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T21:51:08+00:00","records":["2026-10-27-fffd76b0df0c"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"7433d49666b848062b67cf3a16975be907705ab2"},"staged":9,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/blastem_libretro.so.zip","file":"blastem_libretro.so","last_modified":"Tue, 27 Oct 2026 06:40:24 GMT","sha256":"1901fcbccef88c088328a18e680e11f3626175ea4cc36ba31674286afdc7ac9f"},"emulator":"blastem","error":null,"files_opened":[{"created":false,"entry":"files:bios_MD.bin","path":"bios_MD.bin","sha1":"3f50b76b0529db7f79c396b5e808cc0786ffc311"},{"created":false,"entry":"files:bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"f4f315adcef9b8feb0364c21ab7f0eaf5457f3ed"}],"findings":[],"id":"2026-10-27-50353e16d141","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T21:53:14+00:00","records":["2026-10-27-888669b2583b"],"rounds":1,"schema_version":1,"source":{"installer_summary":"-af6705283a8fc4932/tmp/pack_probe/retroarch/installer/system 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"7433d49666b848062b67cf3a16975be907705ab2","save_dir_files":106},"staged":9,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"After Burner III (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/blastem_libretro.so.zip","file":"blastem_libretro.so","last_modified":"Tue, 27 Oct 2026 06:40:24 GMT","sha256":"1901fcbccef88c088328a18e680e11f3626175ea4cc36ba31674286afdc7ac9f"},"emulator":"blastem","error":null,"files_opened":[{"created":false,"entry":"files:bios_MD.bin","path":"bios_MD.bin","sha1":"3f50b76b0529db7f79c396b5e808cc0786ffc311"},{"created":false,"entry":"files:bios_CD_U.bin","path":"bios_CD_U.bin","sha1":"f4f315adcef9b8feb0364c21ab7f0eaf5457f3ed"}],"findings":[],"id":"2026-10-27-0acbb63a8c6b","install":"installer","platform":"retroarch","platform_system":"sega-mega-cd","platform_version":"v1.22.2","probed_at":"2026-10-27T21:53:14+00:00","records":["2026-10-27-888669b2583b"],"rounds":1,"schema_version":1,"source":{"installer_summary":" 37 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (1 file): jazz-jackrabbit-2 (jazz2-native): jazz2/Source/Anims.j2a Each emulator's page says what these files are: https://abdess.github.io/retrobios/emulators/ To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":10986,"path":"install/retroarch.json","sha256":"115639363828622f29ac8784bc534bb8560a0198ca57aaa19be3f649b9bb8199"},"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"7433d49666b848062b67cf3a16975be907705ab2","save_dir_files":106},"staged":9,"system":"sega-mega-cd","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_psx_libretro.so.zip","file":"mednafen_psx_libretro.so","last_modified":"Tue, 27 Oct 2026 05:05:54 GMT","sha256":"a89200443c0afa594e85e5e77630921d5e5b04ebf9c499597f3e6f1c947b0c87"},"emulator":"beetle_psx","error":null,"files_opened":[{"created":false,"entry":"scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-a2f102062caa","install":"pack","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T21:55:45+00:00","records":["2026-10-27-cb2f506d7cc9"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"a2d30046f2903132fe478661edbbf75c7d2a4767"},"staged":24,"system":"sony-playstation","target":"linux-x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 1280 shows Now Loading with the yellow bubble","frame":1280,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"d51ab7783f8eeafc63a28f14ce39f8e2babeffb3","stages":["bios","game"]}} {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_psx_hw_libretro.so.zip","file":"mednafen_psx_hw_libretro.so","last_modified":"Tue, 27 Oct 2026 05:06:00 GMT","sha256":"d16c889c9f12bd3265edc3c9d4b0e6e24c1f244d1d8d475235f6a74e65e5b10b"},"emulator":"beetle_psx","error":null,"files_opened":[{"created":false,"entry":"scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-46d676fbc1ea","install":"pack","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T21:58:05+00:00","records":["2026-10-27-01436f3dd1dd"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"a2d30046f2903132fe478661edbbf75c7d2a4767"},"staged":24,"system":"sony-playstation","target":"linux-x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 1280 shows Now Loading with the yellow bubble","frame":1280,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"d51ab7783f8eeafc63a28f14ce39f8e2babeffb3","stages":["bios","game"]}} {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/pcsx_rearmed_libretro.so.zip","file":"pcsx_rearmed_libretro.so","last_modified":"Sun, 25 Oct 2026 06:43:04 GMT","sha256":"bfda3f667f8ea8c2f5cc939b970628174b6f491ad32d7f6b143728322df46af6"},"emulator":"pcsx_rearmed","error":null,"files_opened":[{"created":false,"entry":"scph5500.bin","path":"scph5500.bin","sha1":"b05def971d8ec59f346f2d9ac21fb742e3eb6917"},{"created":false,"entry":"scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"},{"created":false,"entry":"scph5502.bin","path":"scph5502.bin","sha1":"f6bc2d1f5eb6593de7d089c425ac681d6fffd3f0"}],"findings":[],"id":"2026-10-27-74b70e46cfb2","install":"pack","platform":"retroarch","platform_system":"sony-playstation","platform_version":"v1.22.2","probed_at":"2026-10-27T22:00:07+00:00","records":["2026-10-27-258389f4d7f9"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"44e08162359530f95bb4d34687ca7b22e01296d5"},"staged":22,"system":"sony-playstation","target":"linux-x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 530 shows Now Loading with the yellow bubble","frame":530,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"e5af1f2023d0d729ce7aabf52d8e03f75bb8d230","stages":["bios","game"]}} @@ -80,9 +80,9 @@ {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_psx_libretro.so.zip","file":"mednafen_psx_libretro.so","last_modified":"Tue, 27 Oct 2026 05:05:54 GMT","sha256":"a89200443c0afa594e85e5e77630921d5e5b04ebf9c499597f3e6f1c947b0c87"},"emulator":"beetle_psx","error":null,"files_opened":[{"created":false,"entry":"scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-35f5003364b6","install":"pack","platform":"batocera","platform_system":"sony-playstation","platform_version":"43.1","probed_at":"2026-10-27T22:23:32+00:00","records":["2026-10-27-0959ceac43e0"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"44e08162359530f95bb4d34687ca7b22e01296d5"},"staged":10,"system":"sony-playstation","target":"x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 1280 shows Now Loading with the yellow bubble","frame":1280,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"d51ab7783f8eeafc63a28f14ce39f8e2babeffb3","stages":["bios","game"]}} {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/pcsx_rearmed_libretro.so.zip","file":"pcsx_rearmed_libretro.so","last_modified":"Sun, 25 Oct 2026 06:43:04 GMT","sha256":"bfda3f667f8ea8c2f5cc939b970628174b6f491ad32d7f6b143728322df46af6"},"emulator":"pcsx_rearmed","error":null,"files_opened":[{"created":false,"entry":"scph5500.bin","path":"scph5500.bin","sha1":"b05def971d8ec59f346f2d9ac21fb742e3eb6917"},{"created":false,"entry":"scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"},{"created":false,"entry":"scph5502.bin","path":"scph5502.bin","sha1":"f6bc2d1f5eb6593de7d089c425ac681d6fffd3f0"}],"findings":[],"id":"2026-10-27-2e558db8013d","install":"pack","platform":"batocera","platform_system":"sony-playstation","platform_version":"43.1","probed_at":"2026-10-27T22:24:58+00:00","records":["2026-10-27-9e53883a5566"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"44e08162359530f95bb4d34687ca7b22e01296d5"},"staged":7,"system":"sony-playstation","target":"x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 530 shows Now Loading with the yellow bubble","frame":530,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"e5af1f2023d0d729ce7aabf52d8e03f75bb8d230","stages":["bios","game"]}} {"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/swanstation_libretro.so.zip","file":"swanstation_libretro.so","last_modified":"Tue, 27 Oct 2026 05:20:50 GMT","sha256":"fc65bb2d662c3ad762b611a62ac578f78efd260431d3b8c090f0eb40887a77ea"},"emulator":"swanstation","error":null,"files_opened":[{"created":false,"entry":"scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-44d39a8db0d9","install":"pack","platform":"batocera","platform_system":"sony-playstation","platform_version":"43.1","probed_at":"2026-10-27T22:25:46+00:00","records":["2026-10-27-bc55bc2a98c6"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"44e08162359530f95bb4d34687ca7b22e01296d5"},"staged":258,"system":"sony-playstation","target":"x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 1300 shows Now Loading with the yellow bubble","frame":1300,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"e5af1f2023d0d729ce7aabf52d8e03f75bb8d230","stages":["bios","game"]}} -{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_psx_libretro.so.zip","file":"mednafen_psx_libretro.so","last_modified":"Tue, 27 Oct 2026 05:05:54 GMT","sha256":"a89200443c0afa594e85e5e77630921d5e5b04ebf9c499597f3e6f1c947b0c87"},"emulator":"beetle_psx","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-c47c2647917e","install":"installer","platform":"batocera","platform_system":"sony-playstation","platform_version":"43.1","probed_at":"2026-10-27T22:27:45+00:00","records":["2026-10-27-f2d677e3e4fc"],"rounds":1,"schema_version":1,"source":{"installer_summary":"tem/.claude/worktrees/agent-af6705283a8fc4932/tmp/pack_probe/batocera/installer/system/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"44e08162359530f95bb4d34687ca7b22e01296d5","save_dir_files":23},"staged":10,"system":"sony-playstation","target":"x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 1280 shows Now Loading with the yellow bubble","frame":1280,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"d51ab7783f8eeafc63a28f14ce39f8e2babeffb3","stages":["bios","game"]}} -{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/pcsx_rearmed_libretro.so.zip","file":"pcsx_rearmed_libretro.so","last_modified":"Sun, 25 Oct 2026 06:43:04 GMT","sha256":"bfda3f667f8ea8c2f5cc939b970628174b6f491ad32d7f6b143728322df46af6"},"emulator":"pcsx_rearmed","error":null,"files_opened":[{"created":false,"entry":"files:scph5500.bin","path":"scph5500.bin","sha1":"b05def971d8ec59f346f2d9ac21fb742e3eb6917"},{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"},{"created":false,"entry":"files:scph5502.bin","path":"scph5502.bin","sha1":"f6bc2d1f5eb6593de7d089c425ac681d6fffd3f0"}],"findings":[],"id":"2026-10-27-328db80ddaba","install":"installer","platform":"batocera","platform_system":"sony-playstation","platform_version":"43.1","probed_at":"2026-10-27T22:28:53+00:00","records":["2026-10-27-77ae5a4a34e4"],"rounds":1,"schema_version":1,"source":{"installer_summary":"tem/.claude/worktrees/agent-af6705283a8fc4932/tmp/pack_probe/batocera/installer/system/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"44e08162359530f95bb4d34687ca7b22e01296d5","save_dir_files":23},"staged":7,"system":"sony-playstation","target":"x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 530 shows Now Loading with the yellow bubble","frame":530,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"e5af1f2023d0d729ce7aabf52d8e03f75bb8d230","stages":["bios","game"]}} -{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/swanstation_libretro.so.zip","file":"swanstation_libretro.so","last_modified":"Tue, 27 Oct 2026 05:20:50 GMT","sha256":"fc65bb2d662c3ad762b611a62ac578f78efd260431d3b8c090f0eb40887a77ea"},"emulator":"swanstation","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-3443028e79fd","install":"installer","platform":"batocera","platform_system":"sony-playstation","platform_version":"43.1","probed_at":"2026-10-27T22:30:09+00:00","records":["2026-10-27-c17d693b47cf"],"rounds":1,"schema_version":1,"source":{"installer_summary":"tem/.claude/worktrees/agent-af6705283a8fc4932/tmp/pack_probe/batocera/installer/system/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"44e08162359530f95bb4d34687ca7b22e01296d5","save_dir_files":23},"staged":258,"system":"sony-playstation","target":"x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 1300 shows Now Loading with the yellow bubble","frame":1300,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"e5af1f2023d0d729ce7aabf52d8e03f75bb8d230","stages":["bios","game"]}} +{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_psx_libretro.so.zip","file":"mednafen_psx_libretro.so","last_modified":"Tue, 27 Oct 2026 05:05:54 GMT","sha256":"a89200443c0afa594e85e5e77630921d5e5b04ebf9c499597f3e6f1c947b0c87"},"emulator":"beetle_psx","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-83d50aaa94c9","install":"installer","platform":"batocera","platform_system":"sony-playstation","platform_version":"43.1","probed_at":"2026-10-27T22:27:45+00:00","records":["2026-10-27-f2d677e3e4fc"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"44e08162359530f95bb4d34687ca7b22e01296d5","save_dir_files":23},"staged":10,"system":"sony-playstation","target":"x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 1280 shows Now Loading with the yellow bubble","frame":1280,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"d51ab7783f8eeafc63a28f14ce39f8e2babeffb3","stages":["bios","game"]}} +{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/pcsx_rearmed_libretro.so.zip","file":"pcsx_rearmed_libretro.so","last_modified":"Sun, 25 Oct 2026 06:43:04 GMT","sha256":"bfda3f667f8ea8c2f5cc939b970628174b6f491ad32d7f6b143728322df46af6"},"emulator":"pcsx_rearmed","error":null,"files_opened":[{"created":false,"entry":"files:scph5500.bin","path":"scph5500.bin","sha1":"b05def971d8ec59f346f2d9ac21fb742e3eb6917"},{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"},{"created":false,"entry":"files:scph5502.bin","path":"scph5502.bin","sha1":"f6bc2d1f5eb6593de7d089c425ac681d6fffd3f0"}],"findings":[],"id":"2026-10-27-29a98aa66a34","install":"installer","platform":"batocera","platform_system":"sony-playstation","platform_version":"43.1","probed_at":"2026-10-27T22:28:53+00:00","records":["2026-10-27-77ae5a4a34e4"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"44e08162359530f95bb4d34687ca7b22e01296d5","save_dir_files":23},"staged":7,"system":"sony-playstation","target":"x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 530 shows Now Loading with the yellow bubble","frame":530,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"e5af1f2023d0d729ce7aabf52d8e03f75bb8d230","stages":["bios","game"]}} +{"content":{"catalog":"redump","label":"Bust-A-Move 2 - Arcade Edition (USA)","region":"north-america"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/swanstation_libretro.so.zip","file":"swanstation_libretro.so","last_modified":"Tue, 27 Oct 2026 05:20:50 GMT","sha256":"fc65bb2d662c3ad762b611a62ac578f78efd260431d3b8c090f0eb40887a77ea"},"emulator":"swanstation","error":null,"files_opened":[{"created":false,"entry":"files:scph5501.bin","path":"scph5501.bin","sha1":"0555c6fae8906f3f09baf5988f00e55f88e9f30b"}],"findings":[],"id":"2026-10-27-020dd1920ba1","install":"installer","platform":"batocera","platform_system":"sony-playstation","platform_version":"43.1","probed_at":"2026-10-27T22:30:09+00:00","records":["2026-10-27-c17d693b47cf"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"44e08162359530f95bb4d34687ca7b22e01296d5","save_dir_files":23},"staged":258,"system":"sony-playstation","target":"x86_64","verdict":"boots","verdict_evidence":{"detail":"1 of 1 repetitions: frame 1300 shows Now Loading with the yellow bubble","frame":1300,"repetitions":["boots"],"rule":"repetitions_agree","sha1":"e5af1f2023d0d729ce7aabf52d8e03f75bb8d230","stages":["bios","game"]}} {"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_saturn_libretro.so.zip","file":"mednafen_saturn_libretro.so","last_modified":"Tue, 27 Oct 2026 07:34:40 GMT","sha256":"2cef16a3444d45032ad0ae0d523cbff0e5655f4bc15eb33406c81212f54dc9c1"},"emulator":"beetle_saturn","error":null,"files_opened":[{"created":false,"entry":"sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"}],"findings":[],"id":"2026-10-27-41d8ddbf0157","install":"pack","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T22:38:01+00:00","records":["2026-10-27-b35ffbd53148"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251"},"staged":19,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/kronos_libretro.so.zip","file":"kronos_libretro.so","last_modified":"Tue, 27 Oct 2026 06:30:48 GMT","sha256":"ca7d0655cb4283b1749ddd2751450d2ec0fd2ea3a3686bde5c4b626cb99e06ac"},"emulator":"kronos","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-3f4e8af52ea7","install":"pack","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T22:48:37+00:00","records":["2026-10-27-c30af9f8fdf6"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251"},"staged":18,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabasanshiro_libretro.so.zip","file":"yabasanshiro_libretro.so","last_modified":"Tue, 27 Oct 2026 06:26:02 GMT","sha256":"d75ae811201373f0b3db69b4258f5d19ee42bfb81231af8abeb6a8389a8417ec"},"emulator":"yabasanshiro","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-01198b318217","install":"pack","platform":"retroarch","platform_system":"sega-saturn","platform_version":"v1.22.2","probed_at":"2026-10-27T22:53:33+00:00","records":["2026-10-27-6448739cb5b6"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroArch_v1.22.2_BIOS_Pack.zip","members":10986,"sha256":"2a99c4568ea11beec6a38a7851a4bc1a122309357f601118981674d5d64dbba0","size":4337327025},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251"},"staged":13,"system":"sega-saturn","target":"linux-x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} @@ -96,14 +96,14 @@ {"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_saturn_libretro.so.zip","file":"mednafen_saturn_libretro.so","last_modified":"Tue, 27 Oct 2026 07:34:40 GMT","sha256":"2cef16a3444d45032ad0ae0d523cbff0e5655f4bc15eb33406c81212f54dc9c1"},"emulator":"beetle_saturn","error":null,"files_opened":[{"created":false,"entry":"sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"}],"findings":[],"id":"2026-10-27-4ebba9f9c0df","install":"pack","platform":"batocera","platform_system":"sega-saturn","platform_version":"43.1","probed_at":"2026-10-27T23:18:01+00:00","records":["2026-10-27-613759bd1faf"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251"},"staged":11,"system":"sega-saturn","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/kronos_libretro.so.zip","file":"kronos_libretro.so","last_modified":"Tue, 27 Oct 2026 06:30:48 GMT","sha256":"ca7d0655cb4283b1749ddd2751450d2ec0fd2ea3a3686bde5c4b626cb99e06ac"},"emulator":"kronos","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-2629752432de","install":"pack","platform":"batocera","platform_system":"sega-saturn","platform_version":"43.1","probed_at":"2026-10-27T23:19:16+00:00","records":["2026-10-27-89d7c65a2d30"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251"},"staged":11,"system":"sega-saturn","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabasanshiro_libretro.so.zip","file":"yabasanshiro_libretro.so","last_modified":"Tue, 27 Oct 2026 06:26:02 GMT","sha256":"d75ae811201373f0b3db69b4258f5d19ee42bfb81231af8abeb6a8389a8417ec"},"emulator":"yabasanshiro","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-226e22f6fa88","install":"pack","platform":"batocera","platform_system":"sega-saturn","platform_version":"43.1","probed_at":"2026-10-27T23:21:32+00:00","records":["2026-10-27-19b6b995c8b3"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251"},"staged":5,"system":"sega-saturn","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_saturn_libretro.so.zip","file":"mednafen_saturn_libretro.so","last_modified":"Tue, 27 Oct 2026 07:34:40 GMT","sha256":"2cef16a3444d45032ad0ae0d523cbff0e5655f4bc15eb33406c81212f54dc9c1"},"emulator":"beetle_saturn","error":null,"files_opened":[{"created":false,"entry":"files:sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"}],"findings":[],"id":"2026-10-27-1ef12db748b4","install":"installer","platform":"batocera","platform_system":"sega-saturn","platform_version":"43.1","probed_at":"2026-10-27T23:28:01+00:00","records":["2026-10-27-af15e4bf2a6c"],"rounds":1,"schema_version":1,"source":{"installer_summary":"tem/.claude/worktrees/agent-af6705283a8fc4932/tmp/pack_probe/batocera/installer/system/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":23},"staged":11,"system":"sega-saturn","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/kronos_libretro.so.zip","file":"kronos_libretro.so","last_modified":"Tue, 27 Oct 2026 06:30:48 GMT","sha256":"ca7d0655cb4283b1749ddd2751450d2ec0fd2ea3a3686bde5c4b626cb99e06ac"},"emulator":"kronos","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-6e7e85fca75c","install":"installer","platform":"batocera","platform_system":"sega-saturn","platform_version":"43.1","probed_at":"2026-10-27T23:37:55+00:00","records":["2026-10-27-6cb5e0f338df"],"rounds":1,"schema_version":1,"source":{"installer_summary":"tem/.claude/worktrees/agent-af6705283a8fc4932/tmp/pack_probe/batocera/installer/system/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":23},"staged":11,"system":"sega-saturn","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabasanshiro_libretro.so.zip","file":"yabasanshiro_libretro.so","last_modified":"Tue, 27 Oct 2026 06:26:02 GMT","sha256":"d75ae811201373f0b3db69b4258f5d19ee42bfb81231af8abeb6a8389a8417ec"},"emulator":"yabasanshiro","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-5f6ba47b3c86","install":"installer","platform":"batocera","platform_system":"sega-saturn","platform_version":"43.1","probed_at":"2026-10-27T23:50:03+00:00","records":["2026-10-27-828497f9002d"],"rounds":1,"schema_version":1,"source":{"installer_summary":"tem/.claude/worktrees/agent-af6705283a8fc4932/tmp/pack_probe/batocera/installer/system/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":23},"staged":5,"system":"sega-saturn","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_saturn_libretro.so.zip","file":"mednafen_saturn_libretro.so","last_modified":"Tue, 27 Oct 2026 07:34:40 GMT","sha256":"2cef16a3444d45032ad0ae0d523cbff0e5655f4bc15eb33406c81212f54dc9c1"},"emulator":"beetle_saturn","error":null,"files_opened":[{"created":false,"entry":"files:sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"}],"findings":[],"id":"2026-10-27-bcbd05911326","install":"installer","platform":"batocera","platform_system":"sega-saturn","platform_version":"43.1","probed_at":"2026-10-27T23:28:01+00:00","records":["2026-10-27-af15e4bf2a6c"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":23},"staged":11,"system":"sega-saturn","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/kronos_libretro.so.zip","file":"kronos_libretro.so","last_modified":"Tue, 27 Oct 2026 06:30:48 GMT","sha256":"ca7d0655cb4283b1749ddd2751450d2ec0fd2ea3a3686bde5c4b626cb99e06ac"},"emulator":"kronos","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-4a878b5f4350","install":"installer","platform":"batocera","platform_system":"sega-saturn","platform_version":"43.1","probed_at":"2026-10-27T23:37:55+00:00","records":["2026-10-27-6cb5e0f338df"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":23},"staged":11,"system":"sega-saturn","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabasanshiro_libretro.so.zip","file":"yabasanshiro_libretro.so","last_modified":"Tue, 27 Oct 2026 06:26:02 GMT","sha256":"d75ae811201373f0b3db69b4258f5d19ee42bfb81231af8abeb6a8389a8417ec"},"emulator":"yabasanshiro","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-c50f17aa847e","install":"installer","platform":"batocera","platform_system":"sega-saturn","platform_version":"43.1","probed_at":"2026-10-27T23:50:03+00:00","records":["2026-10-27-828497f9002d"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/, /Source/, /base/ and 10 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":14725,"path":"install/batocera.json","sha256":"960b377a8e03a9333bb4853e54425a0f889eecf9342c9c1aadebf3601ae27f20"},"pack":{"file":"Batocera_43.1_BIOS_Pack.zip","members":14725,"sha256":"494c82fe9aff94e0ad6bc1cad0e14f00719ca5dccf057293009659572988806a","size":6969317213},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":23},"staged":5,"system":"sega-saturn","target":"x86_64","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_saturn_libretro.so.zip","file":"mednafen_saturn_libretro.so","last_modified":"Tue, 27 Oct 2026 07:34:40 GMT","sha256":"2cef16a3444d45032ad0ae0d523cbff0e5655f4bc15eb33406c81212f54dc9c1"},"emulator":"beetle_saturn","error":null,"files_opened":[{"created":false,"entry":"sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"}],"findings":[],"id":"2026-10-27-804ca236cadc","install":"pack","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:51:45+00:00","records":["2026-10-27-07310c597c37"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251"},"staged":13,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/kronos_libretro.so.zip","file":"kronos_libretro.so","last_modified":"Tue, 27 Oct 2026 06:30:48 GMT","sha256":"ca7d0655cb4283b1749ddd2751450d2ec0fd2ea3a3686bde5c4b626cb99e06ac"},"emulator":"kronos","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-bb15cfe730d7","install":"pack","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:52:42+00:00","records":["2026-10-27-6bd260b246f8"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251"},"staged":11,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabasanshiro_libretro.so.zip","file":"yabasanshiro_libretro.so","last_modified":"Tue, 27 Oct 2026 06:26:02 GMT","sha256":"d75ae811201373f0b3db69b4258f5d19ee42bfb81231af8abeb6a8389a8417ec"},"emulator":"yabasanshiro","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-babce101aafc","install":"pack","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:52:42+00:00","records":["2026-10-27-b92d0c40b7cb"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251"},"staged":7,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} {"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/ymir_libretro.so.zip","file":"ymir_libretro.so","last_modified":"Tue, 27 Oct 2026 05:34:56 GMT","sha256":"237d154fa6ef5caccc1d2899e0a2444947e4391a81bd241803932d1319832712"},"emulator":"ymir","error":null,"files_opened":[{"created":false,"entry":"sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"created":false,"entry":"mpr-17933.bin","path":"mpr-17933.bin","sha1":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3"},{"created":false,"entry":"saturn_bios.bin","path":"saturn_bios.bin","sha1":"2b8cb4f87580683eb4d760e4ed210813d667f0a2"},{"created":false,"entry":"cdb/cdb_rom.bin","path":"cdb/cdb_rom.bin","sha1":"b3c20fbe57cd2eb595e9edac86817e5948dccae4"}],"findings":[],"id":"2026-10-27-19073ee28285","install":"pack","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:52:54+00:00","records":["2026-10-27-e4aee262160e"],"rounds":1,"schema_version":1,"source":{"manifest":null,"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251"},"staged":8,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_saturn_libretro.so.zip","file":"mednafen_saturn_libretro.so","last_modified":"Tue, 27 Oct 2026 07:34:40 GMT","sha256":"2cef16a3444d45032ad0ae0d523cbff0e5655f4bc15eb33406c81212f54dc9c1"},"emulator":"beetle_saturn","error":null,"files_opened":[{"created":false,"entry":"files:sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"}],"findings":[],"id":"2026-10-27-ce9ff2f7f69e","install":"installer","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:54:19+00:00","records":["2026-10-27-dcac17ec3f00"],"rounds":1,"schema_version":1,"source":{"installer_summary":".claude/worktrees/agent-af6705283a8fc4932/tmp/pack_probe/retrobat/installer/system/, /base/, /devilutionx/ and 1 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":5561,"path":"install/retrobat.json","sha256":"ef2c453bbd23f10a19322b9b31f0983bfc3cdc36bcea5442f88f42b5a2eb9608"},"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":41},"staged":13,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/kronos_libretro.so.zip","file":"kronos_libretro.so","last_modified":"Tue, 27 Oct 2026 06:30:48 GMT","sha256":"ca7d0655cb4283b1749ddd2751450d2ec0fd2ea3a3686bde5c4b626cb99e06ac"},"emulator":"kronos","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-3c840e36bf04","install":"installer","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:54:19+00:00","records":["2026-10-27-41b7958405f2"],"rounds":1,"schema_version":1,"source":{"installer_summary":".claude/worktrees/agent-af6705283a8fc4932/tmp/pack_probe/retrobat/installer/system/, /base/, /devilutionx/ and 1 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":5561,"path":"install/retrobat.json","sha256":"ef2c453bbd23f10a19322b9b31f0983bfc3cdc36bcea5442f88f42b5a2eb9608"},"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":41},"staged":11,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabasanshiro_libretro.so.zip","file":"yabasanshiro_libretro.so","last_modified":"Tue, 27 Oct 2026 06:26:02 GMT","sha256":"d75ae811201373f0b3db69b4258f5d19ee42bfb81231af8abeb6a8389a8417ec"},"emulator":"yabasanshiro","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-90f89c3310ca","install":"installer","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:54:20+00:00","records":["2026-10-27-b25e77568729"],"rounds":1,"schema_version":1,"source":{"installer_summary":".claude/worktrees/agent-af6705283a8fc4932/tmp/pack_probe/retrobat/installer/system/, /base/, /devilutionx/ and 1 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":5561,"path":"install/retrobat.json","sha256":"ef2c453bbd23f10a19322b9b31f0983bfc3cdc36bcea5442f88f42b5a2eb9608"},"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":41},"staged":7,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} -{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/ymir_libretro.so.zip","file":"ymir_libretro.so","last_modified":"Tue, 27 Oct 2026 05:34:56 GMT","sha256":"237d154fa6ef5caccc1d2899e0a2444947e4391a81bd241803932d1319832712"},"emulator":"ymir","error":null,"files_opened":[{"created":false,"entry":"files:sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"created":false,"entry":"files:mpr-17933.bin","path":"mpr-17933.bin","sha1":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3"},{"created":false,"entry":"files:saturn_bios.bin","path":"saturn_bios.bin","sha1":"2b8cb4f87580683eb4d760e4ed210813d667f0a2"},{"created":false,"entry":"files:cdb/cdb_rom.bin","path":"cdb/cdb_rom.bin","sha1":"b3c20fbe57cd2eb595e9edac86817e5948dccae4"}],"findings":[],"id":"2026-10-27-373980ad54bf","install":"installer","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:54:32+00:00","records":["2026-10-27-9b443640216e"],"rounds":1,"schema_version":1,"source":{"installer_summary":".claude/worktrees/agent-af6705283a8fc4932/tmp/pack_probe/retrobat/installer/system/, /base/, /devilutionx/ and 1 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":5561,"path":"install/retrobat.json","sha256":"ef2c453bbd23f10a19322b9b31f0983bfc3cdc36bcea5442f88f42b5a2eb9608"},"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":41},"staged":8,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/mednafen_saturn_libretro.so.zip","file":"mednafen_saturn_libretro.so","last_modified":"Tue, 27 Oct 2026 07:34:40 GMT","sha256":"2cef16a3444d45032ad0ae0d523cbff0e5655f4bc15eb33406c81212f54dc9c1"},"emulator":"beetle_saturn","error":null,"files_opened":[{"created":false,"entry":"files:sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"}],"findings":[],"id":"2026-10-27-38228b00f155","install":"installer","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:54:19+00:00","records":["2026-10-27-dcac17ec3f00"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/, /base/, /devilutionx/ and 1 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":5561,"path":"install/retrobat.json","sha256":"ef2c453bbd23f10a19322b9b31f0983bfc3cdc36bcea5442f88f42b5a2eb9608"},"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":41},"staged":13,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/kronos_libretro.so.zip","file":"kronos_libretro.so","last_modified":"Tue, 27 Oct 2026 06:30:48 GMT","sha256":"ca7d0655cb4283b1749ddd2751450d2ec0fd2ea3a3686bde5c4b626cb99e06ac"},"emulator":"kronos","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-c59960b4781f","install":"installer","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:54:19+00:00","records":["2026-10-27-41b7958405f2"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/, /base/, /devilutionx/ and 1 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":5561,"path":"install/retrobat.json","sha256":"ef2c453bbd23f10a19322b9b31f0983bfc3cdc36bcea5442f88f42b5a2eb9608"},"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":41},"staged":11,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/yabasanshiro_libretro.so.zip","file":"yabasanshiro_libretro.so","last_modified":"Tue, 27 Oct 2026 06:26:02 GMT","sha256":"d75ae811201373f0b3db69b4258f5d19ee42bfb81231af8abeb6a8389a8417ec"},"emulator":"yabasanshiro","error":null,"files_opened":[],"findings":[],"id":"2026-10-27-52334b9682db","install":"installer","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:54:20+00:00","records":["2026-10-27-b25e77568729"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/, /base/, /devilutionx/ and 1 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":5561,"path":"install/retrobat.json","sha256":"ef2c453bbd23f10a19322b9b31f0983bfc3cdc36bcea5442f88f42b5a2eb9608"},"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":41},"staged":7,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: retro_load_game returned false and no error pattern matched","repetitions":["unknown"],"rule":"repetitions_agree"}} +{"content":{"catalog":"redump","label":"Lunar - Silver Star Story Complete (Japan)","region":"japan"},"core":{"artifact":"https://buildbot.libretro.com/nightly/linux/x86_64/latest/ymir_libretro.so.zip","file":"ymir_libretro.so","last_modified":"Tue, 27 Oct 2026 05:34:56 GMT","sha256":"237d154fa6ef5caccc1d2899e0a2444947e4391a81bd241803932d1319832712"},"emulator":"ymir","error":null,"files_opened":[{"created":false,"entry":"files:sega_101.bin","path":"sega_101.bin","sha1":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"created":false,"entry":"files:mpr-17933.bin","path":"mpr-17933.bin","sha1":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3"},{"created":false,"entry":"files:saturn_bios.bin","path":"saturn_bios.bin","sha1":"2b8cb4f87580683eb4d760e4ed210813d667f0a2"},{"created":false,"entry":"files:cdb/cdb_rom.bin","path":"cdb/cdb_rom.bin","sha1":"b3c20fbe57cd2eb595e9edac86817e5948dccae4"}],"findings":[],"id":"2026-10-27-3b4ca79d1253","install":"installer","platform":"retrobat","platform_system":"sega-saturn","platform_version":"8.3.1","probed_at":"2026-10-27T23:54:32+00:00","records":["2026-10-27-9b443640216e"],"rounds":1,"schema_version":1,"source":{"installer_summary":"/, /base/, /devilutionx/ and 1 more folders. To check this later, run the same command with --check.","manifest":{"generated":"2026-10-27T10:27:58Z","pack_files":5561,"path":"install/retrobat.json","sha256":"ef2c453bbd23f10a19322b9b31f0983bfc3cdc36bcea5442f88f42b5a2eb9608"},"pack":{"file":"RetroBat_8.3.1_BIOS_Pack.zip","members":5561,"sha256":"89bdc445768e9568bd88737871f1e713df3d08fe0e191de18d228291d6e40647","size":5644690738},"repo_commit":"83712af0cea136bfc15a7088c06c27af028d3251","save_dir_files":41},"staged":8,"system":"sega-saturn","target":"windows","verdict":"unknown","verdict_evidence":{"detail":"1 of 1 repetitions: no reference frame is known for this content on this core","repetitions":["unknown"],"rule":"repetitions_agree"}} diff --git a/mypy-baseline.txt b/mypy-baseline.txt new file mode 100644 index 000000000..6aed090c2 --- /dev/null +++ b/mypy-baseline.txt @@ -0,0 +1,974 @@ +install.py:0: error: "Invocation" has no attribute "__iter__" (not iterable) [attr-defined] +install.py:0: error: "None" object is not iterable [misc] +install.py:0: error: "OSError" has no attribute "attempts" [attr-defined] +install.py:0: error: "_Unclosed" has no attribute "name" [attr-defined] +install.py:0: error: "bool" has no attribute "name" [attr-defined] +install.py:0: error: "bool" has no attribute "parent" [attr-defined] +install.py:0: error: "object" has no attribute "get" [attr-defined] +install.py:0: error: "object" has no attribute "get" [attr-defined] +install.py:0: error: "object" has no attribute "rstrip" [attr-defined] +install.py:0: error: "object" object is not iterable [misc] +install.py:0: error: "object" object is not iterable [misc] +install.py:0: error: "str" has no attribute "append" [attr-defined] +install.py:0: error: "str" has no attribute "append" [attr-defined] +install.py:0: error: Argument 1 to "Path" has incompatible type "object"; expected "Union[str, PathLike[str]]" [arg-type] +install.py:0: error: Argument 1 to "Path" has incompatible type "object"; expected "Union[str, PathLike[str]]" [arg-type] +install.py:0: error: Argument 1 to "_ZippedLookup" has incompatible type "Optional[FolderIndex]"; expected "FolderIndex" [arg-type] +install.py:0: error: Argument 1 to "__iadd__" of "list" has incompatible type "Invocation"; expected "Iterable[str]" [arg-type] +install.py:0: error: Argument 1 to "_digest_file" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "_folder_writable" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "_listed_names" has incompatible type "bool"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "_nearest_folder" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "_replace_released" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "append" of "list" has incompatible type "Optional[CardApp]"; expected "CardApp" [arg-type] +install.py:0: error: Argument 1 to "append" of "list" has incompatible type "Union[TuiKey, TuiMouse]"; expected "TuiKey" [arg-type] +install.py:0: error: Argument 1 to "cannot_write_words" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "chmod" has incompatible type "Optional[Path]"; expected "Union[int, Union[str, bytes, PathLike[str], PathLike[bytes]]]" [arg-type] +install.py:0: error: Argument 1 to "copy_source_refusal" has incompatible type "object"; expected "Optional[Path]" [arg-type] +install.py:0: error: Argument 1 to "first_holding" of "FolderIndex" has incompatible type "set[Optional[Any]]"; expected "set[int]" [arg-type] +install.py:0: error: Argument 1 to "len" has incompatible type "Optional[Any]"; expected "Sized" [arg-type] +install.py:0: error: Argument 1 to "len" has incompatible type "Optional[Any]"; expected "Sized" [arg-type] +install.py:0: error: Argument 1 to "len" has incompatible type "Optional[Any]"; expected "Sized" [arg-type] +install.py:0: error: Argument 1 to "len" has incompatible type "Optional[list[dict[Any, Any]]]"; expected "Sized" [arg-type] +install.py:0: error: Argument 1 to "remove" of "list" has incompatible type "int"; expected "FolderIndex" [arg-type] +install.py:0: error: Argument 1 to "replace" has incompatible type "Optional[Path]"; expected "Union[str, bytes, PathLike[str], PathLike[bytes]]" [arg-type] +install.py:0: error: Argument 1 to "setdefault" of "MutableMapping" has incompatible type "Optional[Any]"; expected "str" [arg-type] +install.py:0: error: Argument 1 to "settimeout" of "socket" has incompatible type "object"; expected "Optional[float]" [arg-type] +install.py:0: error: Argument 1 to "shown_path" has incompatible type "Optional[Path]"; expected "Union[Path, str]" [arg-type] +install.py:0: error: Argument 1 to "shown_path" has incompatible type "Optional[Path]"; expected "Union[Path, str]" [arg-type] +install.py:0: error: Argument 1 to "shown_path" has incompatible type "Optional[Path]"; expected "Union[Path, str]" [arg-type] +install.py:0: error: Argument 1 to "shown_path" has incompatible type "Optional[Path]"; expected "Union[Path, str]" [arg-type] +install.py:0: error: Argument 2 to "_browse_to" has incompatible type "object"; expected "Optional[Path]" [arg-type] +install.py:0: error: Argument 2 to "_browse_to" has incompatible type "object"; expected "Optional[Path]" [arg-type] +install.py:0: error: Argument 2 to "_choose_card" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 2 to "_choose_card" has incompatible type "object"; expected "Path" [arg-type] +install.py:0: error: Argument 2 to "_found_elsewhere" has incompatible type "Optional[FolderIndex]"; expected "FolderIndex" [arg-type] +install.py:0: error: Argument 2 to "_setup_label" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 2 to "_toggle_region" has incompatible type "object"; expected "str" [arg-type] +install.py:0: error: Argument 2 to "core_dirs" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 2 to "detected_os" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 2 to "first_holding" of "FolderIndex" has incompatible type "set[Optional[Any]]"; expected "set[str]" [arg-type] +install.py:0: error: Argument 2 to "from_bytes" of "int" has incompatible type "str"; expected "Literal['little', 'big']" [arg-type] +install.py:0: error: Argument 2 to "insert" of "list" has incompatible type "str"; expected "TuiLine" [arg-type] +install.py:0: error: Argument 2 to "insert" of "list" has incompatible type "str"; expected "TuiLine" [arg-type] +install.py:0: error: Argument 2 to "insert" of "list" has incompatible type "str"; expected "TuiLine" [arg-type] +install.py:0: error: Argument 2 to "insert" of "list" has incompatible type "str"; expected "TuiLine" [arg-type] +install.py:0: error: Argument 2 to "insert" of "list" has incompatible type "str"; expected "TuiLine" [arg-type] +install.py:0: error: Argument 3 to "CardSetup" has incompatible type "Optional[str]"; expected "str" [arg-type] +install.py:0: error: Argument 3 to "_choose" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 3 to "_choose" has incompatible type "object"; expected "Path" [arg-type] +install.py:0: error: Argument 3 to "_misplaced" has incompatible type "Optional[FolderIndex]"; expected "FolderIndex" [arg-type] +install.py:0: error: Argument 3 to "_wrong_contents" has incompatible type "Optional[FolderIndex]"; expected "FolderIndex" [arg-type] +install.py:0: error: Argument 3 to "create_connection" has incompatible type "object"; expected "Optional[Union[tuple[Any, ...], str, Buffer]]" [arg-type] +install.py:0: error: Argument 3 to "keep_regions" has incompatible type "list[str]"; expected "frozenset[Any]" [arg-type] +install.py:0: error: Argument 4 to "_write_verified" has incompatible type "Callable[[], IO[bytes]]"; expected "Callable[[], ContextManager[BinaryIO]]" [arg-type] +install.py:0: error: Argument 6 to "_compose" has incompatible type "tuple[()]"; expected "list[Any]" [arg-type] +install.py:0: error: Argument 6 to "_compose" has incompatible type "tuple[()]"; expected "list[Any]" [arg-type] +install.py:0: error: Argument 6 to "_compose" has incompatible type "tuple[()]"; expected "list[Any]" [arg-type] +install.py:0: error: Argument 7 to "_install_data_member" has incompatible type "object"; expected "list[tuple[str, str]]" [arg-type] +install.py:0: error: Cannot determine type of "cores" [has-type] +install.py:0: error: Cannot determine type of "mount" [has-type] +install.py:0: error: Cannot determine type of "mount" [has-type] +install.py:0: error: Cannot determine type of "plat" [has-type] +install.py:0: error: Cannot determine type of "plat" [has-type] +install.py:0: error: Cannot determine type of "plat" [has-type] +install.py:0: error: Cannot determine type of "regions" [has-type] +install.py:0: error: Cannot determine type of "system" [has-type] +install.py:0: error: Cannot determine type of "system" [has-type] +install.py:0: error: Cannot determine type of "system" [has-type] +install.py:0: error: Cannot determine type of "systems" [has-type] +install.py:0: error: Cannot infer type of lambda [misc] +install.py:0: error: Cannot infer type of lambda [misc] +install.py:0: error: Cannot infer type of lambda [misc] +install.py:0: error: Cannot infer type of lambda [misc] +install.py:0: error: Generator has incompatible item type "int"; expected "bool" [misc] +install.py:0: error: Incompatible default for argument "after" (default has type "tuple[()]", argument has type "list[Any]") [assignment] +install.py:0: error: Incompatible default for argument "copied" (default has type "tuple[()]", argument has type "list[str]") [assignment] +install.py:0: error: Incompatible default for argument "cores" (default has type "tuple[()]", argument has type "list[str]") [assignment] +install.py:0: error: Incompatible default for argument "footer" (default has type "tuple[()]", argument has type "list[Any]") [assignment] +install.py:0: error: Incompatible default for argument "omitted" (default has type "tuple[()]", argument has type "list[dict[Any, Any]]") [assignment] +install.py:0: error: Incompatible default for argument "pinned" (default has type "tuple[()]", argument has type "list[Any]") [assignment] +install.py:0: error: Incompatible default for argument "regions" (default has type "tuple[()]", argument has type "list[str]") [assignment] +install.py:0: error: Incompatible default for argument "systems" (default has type "tuple[()]", argument has type "list[str]") [assignment] +install.py:0: error: Incompatible default for argument "trees" (default has type "tuple[()]", argument has type "list[tuple[dict[Any, Any], dict[str, dict[Any, Any]]]]") [assignment] +install.py:0: error: Incompatible return value type (got "IO[bytes]", expected "ContextManager[BinaryIO]") [return-value] +install.py:0: error: Incompatible return value type (got "Optional[Path]", expected "Path") [return-value] +install.py:0: error: Incompatible return value type (got "Union[Any, object]", expected "str") [return-value] +install.py:0: error: Incompatible return value type (got "list[TuiLine]", expected "list[str]") [return-value] +install.py:0: error: Incompatible return value type (got "list[TuiLine]", expected "list[str]") [return-value] +install.py:0: error: Incompatible return value type (got "list[tuple[Optional[str], str]]", expected "list[tuple[str, str]]") [return-value] +install.py:0: error: Incompatible return value type (got "object", expected "dict[Any, Any]") [return-value] +install.py:0: error: Incompatible types in "yield" (actual type "IO[bytes]", expected type "BinaryIO") [misc] +install.py:0: error: Incompatible types in "yield" (actual type "tuple[str, int, Callable[[Any], IO[bytes]]]", expected type "tuple[str, int, Callable[[], BinaryIO]]") [misc] +install.py:0: error: Incompatible types in "yield" (actual type "tuple[str, int, Callable[[Any], Optional[IO[bytes]]]]", expected type "tuple[str, int, Callable[[], BinaryIO]]") [misc] +install.py:0: error: Incompatible types in assignment (expression has type "FolderIndex", base class "tuple" defined the type as "Callable[[Any, SupportsIndex, SupportsIndex], int]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "FolderIndex", base class "tuple" defined the type as "Callable[[Any, SupportsIndex, SupportsIndex], int]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "FolderIndex", variable has type "int") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "GamesChoice", variable has type "Optional[tuple[list[str], list[str], list[str]]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "GamesChoice", variable has type "Optional[tuple[list[str], list[str], list[str]]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "GamesChoice", variable has type "Optional[tuple[list[str], list[str], list[str]]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Optional[Progress]", variable has type "Progress") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Optional[frozenset[Any]]", variable has type "Optional[tuple[int, str]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Optional[frozenset[Any]]", variable has type "list[str]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Optional[int]", variable has type "int") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Path", variable has type "bool") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Union[Any, bool]", variable has type "str") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Union[Any, list[Any]]", variable has type "set[str]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Union[tuple[list[str], list[str], list[str]], str, GamesChoice, None]", variable has type "Optional[tuple[list[str], list[str], list[str]]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "_TemporaryFileWrapper[bytes]", variable has type "_Unclosed") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "float", variable has type "Optional[int]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Any, SupportsIndex, SupportsIndex], int]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Any], int]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "list[dict[Any, Any]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "list[dict[Any, Any]]", variable has type "int") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "Invocation") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "str") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "set[int]", variable has type "int") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "set[int]", variable has type "list[dict[Any, Any]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "set[int]", variable has type "list[dict[Any, Any]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "str", target has type "int") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "Optional[Path]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "Optional[tuple[list[str], list[str], list[str]]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "bool") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "tuple[int, str]", variable has type "Optional[frozenset[Any]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "tuple[tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any]]", variable has type "tuple[tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "tuple[tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any]]", variable has type "tuple[tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any]]") [assignment] +install.py:0: error: Item "None" of "Optional[BinaryIO]" has no attribute "truncate" [union-attr] +install.py:0: error: Item "None" of "Optional[Element[str]]" has no attribute "iter" [union-attr] +install.py:0: error: Item "None" of "Optional[PackSource]" has no attribute "name" [union-attr] +install.py:0: error: Item "None" of "Optional[PackSource]" has no attribute "name" [union-attr] +install.py:0: error: Item "None" of "Optional[Path]" has no attribute "__iter__" (not iterable) [union-attr] +install.py:0: error: Item "None" of "Optional[Path]" has no attribute "__iter__" (not iterable) [union-attr] +install.py:0: error: Item "None" of "Optional[Path]" has no attribute "append" [union-attr] +install.py:0: error: Item "None" of "Optional[Path]" has no attribute "read_text" [union-attr] +install.py:0: error: Item "None" of "Optional[Path]" has no attribute "write_text" [union-attr] +install.py:0: error: Item "None" of "Optional[Terminal]" has no attribute "stream" [union-attr] +install.py:0: error: Item "None" of "Optional[Terminal]" has no attribute "stream" [union-attr] +install.py:0: error: Item "None" of "Optional[Terminal]" has no attribute "stream" [union-attr] +install.py:0: error: Item "None" of "Optional[Terminal]" has no attribute "stream" [union-attr] +install.py:0: error: Item "None" of "Optional[TuiHits]" has no attribute "frame" [union-attr] +install.py:0: error: Item "None" of "Optional[TuiHits]" has no attribute "shown" [union-attr] +install.py:0: error: Item "Path" of "Optional[Path]" has no attribute "__iter__" (not iterable) [union-attr] +install.py:0: error: Item "Path" of "Optional[Path]" has no attribute "__iter__" (not iterable) [union-attr] +install.py:0: error: Item "Path" of "Optional[Path]" has no attribute "append" [union-attr] +install.py:0: error: List comprehension has incompatible type List[object]; expected List[str] [misc] +install.py:0: error: List item 0 has incompatible type "int"; expected "FolderIndex" [list-item] +install.py:0: error: List item 0 has incompatible type "tuple[Optional[str], str]"; expected "tuple[str, str]" [list-item] +install.py:0: error: List item 0 has incompatible type "tuple[Optional[str], str]"; expected "tuple[str, str]" [list-item] +install.py:0: error: List item 3 has incompatible type "tuple[Optional[str], str]"; expected "tuple[str, str]" [list-item] +install.py:0: error: Module has no attribute "HKEY_CURRENT_USER" [attr-defined] +install.py:0: error: Module has no attribute "HKEY_LOCAL_MACHINE" [attr-defined] +install.py:0: error: Module has no attribute "LK_NBLCK" [attr-defined] +install.py:0: error: Module has no attribute "OpenKey" [attr-defined] +install.py:0: error: Module has no attribute "QueryValueEx" [attr-defined] +install.py:0: error: Module has no attribute "locking" [attr-defined] +install.py:0: error: Name "found" already defined on line 0 [no-redef] +install.py:0: error: Name "top" already defined on line 0 [no-redef] +install.py:0: error: Need type annotation for "removed" (hint: "removed: list[] = ...") [var-annotated] +install.py:0: error: Need type annotation for "words" (hint: "words: list[] = ...") [var-annotated] +install.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "object" [call-overload] +install.py:0: error: No overload variant of "min" matches argument types "float", "object" [call-overload] +install.py:0: error: Unsupported left operand type for + ("None") [operator] +install.py:0: error: Unsupported left operand type for + ("None") [operator] +install.py:0: error: Unsupported left operand type for + ("None") [operator] +install.py:0: error: Unsupported left operand type for < ("object") [operator] +install.py:0: error: Unsupported operand types for + ("object" and "int") [operator] +install.py:0: error: Unsupported operand types for + ("object" and "int") [operator] +install.py:0: error: Unsupported operand types for / ("Path" and "None") [operator] +install.py:0: error: Unsupported operand types for / ("Path" and "None") [operator] +install.py:0: error: Unsupported operand types for > ("str" and "int") [operator] +install.py:0: error: Unsupported operand types for in ("object" and "str") [operator] +install.py:0: error: Unsupported right operand type for in ("int") [operator] +install.py:0: error: Value of type "Optional[Match[str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[Match[str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[dict[Any, Any]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "object" is not indexable [index] +install.py:0: error: Value of type "object" is not indexable [index] +install.py:0: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "object" [type-var] +install.py:0: error: Value of type variable "SupportsRichComparisonT" of "sorted" cannot be "Optional[Any]" [type-var] +scripts/campaign.py:0: error: "object" has no attribute "__enter__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "__enter__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "__enter__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "__exit__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "__exit__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "__exit__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "decode" [attr-defined] +scripts/campaign.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "bytes") [assignment] +scripts/campaign.py:0: error: Invalid index type "bytes" for "dict[str, str]"; expected type "str" [index] +scripts/campaign.py:0: error: Item "None" of "Optional[dict[Any, Any]]" has no attribute "get" [union-attr] +scripts/campaign.py:0: error: No overload variant of "Popen" matches argument types "list[str]", "bool", "dict[str, object]" [call-overload] +scripts/campaign.py:0: error: Unpacked dict entry 0 has incompatible type "object"; expected "SupportsKeysAndGetItem[Any, Any]" [dict-item] +scripts/campaign.py:0: error: Unsupported operand types for / ("Path" and "bytes") [operator] +scripts/campaign.py:0: error: Value of type "object" is not indexable [index] +scripts/campaign.py:0: error: Value of type "object" is not indexable [index] +scripts/campaign_profiles.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] +scripts/campaign_profiles.py:0: error: Incompatible types in assignment (expression has type "tuple[str, int]", target has type "tuple[int, int]") [assignment] +scripts/campaign_profiles.py:0: error: Unsupported operand types for + ("str" and "int") [operator] +scripts/campaign_rules.py:0: error: "object" not callable [operator] +scripts/campaign_rules.py:0: error: Argument 1 to "get" of "dict" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/campaign_rules.py:0: error: No overload variant of "set" matches argument type "object" [call-overload] +scripts/check_freshness.py:0: error: Argument 2 to "unresolved_target_cores" has incompatible type "dict[str, set[str]]"; expected "dict[str, str]" [arg-type] +scripts/check_freshness.py:0: error: Argument 2 to "unresolved_target_cores" has incompatible type "dict[str, set[str]]"; expected "dict[str, str]" [arg-type] +scripts/check_freshness.py:0: error: Argument 2 to "unresolved_target_cores" has incompatible type "dict[str, set[str]]"; expected "dict[str, str]" [arg-type] +scripts/check_freshness.py:0: error: Argument 2 to "unresolved_target_cores" has incompatible type "dict[str, set[str]]"; expected "dict[str, str]" [arg-type] +scripts/check_freshness.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/check_freshness.py:0: error: Need type annotation for "overrides" (hint: "overrides: dict[, ] = ...") [var-annotated] +scripts/check_platform_folder.py:0: error: Argument 1 to "exit" has incompatible type "Union[str, HTTPError]"; expected "Union[str, int, None]" [arg-type] +scripts/check_platform_folder.py:0: error: Argument 1 to "read" of "ZipFile" has incompatible type "Optional[str]"; expected "Union[str, ZipInfo]" [arg-type] +scripts/check_platform_folder.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "dict[str, str]") [assignment] +scripts/check_release_assets.py:0: error: Argument 1 to "get" of "dict" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/common.py:0: error: Argument "sha1_candidates" to "declared_hash_verdict" has incompatible type "list[str]"; expected "set[Any]" [arg-type] +scripts/common.py:0: error: Argument "sha256_candidates" to "declared_hash_verdict" has incompatible type "list[str]"; expected "set[Any]" [arg-type] +scripts/common.py:0: error: Argument 1 to "append" of "list" has incompatible type "tuple[Any, Any, Any]"; expected "str" [arg-type] +scripts/common.py:0: error: Argument 2 to "name_match_size_ok" has incompatible type "str"; expected "Optional[int]" [arg-type] +scripts/common.py:0: error: Cannot determine type of "m" [has-type] +scripts/common.py:0: error: Cannot determine type of "size" [has-type] +scripts/common.py:0: error: Incompatible return value type (got "str", expected "tuple[Optional[str], str]") [return-value] +scripts/common.py:0: error: Incompatible return value type (got "str", expected "tuple[Optional[str], str]") [return-value] +scripts/common.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[str, str]]", variable has type "Optional[str]") [assignment] +scripts/common.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[str, str]]", variable has type "Optional[str]") [assignment] +scripts/common.py:0: error: Name "folded" already defined on line 0 [no-redef] +scripts/common.py:0: error: Unpacking a string is disallowed [misc] +scripts/common.py:0: error: Unpacking a string is disallowed [misc] +scripts/common.py:0: error: Unsupported left operand type for & ("None") [operator] +scripts/core_probe.py:0: error: Incompatible types in assignment (expression has type "list[list[Any]]", target has type "Optional[str]") [assignment] +scripts/core_probe.py:0: error: Incompatible types in assignment (expression has type "list[str]", target has type "Optional[str]") [assignment] +scripts/core_probe.py:0: error: Item "None" of "Optional[Path]" has no attribute "name" [union-attr] +scripts/core_probe.py:0: error: Item "None" of "Optional[Path]" has no attribute "open" [union-attr] +scripts/core_probe.py:0: error: Need type annotation for "info" [var-annotated] +scripts/core_probe.py:0: error: Value of type "Optional[Any]" is not indexable [index] +scripts/cross_reference.py:0: error: Need type annotation for "declared" (hint: "declared: dict[, ] = ...") [var-annotated] +scripts/cross_reference.py:0: error: Need type annotation for "platform_data_dirs" (hint: "platform_data_dirs: dict[, ] = ...") [var-annotated] +scripts/cross_reference.py:0: error: Need type annotation for "report" (hint: "report: dict[, ] = ...") [var-annotated] +scripts/dedup.py:0: error: "Sequence[str]" has no attribute "append" [attr-defined] +scripts/derivations.py:0: error: Argument 1 to "_gf2_times" has incompatible type "tuple[int, ...]"; expected "list[int]" [arg-type] +scripts/derivations.py:0: error: Argument 1 to "_gf2_times" has incompatible type "tuple[int, ...]"; expected "list[int]" [arg-type] +scripts/derivations.py:0: error: Argument 1 to "get" of "dict" has incompatible type "tuple[Optional[int], str]"; expected "tuple[int, str]" [arg-type] +scripts/derivations.py:0: error: Incompatible types in assignment (expression has type "Optional[ZipFile]", variable has type "ZipFile") [assignment] +scripts/dumpcatalog.py:0: error: Need type annotation for "snapshots" (hint: "snapshots: dict[, ] = ...") [var-annotated] +scripts/export_native.py:0: error: "object" has no attribute "carries" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "clears" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "component_url" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "destination" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "fills" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "lands" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "name" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "names_anew" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "native_sources" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "native_system" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "platform" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "platform" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "platform_name" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "required_value" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "required_value" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "required_value" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "size" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "size" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "states" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "states" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "states" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "states" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "states" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "written" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "written_list" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "written_list" [attr-defined] +scripts/export_native.py:0: error: Argument 1 to "_read_archive" has incompatible type "Optional[Any]"; expected "Callable[[bytes], dict[str, str]]" [arg-type] +scripts/export_native.py:0: error: Argument 1 to "_read_archive" has incompatible type "Optional[Any]"; expected "Callable[[bytes], dict[str, str]]" [arg-type] +scripts/exporter/baseline.py:0: error: Incompatible types in assignment (expression has type "Optional[int]", variable has type "int") [assignment] +scripts/exporter/baseline.py:0: error: No overload variant of "int" matches argument type "object" [call-overload] +scripts/exporter/batocera_exporter.py:0: error: Argument 1 to "append" of "list" has incompatible type "tuple[tuple[Any, ...], str]"; expected "tuple[tuple[Any, ...], dict[Any, Any]]" [arg-type] +scripts/exporter/batocera_exporter.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[str, Optional[str]]]", target has type "Optional[tuple[str, str]]") [assignment] +scripts/exporter/batocera_exporter.py:0: error: Incompatible types in assignment (expression has type "dict[Any, Any]", variable has type "str") [assignment] +scripts/exporter/batocera_exporter.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/exporter/batocera_exporter.py:0: error: Item "None" of "Union[Any, dict[Any, Any], None]" has no attribute "get" [union-attr] +scripts/exporter/batocera_exporter.py:0: error: Signature of "writable" incompatible with supertype "scripts.exporter.base_exporter.BaseExporter" [override] +scripts/exporter/misterfpga_exporter.py:0: error: "object" has no attribute "size" [attr-defined] +scripts/exporter/misterfpga_exporter.py:0: error: Argument 1 to "written" of "Exporter" has incompatible type "object"; expected "NativeFile" [arg-type] +scripts/exporter/misterfpga_exporter.py:0: error: Argument 1 to "written" of "Exporter" has incompatible type "object"; expected "NativeFile" [arg-type] +scripts/exporter/misterfpga_exporter.py:0: error: Return type "dict[str, Union[str, bytes]]" of "render" incompatible with return type "dict[str, str]" in supertype "scripts.exporter.base_exporter.BaseExporter" [override] +scripts/exporter/retrodeck_exporter.py:0: error: Argument 1 to "add" of "set" has incompatible type "tuple[str, str]"; expected "tuple[str, str, tuple[str, ...]]" [arg-type] +scripts/exporter/retrodeck_exporter.py:0: error: Incompatible types in assignment (expression has type "dict[Any, Any]", variable has type "OrderedDict[Any, Any]") [assignment] +scripts/exporter/retrodeck_exporter.py:0: error: Incompatible types in assignment (expression has type "dict[Any, Any]", variable has type "OrderedDict[Any, Any]") [assignment] +scripts/exporter/retrodeck_exporter.py:0: error: Incompatible types in assignment (expression has type "tuple[str, str, tuple[str, ...]]", variable has type "tuple[str, str]") [assignment] +scripts/exporter/retropie_exporter.py:0: error: "object" has no attribute "__iter__"; maybe "__dir__" or "__str__"? (not iterable) [attr-defined] +scripts/exporter/retropie_exporter.py:0: error: "object" has no attribute "__iter__"; maybe "__dir__" or "__str__"? (not iterable) [attr-defined] +scripts/exporter/retropie_exporter.py:0: error: Argument 2 to "_extended" of "Exporter" has incompatible type "Optional[tuple[int, int, list[str]]]"; expected "tuple[int, int, list[str]]" [arg-type] +scripts/exporter/retropie_exporter.py:0: error: Expected iterable as variadic argument [misc] +scripts/exporter/retropie_exporter.py:0: error: Value of type "Optional[tuple[int, int, list[str]]]" is not indexable [index] +scripts/exporter/retropie_exporter.py:0: error: Value of type "Optional[tuple[int, int, list[str]]]" is not indexable [index] +scripts/exporter/retropie_exporter.py:0: error: Value of type "Optional[tuple[int, int, list[str]]]" is not indexable [index] +scripts/exporter/romm_exporter.py:0: error: Signature of "writable" incompatible with supertype "scripts.exporter.base_exporter.BaseExporter" [override] +scripts/game_rules.py:0: error: Item "None" of "Optional[dict[str, dict[Any, Any]]]" has no attribute "get" [union-attr] +scripts/generate_db.py:0: error: Name "matched" already defined on line 0 [no-redef] +scripts/generate_db.py:0: error: Need type annotation for "aliases" (hint: "aliases: dict[, ] = ...") [var-annotated] +scripts/generate_db.py:0: error: Need type annotation for "aliases" (hint: "aliases: dict[, ] = ...") [var-annotated] +scripts/generate_db.py:0: error: Need type annotation for "by_name" (hint: "by_name: dict[, ] = ...") [var-annotated] +scripts/generate_db.py:0: error: Need type annotation for "by_path_suffix" (hint: "by_path_suffix: dict[, ] = ...") [var-annotated] +scripts/generate_db.py:0: error: Need type annotation for "files" (hint: "files: dict[, ] = ...") [var-annotated] +scripts/generate_db.py:0: error: Unsupported left operand type for | ("None") [operator] +scripts/generate_event_schema.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] +scripts/generate_event_schema.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] +scripts/generate_event_schema.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] +scripts/generate_games_index.py:0: error: Argument 1 to "fallback_set" has incompatible type "Optional[dict[Any, Any]]"; expected "dict[Any, Any]" [arg-type] +scripts/generate_installer_doc.py:0: error: "object" has no attribute "add" [attr-defined] +scripts/generate_installer_doc.py:0: error: Argument 1 to "__init__" of "SimpleHTTPRequestHandler" has incompatible type "*tuple[object, ...]"; expected "BaseServer" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "__init__" of "SimpleHTTPRequestHandler" has incompatible type "*tuple[object, ...]"; expected "Union[socket, tuple[bytes, socket]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "enter_context" of "_BaseExitStack" has incompatible type "object"; expected "AbstractContextManager[Never, Optional[bool]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "join" of "str" has incompatible type "Union[bool, list[str], str, None]"; expected "Iterable[str]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "sample_writers" has incompatible type "_LazySandbox"; expected "Sandbox" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "startswith" of "str" has incompatible type "Optional[str]"; expected "Union[str, tuple[str, ...]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 2 has incompatible type "*tuple[object, ...]"; expected "Optional[Union[tuple[Any, ...], str, Buffer]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 2 has incompatible type "*tuple[object, ...]"; expected "Optional[float]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 has incompatible type "**dict[str, object]"; expected "Optional[Union[tuple[Any, ...], str, Buffer]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 has incompatible type "**dict[str, object]"; expected "Optional[float]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 to "__init__" of "SimpleHTTPRequestHandler" has incompatible type "**dict[str, object]"; expected "BaseServer" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 to "__init__" of "SimpleHTTPRequestHandler" has incompatible type "**dict[str, object]"; expected "Union[socket, tuple[bytes, socket]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 to "run" of "Sandbox" has incompatible type "**dict[str, bool]"; expected "Optional[Path]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 to "run" of "Sandbox" has incompatible type "**dict[str, bool]"; expected "tuple[Any, ...]" [arg-type] +scripts/generate_installer_doc.py:0: error: Incompatible return value type (got "tuple[int, ...]", expected "tuple[int, int]") [return-value] +scripts/generate_installer_doc.py:0: error: Incompatible types in assignment (expression has type "Union[str, bytes, bytearray]", variable has type "str") [assignment] +scripts/generate_installer_doc.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] +scripts/generate_installer_doc.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] +scripts/generate_installer_doc.py:0: error: Name "_install._OsRule" is not defined [name-defined] +scripts/generate_launcher_index.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] +scripts/generate_launcher_index.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] +scripts/generate_launcher_index.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] +scripts/generate_pack.py:0: error: Argument 1 to "_narrowings" has incompatible type "Union[str, bool]"; expected "str" [arg-type] +scripts/generate_pack.py:0: error: Argument 1 to "_release_asset_name" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[bool]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[dict[Any, Any]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[list[dict[Any, Any]]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[list[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[set[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[str]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "bool" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "str" [arg-type] +scripts/generate_pack.py:0: error: Argument 4 to "_manifest_core_index" has incompatible type "dict_values[str, dict[Any, Any]]"; expected "list[dict[Any, Any]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 4 to "_narrowings" has incompatible type "Union[str, bool]"; expected "bool" [arg-type] +scripts/generate_pack.py:0: error: Argument 5 to "_narrowings" has incompatible type "Union[str, bool]"; expected "bool" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[bool]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[dict[Any, Any]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[dict[str, str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[list[dict[Any, Any]]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[list[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[set[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[str]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "bool" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "str" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[dict[Any, Any]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[list[dict[Any, Any]]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[list[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[set[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "bool" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/generate_pack.py:0: error: Need type annotation for "missing_files" (hint: "missing_files: list[] = ...") [var-annotated] +scripts/generate_pack.py:0: error: Unsupported target for indexed assignment ("Optional[dict[str, PackRecord]]") [index] +scripts/generate_readme.py:0: error: "object" has no attribute "TUI_SYSTEMS_ROW" [attr-defined] +scripts/generate_readme.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] +scripts/generate_readme.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] +scripts/generate_readme.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] +scripts/generate_site.py:0: error: "object" has no attribute "EVERYTHING_IN_PLACE" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_CHOOSE_TITLE" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_FOLDER_TITLE" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_FOUND_TITLE" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_INSTALL_ROW" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_SYSTEMS_ROW" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_SYSTEMS_ROW" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "__iter__"; maybe "__dir__" or "__str__"? (not iterable) [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "emulator_names" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "values" [attr-defined] +scripts/generate_site.py:0: error: "set[str]" has no attribute "get" [attr-defined] +scripts/generate_site.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] +scripts/generate_site.py:0: error: Argument 1 to "setdefault" of "MutableMapping" has incompatible type "tuple[str, str, str]"; expected "tuple[str, str]" [arg-type] +scripts/generate_site.py:0: error: Argument 2 to "_version_said" has incompatible type "Union[Any, dict[Any, Any], None]"; expected "dict[Any, Any]" [arg-type] +scripts/generate_site.py:0: error: Argument 3 to "_blocks_in_runs" has incompatible type "Callable[[list[str]], float]"; expected "Callable[[list[str]], int]" [arg-type] +scripts/generate_site.py:0: error: Argument 3 to "_render_platform_systems" has incompatible type "Optional[dict[Any, Any]]"; expected "dict[Any, Any]" [arg-type] +scripts/generate_site.py:0: error: Argument 4 to "_emulator_part_page" has incompatible type "list[str]"; expected "list[tuple[str, range]]" [arg-type] +scripts/generate_site.py:0: error: Argument 5 to "_card_index" has incompatible type "list[str]"; expected "list[tuple[str, range]]" [arg-type] +scripts/generate_site.py:0: error: Cannot determine type of "entries" [has-type] +scripts/generate_site.py:0: error: Cannot determine type of "entries" [has-type] +scripts/generate_site.py:0: error: Cannot determine type of "entries" [has-type] +scripts/generate_site.py:0: error: Cannot determine type of "stem" [has-type] +scripts/generate_site.py:0: error: Expected iterable as variadic argument [misc] +scripts/generate_site.py:0: error: Generator has incompatible item type "int"; expected "bool" [misc] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Optional[Any]", target has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Union[dict[Any, Any], str, None]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "dict[Any, Any]", variable has type "int") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "dict[str, set[str]]", variable has type "set[str]") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "dict[str, str]", variable has type "dict[str, tuple[str, dict[str, list[Any]]]]") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[dict[str, Sequence[str]]]", target has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[str]", target has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[tuple[str, Any]]", variable has type "Optional[dict[Any, Any]]") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[tuple[str, list[int]]]", variable has type "list[str]") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "tuple[Any, Union[Any, str]]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "tuple[str, dict[str, list[Any]]]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "tuple[str, str]", variable has type "tuple[str, str, str]") [assignment] +scripts/generate_site.py:0: error: Invalid index type "str" for "dict[tuple[str, str], dict[Any, Any]]"; expected type "tuple[str, str]" [index] +scripts/generate_site.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] +scripts/generate_site.py:0: error: Item "None" of "Optional[dict[Any, Any]]" has no attribute "__iter__" (not iterable) [union-attr] +scripts/generate_site.py:0: error: Item "None" of "Optional[str]" has no attribute "startswith" [union-attr] +scripts/generate_site.py:0: error: Item "None" of "Union[Any, dict[Any, Any], None]" has no attribute "get" [union-attr] +scripts/generate_site.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] +scripts/generate_site.py:0: error: Need type annotation for "arcade" (hint: "arcade: list[] = ...") [var-annotated] +scripts/generate_site.py:0: error: Need type annotation for "links" (hint: "links: list[] = ...") [var-annotated] +scripts/generate_site.py:0: error: Need type annotation for "machines" (hint: "machines: list[] = ...") [var-annotated] +scripts/generate_site.py:0: error: Need type annotation for "record" [var-annotated] +scripts/generate_site.py:0: error: Need type annotation for "sample" [var-annotated] +scripts/generate_site.py:0: error: No overload variant of "get" of "dict" matches argument type "str" [call-overload] +scripts/generate_site.py:0: error: Unpacking a string is disallowed [misc] +scripts/generate_site.py:0: error: Value of type "Optional[Match[str]]" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "object" is not indexable [index] +scripts/hashing.py:0: error: No overload variant of "open" matches argument types "object", "str" [call-overload] +scripts/launchers.py:0: error: Argument 1 to "len" has incompatible type "Optional[list[str]]"; expected "Sized" [arg-type] +scripts/launchers.py:0: error: Incompatible types in assignment (expression has type "Optional[dict[Any, Any]]", variable has type "dict[Any, Any]") [assignment] +scripts/launchers.py:0: error: Item "None" of "Optional[list[str]]" has no attribute "__iter__" (not iterable) [union-attr] +scripts/measurement_records.py:0: error: Argument 1 to "add" of "set" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/pack_probe.py:0: error: Need type annotation for "out" (hint: "out: list[] = ...") [var-annotated] +scripts/pack_probe.py:0: error: Need type annotation for "units" (hint: "units: list[] = ...") [var-annotated] +scripts/packextras.py:0: error: Argument 4 to "_emulator_ships" has incompatible type "Optional[dict[Any, Any]]"; expected "dict[Any, Any]" [arg-type] +scripts/packextras.py:0: error: Item "None" of "Optional[list[dict[Any, Any]]]" has no attribute "append" [union-attr] +scripts/packextras.py:0: error: Need type annotation for "profile" [var-annotated] +scripts/packresolve.py:0: error: Incompatible types in assignment (expression has type "Optional[Any]", variable has type "str") [assignment] +scripts/packresolve.py:0: error: Incompatible types in assignment (expression has type "Optional[Any]", variable has type "str") [assignment] +scripts/packverify.py:0: error: Incompatible types in assignment (expression has type "Union[Any, str, None]", variable has type "str") [assignment] +scripts/packverify.py:0: error: Incompatible types in assignment (expression has type "dict[str, str]", variable has type "str") [assignment] +scripts/packverify.py:0: error: Invalid index type "str" for "str"; expected type "Union[SupportsIndex, slice[Any, Any, Any]]" [index] +scripts/packverify.py:0: error: Need type annotation for "errors" (hint: "errors: list[] = ...") [var-annotated] +scripts/packverify.py:0: error: Unsupported target for indexed assignment ("str") [index] +scripts/pipeline.py:0: error: "object" has no attribute "members" [attr-defined] +scripts/pipeline.py:0: error: "object" has no attribute "members" [attr-defined] +scripts/pipeline.py:0: error: "object" has no attribute "sources" [attr-defined] +scripts/pipeline.py:0: error: "object" has no attribute "sources" [attr-defined] +scripts/pipeline.py:0: error: Item "None" of "Optional[IO[str]]" has no attribute "__iter__" (not iterable) [union-attr] +scripts/profile_sync.py:0: error: "list[tuple[int, int, str]]" has no attribute "items" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "name" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "name" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" object is not iterable [misc] +scripts/profile_sync.py:0: error: Argument "entry" to "Citation" has incompatible type "Union[dict[Any, Any], list[Any]]"; expected "Optional[dict[Any, Any]]" [arg-type] +scripts/profile_sync.py:0: error: Argument 1 to "_anchor_tokens" has incompatible type "Optional[dict[Any, Any]]"; expected "dict[Any, Any]" [arg-type] +scripts/profile_sync.py:0: error: Argument 1 to "append" of "list" has incompatible type "object"; expected "str" [arg-type] +scripts/profile_sync.py:0: error: Argument 1 to "entry_hashes" has incompatible type "Optional[dict[Any, Any]]"; expected "dict[Any, Any]" [arg-type] +scripts/profile_sync.py:0: error: Argument 1 to "setdefault" of "MutableMapping" has incompatible type "Optional[int]"; expected "int" [arg-type] +scripts/profile_sync.py:0: error: Argument 1 to "version_tag_candidates" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/profile_sync.py:0: error: Argument 4 to "RepoView" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/profile_sync.py:0: error: Argument 4 to "_set_pin" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/profile_sync.py:0: error: Argument 4 to "_set_pin" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/profile_sync.py:0: error: Cannot determine type of "actual" [has-type] +scripts/profile_sync.py:0: error: Cannot determine type of "old_lines" [has-type] +scripts/profile_sync.py:0: error: Cannot infer type of lambda [misc] +scripts/profile_sync.py:0: error: Cannot infer type of lambda [misc] +scripts/profile_sync.py:0: error: Incompatible return value type (got "object", expected "tuple[Any, list[str]]") [return-value] +scripts/profile_sync.py:0: error: Incompatible types in assignment (expression has type "dict[int, list[Any]]", variable has type "list[tuple[int, int, str]]") [assignment] +scripts/profile_sync.py:0: error: Invalid index type "Optional[int]" for "list[str]"; expected type "SupportsIndex" [index] +scripts/profile_sync.py:0: error: Invalid index type "tuple[str, Optional[int], tuple[Any, ...], str]" for "dict[tuple[str, Optional[int]], tuple[RepoView, str]]"; expected type "tuple[str, Optional[int]]" [index] +scripts/profile_sync.py:0: error: Invalid index type "tuple[str, Optional[int], tuple[Any, ...], str]" for "dict[tuple[str, Optional[int]], tuple[RepoView, str]]"; expected type "tuple[str, Optional[int]]" [index] +scripts/profile_sync.py:0: error: Invalid index type "tuple[str, str, Optional[int], tuple[str, ...], Union[RepoView, Any, None], str]" for "dict[tuple[str, str, Optional[int]], Optional[list[str]]]"; expected type "tuple[str, str, Optional[int]]" [index] +scripts/profile_sync.py:0: error: Invalid index type "tuple[str, str, Optional[int], tuple[str, ...], Union[RepoView, Any, None], str]" for "dict[tuple[str, str, Optional[int]], Optional[list[str]]]"; expected type "tuple[str, str, Optional[int]]" [index] +scripts/profile_sync.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "start" [union-attr] +scripts/profile_sync.py:0: error: Item "None" of "Optional[VersionReport]" has no attribute "declared" [union-attr] +scripts/profile_sync.py:0: error: Item "None" of "Optional[VersionReport]" has no attribute "latest_release" [union-attr] +scripts/profile_sync.py:0: error: Item "None" of "Optional[dict[Any, Any]]" has no attribute "get" [union-attr] +scripts/profile_sync.py:0: error: Item "None" of "Optional[dict[Any, Any]]" has no attribute "get" [union-attr] +scripts/profile_sync.py:0: error: Item "None" of "Optional[str]" has no attribute "startswith" [union-attr] +scripts/profile_sync.py:0: error: List item 0 has incompatible type "Optional[int]"; expected "int" [list-item] +scripts/profile_sync.py:0: error: Unsupported operand types for + ("None" and "int") [operator] +scripts/profile_sync.py:0: error: Unsupported target for indexed assignment ("object") [index] +scripts/profile_sync.py:0: error: Value of type "Optional[tuple[int, int]]" is not indexable [index] +scripts/profile_sync.py:0: error: Value of type "object" is not indexable [index] +scripts/profile_sync.py:0: error: Value of type "object" is not indexable [index] +scripts/provenance_report.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "set[Any]") [assignment] +scripts/provenance_report.py:0: error: List comprehension has incompatible type List[tuple[str, str, Any]]; expected List[tuple[str, str]] [misc] +scripts/refresh_data_dirs.py:0: error: Item "None" of "Optional[IO[bytes]]" has no attribute "__enter__" [union-attr] +scripts/refresh_data_dirs.py:0: error: Item "None" of "Optional[IO[bytes]]" has no attribute "__exit__" [union-attr] +scripts/refresh_stale.py:0: error: Argument "area" to "Job" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/refresh_stale.py:0: error: Argument 1 to "_command_for" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/refresh_stale.py:0: error: Argument 1 to "add" of "set" has incompatible type "tuple[Optional[Any], str]"; expected "tuple[str, str]" [arg-type] +scripts/region.py:0: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "Optional[int]" [type-var] +scripts/region_check.py:0: error: Dict entry 0 has incompatible type "str": "dict[str, Optional[Path]]"; expected "str": "dict[str, Path]" [dict-item] +scripts/region_check.py:0: error: Dict entry 1 has incompatible type "str": "dict[str, Optional[Path]]"; expected "str": "dict[str, Path]" [dict-item] +scripts/region_check.py:0: error: Dict entry 2 has incompatible type "str": "dict[str, Optional[Path]]"; expected "str": "dict[str, Path]" [dict-item] +scripts/safeparse.py:0: error: Invalid base class "base" [misc] +scripts/safeparse.py:0: error: Variable "base" is not valid as a type [valid-type] +scripts/scraper/_hash_merge.py:0: error: Unsupported operand types for + ("None" and "int") [operator] +scripts/scraper/_hash_merge.py:0: error: Unsupported operand types for - ("None" and "int") [operator] +scripts/scraper/arcade_parent_importer.py:0: error: "IO[bytes]" has no attribute "close_archive" [attr-defined] +scripts/scraper/arcade_parent_importer.py:0: error: Argument 1 to "fbneo_sets" has incompatible type "list[Callable[[Any], Any]]"; expected "list[Callable[[], BufferedIOBase]]" [arg-type] +scripts/scraper/arcade_parent_importer.py:0: error: Argument 1 to "read_sets" has incompatible type "list[Callable[[Any], Any]]"; expected "list[Callable[[], BufferedIOBase]]" [arg-type] +scripts/scraper/arcade_parent_importer.py:0: error: Dict entry 2 has incompatible type "str": "dict[str, str]"; expected "str": "str" [dict-item] +scripts/scraper/arcade_parent_importer.py:0: error: Dict entry 2 has incompatible type "str": "dict[str, str]"; expected "str": "str" [dict-item] +scripts/scraper/arcade_parent_importer.py:0: error: Dict entry 2 has incompatible type "str": "dict[str, str]"; expected "str": "str" [dict-item] +scripts/scraper/arcade_parent_importer.py:0: error: Incompatible return value type (got "IO[bytes]", expected "BufferedIOBase") [return-value] +scripts/scraper/arcade_parent_importer.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/scraper/arcade_parent_importer.py:0: error: List comprehension has incompatible type List[Callable[[], BufferedIOBase]]; expected List[Callable[[Any], Any]] [misc] +scripts/scraper/base_scraper.py:0: error: "object" has no attribute "read" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "expr" has no attribute "left" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "stmt" has no attribute "body" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "stmt" has no attribute "body" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: Argument "depends_on_option" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "Optional[str]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument "names" to "Launcher" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "list[str]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument "passes_to" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "Optional[str]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 1 to "join" of "str" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "Iterable[str]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 1 to "ref" of "Source" has incompatible type "list[Union[tuple[int, Optional[int]], tuple[int, int]]]"; expected "list[tuple[int, int]]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 1 to has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 2 to "_string_lists" has incompatible type "Optional[expr]"; expected "AST" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 2 to "_string_lists" has incompatible type "Optional[expr]"; expected "AST" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 3 to "_mentions" has incompatible type "set[Optional[str]]"; expected "set[str]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 3 to "_raise_message" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/batocera_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/batocera_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/batocera_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/batocera_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/batocera_launchers.py:0: error: Incompatible return value type (got "tuple[Union[str, bytes, int, float, complex, ellipsis, None], Union[str, bytes, int, float, complex, ellipsis, None]]", expected "Optional[tuple[str, Any]]") [return-value] +scripts/scraper/batocera_launchers.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/scraper/batocera_launchers.py:0: error: Item "None" of "Optional[expr]" has no attribute "lineno" [union-attr] +scripts/scraper/batocera_launchers.py:0: error: List comprehension has incompatible type List[Optional[str]]; expected List[str] [misc] +scripts/scraper/batocera_launchers.py:0: error: List comprehension has incompatible type List[tuple[int, Optional[int]]]; expected List[tuple[int, int]] [misc] +scripts/scraper/batocera_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: List item 1 has incompatible type "tuple[Union[int, Any], Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: List item 1 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: Need type annotation for "launchers" (hint: "launchers: list[] = ...") [var-annotated] +scripts/scraper/batocera_launchers.py:0: error: Unsupported left operand type for + ("ellipsis") [operator] +scripts/scraper/batocera_launchers.py:0: error: Unsupported operand types for + ("bytes" and "str") [operator] +scripts/scraper/batocera_launchers.py:0: error: Unsupported operand types for + ("complex" and "str") [operator] +scripts/scraper/batocera_launchers.py:0: error: Unsupported operand types for + ("float" and "str") [operator] +scripts/scraper/batocera_launchers.py:0: error: Unsupported operand types for + ("int" and "str") [operator] +scripts/scraper/batocera_launchers.py:0: error: Unsupported operand types for in ("Union[str, bytes, int, float, complex, ellipsis]" and "str") [operator] +scripts/scraper/bizhawk_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/bizhawk_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/bizhawk_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/bizhawk_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/bizhawk_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/configgen_launchers.py:0: error: Incompatible return value type (got "AST", expected "Union[Assign, AnnAssign]") [return-value] +scripts/scraper/coreinfo_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/coreinfo_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/coreinfo_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/coreinfo_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Item "None" of "Optional[str]" has no attribute "split" [union-attr] +scripts/scraper/emudeck_launchers.py:0: error: Name "Launcher" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "LauncherPatternMissing" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "RepoTree" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "Script" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "describe_keys" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "settings" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "source_ref" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "words" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Need type annotation for "records" (hint: "records: list[] = ...") [var-annotated] +scripts/scraper/emudeck_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "RepoTree" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "emulator_of_script" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "extract_launchers" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Need type annotation for "entries" (hint: "entries: list[] = ...") [var-annotated] +scripts/scraper/libretro_scraper.py:0: error: Unpacked dict entry 0 has incompatible type "Collection[str]"; expected "SupportsKeysAndGetItem[Any, Any]" [dict-item] +scripts/scraper/mame_devices.py:0: error: Incompatible types in "yield" (actual type "IO[bytes]", expected type "BufferedIOBase") [misc] +scripts/scraper/mame_hash_scraper.py:0: error: Item "None" of "Optional[dict[str, Any]]" has no attribute "get" [union-attr] +scripts/scraper/minui_scraper.py:0: error: Cannot infer type of lambda [misc] +scripts/scraper/minui_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/minui_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/minui_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/minui_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/minui_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/minui_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "_read_limited" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/myminui_scraper.py:0: error: Name "minui_scraper" already defined (by an import) [no-redef] +scripts/scraper/myminui_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/nextui_scraper.py:0: error: Name "minui_scraper" already defined (by an import) [no-redef] +scripts/scraper/nextui_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "lineno" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "lineno" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "lineno" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "None" object is not iterable [misc] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "args" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "attr" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "comparators" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "elts" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "id" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "values" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "values" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: Argument "emulator" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "emulator" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "fails_when_missing" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "names" to "Launcher" has incompatible type "list[Optional[str]]"; expected "list[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "names" to "Launcher" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "list[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "passes_to" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "slot" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "index" of "list" has incompatible type "Optional[stmt]"; expected "stmt" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "join" has incompatible type "*list[Optional[str]]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "join" of "str" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "Iterable[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "join" of "str" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "Iterable[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "join" of "str" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "Iterable[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "ref" of "Source" has incompatible type "list[tuple[int, Optional[int]]]"; expected "list[tuple[int, int]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "ref" of "Source" has incompatible type "list[tuple[int, Optional[int]]]"; expected "list[tuple[int, int]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "setdefault" of "MutableMapping" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 4 to "record" has incompatible type "list[tuple[int, Optional[int]]]"; expected "list[tuple[int, int]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/recalbox_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/recalbox_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/recalbox_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible return value type (got "Union[str, bytes, int, float, complex, ellipsis, None]", expected "Optional[str]") [return-value] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible return value type (got "dict[str, tuple[list[Union[str, bytes, int, float, complex, ellipsis, None]], AST]]", expected "dict[str, tuple[list[str], stmt]]") [return-value] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible return value type (got "tuple[list[Optional[str]], If]", expected "tuple[list[str], If]") [return-value] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible types in assignment (expression has type "Optional[stmt]", variable has type "stmt") [assignment] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", target has type "str") [assignment] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible types in assignment (expression has type "tuple[Union[str, bytes, int, float, complex, ellipsis, None], Optional[str]]", variable has type "Optional[tuple[str, str]]") [assignment] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[FunctionDef]" has no attribute "args" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[FunctionDef]" has no attribute "name" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[expr]" has no attribute "keys" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[expr]" has no attribute "values" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[list[str]]" has no attribute "__iter__" (not iterable) [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[stmt]" has no attribute "body" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[stmt]" has no attribute "lineno" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[stmt]" has no attribute "lineno" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[stmt]" has no attribute "orelse" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "expr" of "Optional[expr]" has no attribute "keys" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "expr" of "Optional[expr]" has no attribute "values" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "stmt" of "Optional[stmt]" has no attribute "body" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "stmt" of "Optional[stmt]" has no attribute "orelse" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[Any, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 1 has incompatible type "list[Optional[str]]"; expected "str" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 1 has incompatible type "list[tuple[int, Optional[int]]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 1 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 1 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 2 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 2 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 3 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: Module has no attribute "Match" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: Module has no attribute "MatchAs" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: Module has no attribute "MatchOr" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: Module has no attribute "MatchValue" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: Value of type variable "SupportsRichComparisonT" of "max" cannot be "Optional[int]" [type-var] +scripts/scraper/retrobat_launchers.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[str, list[tuple[int, int]]]]", variable has type "str") [assignment] +scripts/scraper/retrobat_launchers.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/scraper/retrobat_scraper.py:0: error: Argument 1 to "urlopen" has incompatible type "BiosRequirement"; expected "Union[str, Request]" [arg-type] +scripts/scraper/retrobat_scraper.py:0: error: Incompatible types in assignment (expression has type "Request", variable has type "BiosRequirement") [assignment] +scripts/scraper/retrobat_scraper.py:0: error: Name "BATOCERA_SLUGS" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "LAUNCHER_FILES" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "extract_launchers" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_launchers.py:0: error: Incompatible types in assignment (expression has type "tuple[str, ...]", variable has type "tuple[str, str, str, str]") [assignment] +scripts/scraper/retrodeck_launchers.py:0: error: Name "Launcher" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_launchers.py:0: error: Name "LauncherPatternMissing" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_launchers.py:0: error: Name "merge_records" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_launchers.py:0: error: Name "source_ref" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "extract_launchers" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_launchers.py:0: error: "int" has no attribute "rstrip" [attr-defined] +scripts/scraper/retropie_launchers.py:0: error: "int" has no attribute "strip" [attr-defined] +scripts/scraper/retropie_launchers.py:0: error: Argument 3 to "Event" has incompatible type "list[str]"; expected "list[list[str]]" [arg-type] +scripts/scraper/retropie_launchers.py:0: error: Argument 3 to "Event" has incompatible type "list[str]"; expected "list[list[str]]" [arg-type] +scripts/scraper/retropie_launchers.py:0: error: Argument 3 to "_walk" of "Scripts" has incompatible type "dict[str, str]"; expected "dict[str, list[str]]" [arg-type] +scripts/scraper/retropie_launchers.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] +scripts/scraper/retropie_launchers.py:0: error: List comprehension has incompatible type List[list[str]]; expected List[str] [misc] +scripts/scraper/retropie_launchers.py:0: error: List comprehension has incompatible type List[list[str]]; expected List[str] [misc] +scripts/scraper/retropie_launchers.py:0: error: List comprehension has incompatible type List[tuple[str, ...]]; expected List[tuple[str, str]] [misc] +scripts/scraper/retropie_launchers.py:0: error: No overload variant of "match" of "Pattern" matches argument type "int" [call-overload] +scripts/scraper/retropie_launchers.py:0: error: No overload variant of "search" of "Pattern" matches argument type "int" [call-overload] +scripts/scraper/retropie_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "SCRIPTMODULES" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "_SCRIPT_SECTIONS" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "extract_launchers" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Argument "search_dir" to "Launcher" has incompatible type "object"; expected "Optional[str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 1 to "get" of "dict" has incompatible type "Union[str, Match[str]]"; expected "str" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 1 to has incompatible type "Optional[int]"; expected "int" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 2 to "next" has incompatible type "None"; expected "Match[str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Generator has incompatible item type "str"; expected "Match[str]" [misc] +scripts/scraper/rocknix_launchers.py:0: error: Name "Launcher" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "LauncherPatternMissing" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "Link" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "RepoTree" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "Script" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "Setting" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "describe_keys" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "settings" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "source_ref" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Argument 1 to "get" of "dict" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/scraper/rocknix_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "RepoTree" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "extract_launchers" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "md5_field" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/romfolders/card.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "str") [assignment] +scripts/scraper/romfolders/card.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "Optional[Match[str]]") [assignment] +scripts/scraper/romfolders/card.py:0: error: No overload variant of "get" of "dict" matches argument types "Optional[Match[str]]", "Optional[Match[str]]" [call-overload] +scripts/scraper/romfolders/es.py:0: error: Argument 2 to "setdefault" of "MutableMapping" has incompatible type "tuple[str, Any, str]"; expected "tuple[str, str, int]" [arg-type] +scripts/scraper/romfolders/es.py:0: error: Incompatible types in assignment (expression has type "Optional[Any]", variable has type "str") [assignment] +scripts/scraper/romfolders/es.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] +scripts/scraper/romfolders/es.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] +scripts/scraper/romfolders/es.py:0: error: List item 0 has incompatible type "None"; expected "dict[Any, Any]" [list-item] +scripts/scraper/romfolders/es.py:0: error: Value of type "Optional[str]" is not indexable [index] +scripts/scraper/romm_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/romm_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/romm_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/romm_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/romm_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/romset_dat_importer.py:0: error: "IO[bytes]" has no attribute "close_archive" [attr-defined] +scripts/scraper/romset_dat_importer.py:0: error: "object" has no attribute "__iter__"; maybe "__dir__" or "__str__"? (not iterable) [attr-defined] +scripts/scraper/romset_dat_importer.py:0: error: Incompatible return value type (got "IO[bytes]", expected "ZipExtFile") [return-value] +scripts/scraper/shell_launchers.py:0: error: Name "LauncherShapeError" already defined (possibly by an import) [no-redef] +scripts/scraper/shell_launchers.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/shell_launchers.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "BUILDS_OF" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "ES_SYSTEMS_TEMPLATE" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "RUNNERS" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "_not_cores" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "es_cores" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "xml_systems" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/batocera_targets_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "BUILDS_OF" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "ES_SYSTEMS_TEMPLATE" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "PORTS_TEMPLATE" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "RUNNERS" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "SystemEmulators" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "_not_cores" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "es_cores" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "port_engines" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "with_ports" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/lakka_targets_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/lakka_targets_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/recalbox_targets_scraper.py:0: error: Name "PlatformScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/recalbox_targets_scraper.py:0: error: Name "SYSTEMS_TREE_URL" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/recalbox_targets_scraper.py:0: error: Name "board_cores" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/recalbox_targets_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/rocknix_targets_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/rocknix_targets_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/rocknix_targets_scraper.py:0: error: Name "standalone_emulators" already defined (possibly by an import) [no-redef] +scripts/sect233r1.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[int, int]]", variable has type "tuple[int, int]") [assignment] +scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] +scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] +scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] +scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] +scripts/sitediagrams.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/sitediagrams.py:0: error: Argument 1 to "lstrip" of "bytes" has incompatible type "str"; expected "Optional[Buffer]" [arg-type] +scripts/sitediagrams.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/sitediagrams.py:0: error: Incompatible types in assignment (expression has type "Union[str, bytes, int, float, complex, ellipsis, Any, None]", variable has type "str") [assignment] +scripts/sitediagrams.py:0: error: Item "None" of "Union[str, bytes, int, float, complex, ellipsis, None]" has no attribute "lstrip" [union-attr] +scripts/sitediagrams.py:0: error: Item "complex" of "Union[str, bytes, int, float, complex, ellipsis, None]" has no attribute "lstrip" [union-attr] +scripts/sitediagrams.py:0: error: Item "ellipsis" of "Union[str, bytes, int, float, complex, ellipsis, None]" has no attribute "lstrip" [union-attr] +scripts/sitediagrams.py:0: error: Item "float" of "Union[str, bytes, int, float, complex, ellipsis, None]" has no attribute "lstrip" [union-attr] +scripts/sitediagrams.py:0: error: Item "int" of "Union[str, bytes, int, float, complex, ellipsis, None]" has no attribute "lstrip" [union-attr] +scripts/siteguides.py:0: error: Argument 1 to "tuple" has incompatible type "list[tuple[str, ...]]"; expected "Iterable[tuple[str, str, str, str, str]]" [arg-type] +scripts/siteguides.py:0: error: Incompatible types in assignment (expression has type "tuple[str, ...]", target has type "tuple[str, str, str]") [assignment] +scripts/sitemeasurements.py:0: error: Argument 1 to "permalink" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/sitemeasurements.py:0: error: Argument 2 to "permalink" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/sitemeasurements.py:0: error: Value of type "Optional[Match[str]]" is not indexable [index] +scripts/siterender.py:0: error: "None" object is not iterable [misc] +scripts/siterender.py:0: error: Argument 1 to "add" of "set" has incompatible type "Optional[tuple[str, str]]"; expected "tuple[str, str]" [arg-type] +scripts/sitestarlight.py:0: error: "object" has no attribute "items" [attr-defined] +scripts/sitestarlight.py:0: error: Argument "key" to "max" has incompatible type overloaded function; expected "Callable[[str], Union[SupportsDunderLT[Any], SupportsDunderGT[Any]]]" [arg-type] +scripts/sitestarlight.py:0: error: Incompatible types in assignment (expression has type "Optional[dict[str, int]]", variable has type "dict[str, int]") [assignment] +scripts/slot.py:0: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "Optional[int]" [type-var] +scripts/sourceref.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/upstream.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/upstream.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/upstream.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/upstream.py:0: error: Value of type "object" is not indexable [index] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "add" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "setdefault" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "setdefault" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "update" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "update" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "update" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "update" [attr-defined] +scripts/validate_pr.py:0: error: "_MutuallyExclusiveGroup" has no attribute "append" [attr-defined] +scripts/validate_pr.py:0: error: "add" of "set" does not return a value (it only ever returns None) [func-returns-value] +scripts/validate_pr.py:0: error: Argument 1 to "_note_sizes" has incompatible type "Collection[Any]"; expected "dict[str, list[Any]]" [arg-type] +scripts/validate_pr.py:0: error: Incompatible types in assignment (expression has type "list[Never]", variable has type "_MutuallyExclusiveGroup") [assignment] +scripts/validate_pr.py:0: error: Incompatible types in assignment (expression has type "set[Any]", variable has type "list[Any]") [assignment] +scripts/validate_pr.py:0: error: List item 0 has incompatible type "None"; expected "int" [list-item] +scripts/validate_pr.py:0: error: Need type annotation for "checks" (hint: "checks: list[] = ...") [var-annotated] +scripts/validate_pr.py:0: error: Need type annotation for "expected" [var-annotated] +scripts/validate_pr.py:0: error: Need type annotation for "known" [var-annotated] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: Argument 1 to "_semantic_database_checks" has incompatible type "object"; expected "dict[Any, Any]" [arg-type] +scripts/validate_schemas.py:0: error: Argument 3 to "_validate_json_files" has incompatible type "Callable[[Path, dict[Any, Any]], list[str]]"; expected "Optional[Callable[[Path, object], list[str]]]" [arg-type] +scripts/validate_schemas.py:0: error: Argument 3 to "_validate_json_files" has incompatible type "Callable[[Path, dict[Any, Any]], list[str]]"; expected "Optional[Callable[[Path, object], list[str]]]" [arg-type] +scripts/validate_schemas.py:0: error: Unpacked dict entry 0 has incompatible type "object"; expected "SupportsKeysAndGetItem[str, str]" [dict-item] +scripts/validation.py:0: error: "dict[str, str]" has no attribute "startswith" [attr-defined] +scripts/validation.py:0: error: Argument 1 to "_merged_reason" has incompatible type "list[Optional[str]]"; expected "list[str]" [arg-type] +scripts/validation.py:0: error: Argument 1 to "set" has incompatible type "Union[dict[Any, Any], list[str], Any, None]"; expected "Iterable[Any]" [arg-type] +scripts/validation.py:0: error: Argument 1 to "setdefault" of "MutableMapping" has incompatible type "tuple[Optional[Any], Optional[Any]]"; expected "tuple[str, str]" [arg-type] +scripts/validation.py:0: error: Argument 2 to "frontend_digest_matches" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/validation.py:0: error: Argument 3 to "_entry_rule" has incompatible type "list[str]"; expected "set[str]" [arg-type] +scripts/validation.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "dict[str, str]") [assignment] +scripts/validation.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/validation.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/validation.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/validation.py:0: error: Item "None" of "Optional[str]" has no attribute "lower" [union-attr] +scripts/verify.py:0: error: Item "None" of "Optional[dict[Any, Any]]" has no attribute "setdefault" [union-attr] +scripts/version_basis.py:0: error: Too many arguments for "describe_form" [call-arg] diff --git a/platforms/targets/emudeck.yml b/platforms/targets/emudeck.yml index c445457fb..e20859dc3 100644 --- a/platforms/targets/emudeck.yml +++ b/platforms/targets/emudeck.yml @@ -1,6 +1,6 @@ platform: emudeck source: https://github.com/dragoonDorise/EmuDeck/tree/dddb30229adbbd54d5ff7283cfd70e11c4322c26 -scraped_at: '2026-11-06T07:10:22Z' +scraped_at: '2026-11-06T13:46:57Z' targets: steamos: architecture: x86_64 @@ -348,7 +348,6 @@ targets: - freechaf - freeintv - frodo - - fsuae - fuse - galaksija - gam4980 @@ -430,7 +429,6 @@ targets: - nxengine - o2em - oberon - - omicron - openlara - opera - panda3ds @@ -511,7 +509,6 @@ targets: - vice_xplus4 - vice_xscpu64 - vice_xvic - - video_processor - vircon32 - virtualjaguar - virtualxt diff --git a/pyproject.toml b/pyproject.toml index 508eecc34..bf5a325ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,8 @@ description = "BIOS collection manager for retrogaming platforms" # install.py is the only entry point a user runs before cloning, and both # bootstraps accept 3.8, so the floor stays where the bootstraps put it. That # floor covers install.py alone: the build scripts under scripts/ need 3.9 -# (pipeline.py checks it), and CI runs the test suite on 3.12. +# (common.py, which every build script imports, checks it), and CI runs the +# test suite on 3.12. requires-python = ">=3.8" dependencies = [ "pyyaml", @@ -37,9 +38,34 @@ dev = [ # installs only in its isolated build environment and 3.12 no longer # bundles: without it the guard skipped on every CI run. "setuptools>=61", + # The type check CI runs, pinned: another mypy reports other errors + # than the ones mypy-baseline.txt holds. + "mypy==1.18.2", + "mypy-baseline==0.7.4", ] # The site is built by Astro Starlight from website/package.json and its # lockfile (npm ci --prefix website): no Python package builds it. [project.urls] Repository = "https://github.com/Abdess/retrobios" + +# The annotations are checked, not only written. Third-party packages are left +# out (no_site_packages) so the result does not depend on what the machine +# has installed: yaml, jsonschema and the rest read as Any everywhere. The +# errors the code carried when the check started sit in mypy-baseline.txt. +# CI fails on an error that is not there and on an entry that no longer +# occurs: `mypy > out.txt`, then `mypy-baseline filter < out.txt`, and +# `mypy-baseline sync < out.txt` once the fix is in. +[tool.mypy] +files = ["install.py", "scripts"] +python_version = "3.9" +explicit_package_bases = true +ignore_missing_imports = true +no_site_packages = true +disable_error_code = ["import-untyped"] +cache_dir = "tmp/mypy_cache" + +[tool.mypy-baseline] +baseline_path = "mypy-baseline.txt" +ignore_categories = ["note"] +sort_baseline = true diff --git a/scripts/bundled_only.py b/scripts/bundled_only.py index d80d5bac5..bf4e34471 100644 --- a/scripts/bundled_only.py +++ b/scripts/bundled_only.py @@ -116,6 +116,9 @@ def main() -> int: parser.add_argument("--json", action="store_true", help="print the list as JSON") parser.add_argument("--strict", action="store_true", help="exit 1 while a file is listed") args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "emulators_dir", "platforms_dir", "install_dir"]) rows = bundled_only( load_database(args.db), load_emulator_profiles(args.emulators_dir), diff --git a/scripts/campaign.py b/scripts/campaign.py index fd014113a..1021aee9f 100644 --- a/scripts/campaign.py +++ b/scripts/campaign.py @@ -83,14 +83,17 @@ from collections.abc import Callable, Iterator, Sequence from dataclasses import dataclass, field from pathlib import Path -import yaml - sys.path.insert(0, str(Path(__file__).resolve().parent)) +# First: core_probe stops an interpreter older than 3.9 before anything else +# fails on it. +import core_probe + +import yaml + import autorun import campaign_profiles import campaign_rules as rules -import core_probe import measurement_records as mr ROOT = Path(__file__).resolve().parent.parent diff --git a/scripts/check_freshness.py b/scripts/check_freshness.py index d5b0df8c9..ac1e02188 100644 --- a/scripts/check_freshness.py +++ b/scripts/check_freshness.py @@ -54,11 +54,13 @@ import pin_installer # noqa: E402 import refresh_data_dirs # noqa: E402 import upstream # noqa: E402 from common import ( # noqa: E402 + InputMissing, apply_target_overrides, list_registered_platforms, load_emulator_profiles, load_platform_registry, require_directory, + suggest_names, upstream_profile_index, write_text_atomic, yaml_load, @@ -73,9 +75,13 @@ REPO_ROOT = Path(__file__).resolve().parents[1] CORE_INFO_REPO = "https://github.com/libretro/libretro-core-info" # The git blob of every .info as last reviewed: `--record-coreinfo` writes it. CORE_INFO_RECORD = REPO_ROOT / "platforms" / "_core_info.json" -NO_INTRO_MIRROR = "https://github.com/hugo19941994/auto-datfile-generator" -NO_INTRO_RELEASE = "Daily_Rebuild" -NO_INTRO_ASSET = "no-intro.zip" +NO_INTRO_MIRROR = game_dat_importer.NOINTRO_MIRROR +NO_INTRO_RELEASE = game_dat_importer.NOINTRO_RELEASE +NO_INTRO_ASSET = game_dat_importer.NOINTRO_ASSET +# The mirror rebuilds daily. A pack older than this is a mirror that stopped: +# hugo19941994's failed every day from 2026-07-08, and comparing our import +# date with its asset read OK for four months. +NO_INTRO_MAX_AGE_DAYS = 7 TOSEC_DOWNLOADS = "https://www.tosecdev.org/downloads" MAME_REPO = "https://github.com/mamedev/mame" FBNEO_REPO = "https://github.com/libretro/FBNeo" @@ -893,7 +899,10 @@ def record_core_info( """ unknown = sorted(set(names) - set(blobs)) if unknown: - raise ValueError(f"no such .info in core-info: {', '.join(unknown)}") + raise InputMissing( + f"no such .info in core-info: {', '.join(unknown)}. " + + suggest_names(unknown, list(blobs), "libretro-core-info") + ) recorded = load_core_info_record(path) or {} recorded.update({n: blobs[n] for n in (names or blobs)}) payload = {"repository": CORE_INFO_REPO, "blobs": dict(sorted(recorded.items()))} @@ -1083,6 +1092,25 @@ def _no_intro_asset_date(cache_dir: str, offline: bool) -> str: return "" +def _today() -> str: + return datetime.now(timezone.utc).strftime("%Y-%m-%d") + + +def _no_intro_row(subject: str, local: str, updated: str) -> Finding: + """Our import against the mirror's pack, and the pack against today.""" + age = (datetime.fromisoformat(_today()) - datetime.fromisoformat(updated)).days + if age > NO_INTRO_MAX_AGE_DAYS: + return Finding( + "catalogs", subject, STALE, local=local, remote=updated, + detail=f"the mirror last rebuilt {NO_INTRO_ASSET} on {updated}, {age} days ago, " + f"though it rebuilds daily: {NO_INTRO_MIRROR}/actions", + ) + return Finding( + "catalogs", subject, STALE if updated > local else OK, + local=local, remote=updated, detail=f"{NO_INTRO_ASSET} rebuilt {updated}", + ) + + def _check_nointro_games(cache_dir: str, offline: bool) -> Finding: """games/catalogs/no-intro/ against the pack it was read from. The pack holds every DAT: rebuilt after the catalogs were read, it may carry @@ -1094,10 +1122,7 @@ def _check_nointro_games(cache_dir: str, offline: bool) -> Finding: updated = _no_intro_asset_date(cache_dir, offline) if not updated: return Finding("catalogs", subject, UNKNOWN, local=fetched, detail="asset not listed") - return Finding( - "catalogs", subject, STALE if updated > fetched else OK, - local=fetched, remote=updated, detail=f"{NO_INTRO_ASSET} rebuilt {updated}", - ) + return _no_intro_row(subject, fetched, updated) def redump_datfile_version(code: str) -> str: @@ -1171,14 +1196,7 @@ def _check_no_intro(cache_dir: str, offline: bool) -> Finding: updated = _no_intro_asset_date(cache_dir, offline) if not updated: return Finding("catalogs", "no-intro", UNKNOWN, local=imported, detail="asset not listed") - return Finding( - "catalogs", - "no-intro", - STALE if updated > imported else OK, - local=imported, - remote=updated, - detail=f"{NO_INTRO_ASSET} rebuilt {updated}", - ) + return _no_intro_row("no-intro", imported, updated) def tosec_latest_pack(html: str) -> str | None: @@ -1839,7 +1857,10 @@ def main() -> int: if args.record_coreinfo is not None: blobs = core_info_blobs(args.cache_dir, args.offline) if not blobs: - raise RuntimeError("core-info listing unavailable: nothing recorded") + raise InputMissing( + f"core-info listing not in {args.cache_dir}: run without --offline" + if args.offline else "core-info listing unavailable: the GitHub API did not answer" + ) done = record_core_info(blobs, args.record_coreinfo) print(f"recorded {len(done)} .info file(s) as reviewed") return 0 diff --git a/scripts/check_platform_folder.py b/scripts/check_platform_folder.py index 4c5401a44..7c8de3ddd 100644 --- a/scripts/check_platform_folder.py +++ b/scripts/check_platform_folder.py @@ -1,11 +1,9 @@ -"""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 @@ -42,14 +40,12 @@ 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")): path = root / entry["destination"] - verdict = "ok" if accepts(entry, path, mode) else "wrong" if path.is_file() else "missing" - results.append((entry["destination"], verdict)) + results.append((entry["destination"], "ok" if accepts(entry, path, mode) else "wrong" if path.is_file() else "missing")) if listed: held = any(accepts(e, path, mode) for e in listed for path in loose) results.append((f"{system_id}: any file", "ok" if held else "missing")) @@ -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/common.py b/scripts/common.py index 2bf7ee504..02c559415 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -9,6 +9,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable from collections import Counter +import argparse import filecmp import hashlib import functools @@ -19,6 +20,16 @@ import signal import sys import zipfile from pathlib import Path +from typing import Any, TextIO + +# Every build script imports this module. verify.py, slots.py, validate_pr.py +# and cross_reference.py started under 3.8 and stopped halfway on a +# str.removeprefix AttributeError, which named no version. +if sys.version_info < (3, 9): + sys.exit( + f"{Path(sys.argv[0]).name} needs Python 3.9 or newer, " + f"this is {sys.version.split()[0]}" + ) try: @@ -53,6 +64,33 @@ class DatabaseUnreadable(InputMissing): """The database a command was pointed at does not exist or is not JSON.""" +class SourceUnreadable(ValueError): + """A profile or platform file that is not valid YAML. Not an InputMissing: + the callers that skip a platform whose file is absent must not skip one + whose file is damaged. run_cli prints the message on one line.""" + + +def source_yaml(stream: TextIO) -> Any: + """Parse one of our YAML sources, naming the file a syntax error is in. + + Every profile is loaded whatever the command, so one misindented key in + an unrelated profile stopped `verify.py --emulator handy` on a 37-line + traceback, the faulty file named only near its end. + """ + if yaml is None: + return yaml_load(stream) + try: + return yaml_load(stream) + except yaml.YAMLError as exc: + mark = getattr(exc, "problem_mark", None) + where = f"{stream.name}:{mark.line + 1}" if mark else stream.name + problem = getattr(exc, "problem", None) or str(exc).splitlines()[0] + raise SourceUnreadable( + f"{where}: {problem}. Check it with: " + "python scripts/validate_schemas.py --source-only" + ) from None + + def load_database(db_path: str) -> dict: """Load database.json and return parsed dict. @@ -64,6 +102,18 @@ def load_database(db_path: str) -> dict: with open(db_path) as f: return json.load(f) except FileNotFoundError: + # A mistyped --db beside the real database was told to rescan the + # whole collection into a second file under the typo. + import difflib # noqa: PLC0415 + + typed = Path(db_path) + close = difflib.get_close_matches( + typed.name, [p.name for p in typed.parent.glob("*.json")], n=1, cutoff=0.8 + ) + if close: + raise DatabaseUnreadable( + f"{db_path} not found. Did you mean {typed.with_name(close[0])}?" + ) from None raise DatabaseUnreadable( f"{db_path} not found. Build it with: python scripts/generate_db.py " f"--force --bios-dir bios --output {db_path}" @@ -302,7 +352,7 @@ def _inherited_yaml(platform_name: str, platforms_dir: str) -> tuple[dict, list[ raise FileNotFoundError(f"Platform config not found: {config_file}") with open(config_file) as f: - config = yaml_load(f) or {} + config = source_yaml(f) or {} if "inherits" not in config: return config, [platform_name] @@ -348,7 +398,7 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") - reg_real = os.path.realpath(registry_path) if reg_real not in _shared_yml_cache: with open(registry_path) as f: - _shared_yml_cache[reg_real] = yaml_load(f) or {} + _shared_yml_cache[reg_real] = source_yaml(f) or {} reg = _shared_yml_cache[reg_real] # The platform's own entry first, then its parents' for what it does # not say: RetroPie's status, scraper and install are its own, its @@ -387,7 +437,7 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") - shared_real = os.path.realpath(shared_path) if shared_real not in _shared_yml_cache: with open(shared_path) as f: - _shared_yml_cache[shared_real] = yaml_load(f) or {} + _shared_yml_cache[shared_real] = source_yaml(f) or {} shared = _shared_yml_cache[shared_real] shared_groups = shared.get("shared_groups", {}) for system in config.get("systems", {}).values(): @@ -494,7 +544,7 @@ def load_data_dir_registry(platforms_dir: str = "platforms") -> dict: if not os.path.exists(registry_path): return {} with open(registry_path) as f: - data = yaml_load(f) or {} + data = source_yaml(f) or {} return data.get("data_directories", {}) @@ -504,7 +554,7 @@ def load_platform_registry(platforms_dir: str = "platforms") -> dict: if not os.path.exists(registry_path): return {} with open(registry_path) as f: - return (yaml_load(f) or {}).get("platforms", {}) + return (source_yaml(f) or {}).get("platforms", {}) def list_registered_platforms(platforms_dir: str = "platforms") -> list[str]: @@ -521,7 +571,7 @@ def list_registered_platforms(platforms_dir: str = "platforms") -> list[str]: if not os.path.exists(registry_path): raise InputMissing(f"no platform registry at {registry_path}") with open(registry_path) as f: - registry = yaml_load(f) or {} + registry = source_yaml(f) or {} platforms = [] for name, meta in sorted(registry.get("platforms", {}).items()): config_path = os.path.join(platforms_dir, meta.get("config", f"{name}.yml")) @@ -621,6 +671,31 @@ def suggest_names( return f"{listing_flag} lists all {total}." +def work_from_repo_root( + args: argparse.Namespace, parser: argparse.ArgumentParser, path_flags: list[str] +) -> None: + """Run from the repository root, whichever directory the command started in. + + database.json records every file from the root and the data registry + names its caches the same way, so a run from scripts/ ended on a + FileNotFoundError traceback. A path the caller typed is read from where + they typed it, a default from the root. + """ + root = Path(__file__).resolve().parent.parent + if Path.cwd().resolve() == root: + return + for flag in path_flags: + value = getattr(args, flag, None) + if value and value != parser.get_default(flag): + # A Path stays a Path for the code that joins onto it. + setattr(args, flag, type(value)(os.path.abspath(value))) + os.chdir(root) + if getattr(args, "platform", None): + # Read before the move, the registry was not there to map + # "RetroArch" to its slug. + args.platform = platform_arg(args.platform) + + def platform_arg(value: str) -> str: """--platform as typed, mapped to the registry's slug when it names one. @@ -669,7 +744,7 @@ def _target_overrides(platform_name: str, targets_dir: str) -> dict: if not os.path.exists(overrides_file): return {} with open(overrides_file) as f: - all_overrides = yaml_load(f) or {} + all_overrides = source_yaml(f) or {} return all_overrides.get(platform_name, {}).get("targets", {}) @@ -690,7 +765,7 @@ def platform_files_target(platform: str, target: str, platforms_dir: str = "plat if not os.path.exists(target_file): return False with open(target_file) as f: - targets = (yaml_load(f) or {}).get("targets", {}) + targets = (source_yaml(f) or {}).get("targets", {}) return target in _target_alias_index(targets, _target_overrides(platform, targets_dir)) @@ -710,7 +785,7 @@ def canonical_target_name( if not os.path.exists(target_file): continue with open(target_file) as f: - targets = (yaml_load(f) or {}).get("targets", {}) + targets = (source_yaml(f) or {}).get("targets", {}) canonical = _target_alias_index( targets, _target_overrides(platform_name, targets_dir) ).get(target) @@ -764,7 +839,7 @@ def load_target_config( f"(platforms with targets: {', '.join(with_targets)})" ) with open(target_file) as f: - data = yaml_load(f) or {} + data = source_yaml(f) or {} targets = data.get("targets", {}) overrides = _target_overrides(platform_name, targets_dir) @@ -822,7 +897,7 @@ def list_available_targets( if not os.path.exists(target_file): return [] with open(target_file) as f: - data = yaml_load(f) or {} + data = source_yaml(f) or {} # The count --target filters on: overrides applied, as load_target_config # applies them. The scraped list counted Batocera's machine variants and @@ -2229,7 +2304,7 @@ def load_emulator_profiles( raise InputMissing(f"no emulator profile directory at {emulators_dir}") for f in source_yaml_paths(emu_path): with open(f) as fh: - profile = yaml_load(fh) or {} + profile = source_yaml(fh) or {} if "emulator" not in profile: continue _normalize_hash_fields(profile) @@ -2359,7 +2434,7 @@ def group_identical_platforms( try: raw_path = os.path.join(platforms_dir, f"{platform}.yml") with open(raw_path) as f: - raw = yaml_load(f) or {} + raw = source_yaml(f) or {} inherits[platform] = "inherits" in raw config = load_platform_config(platform, platforms_dir) except FileNotFoundError: @@ -3503,6 +3578,9 @@ def build_target_cores_cache( raise notes.append(f"INFO: Skipping {p}: no hardware targets") skip.append(p) + except SourceUnreadable: + # A damaged targets file is not a platform without the target. + raise except ValueError as e: if is_all: notes.append(f"INFO: Skipping {p}: {e}") @@ -3530,7 +3608,7 @@ def _unknown_target_message(target: str, platforms: list[str], platforms_dir: st if not os.path.exists(target_file): continue with open(target_file) as f: - targets = (yaml_load(f) or {}).get("targets", {}) + targets = (source_yaml(f) or {}).get("targets", {}) overrides = _target_overrides(platform, targets_dir) for name in _target_alias_index(targets, overrides): owners.setdefault(name, []).append(platform) @@ -3624,10 +3702,18 @@ def run_cli(main: Callable[[], int | None]) -> None: for name in ("SIGTERM", "SIGHUP"): if hasattr(signal, name): signal.signal(getattr(signal, name), _exit_on_signal) + # Redirected, stdout is block-buffered and stderr is not: the --strict + # verdict landed on the first line of the log, before the report it + # closes. Line buffering keeps the two in the order they were written. + # A stream a caller swapped in (a test's StringIO) has nothing to set. + reconfigure = getattr(sys.stdout, "reconfigure", None) + if reconfigure is not None: + reconfigure(line_buffering=True) try: code = main() sys.stdout.flush() - except (InputMissing, ArtifactLockBusy, ArtifactDirectoryMissing) as exc: + except (InputMissing, SourceUnreadable, ArtifactLockBusy, + ArtifactDirectoryMissing) as exc: print(f"ERROR: {exc}", file=sys.stderr) sys.exit(1) except BrokenPipeError: diff --git a/scripts/core_probe.py b/scripts/core_probe.py index 4d3cc7f1c..d58142c78 100644 --- a/scripts/core_probe.py +++ b/scripts/core_probe.py @@ -40,6 +40,14 @@ from collections.abc import Iterable, Iterator, Sequence from dataclasses import dataclass, field from pathlib import Path +# The probe tools do not import common.py, which states the 3.9 floor for the +# other scripts. Under 3.8 they stopped on a str.removesuffix AttributeError. +if sys.version_info < (3, 9): + sys.exit( + f"{Path(sys.argv[0]).name} needs Python 3.9 or newer, " + f"this is {sys.version.split()[0]}" + ) + ROOT = Path(__file__).resolve().parent.parent MIN_BENCH_REPETITIONS = 5 DEFAULT_WORK_DIR = ROOT / "tmp" / "core_probe" diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index f92545b27..365efefda 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -1191,6 +1191,9 @@ def main() -> None: "--list-emulators", action="store_true", help="List available emulators" ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["emulators_dir", "platforms_dir", "db"]) if args.list_emulators: ignored = [flag for flag, on in ( ("--emulator", args.emulator), ("--platform", args.platform), diff --git a/scripts/exporter/recalbox_exporter.py b/scripts/exporter/recalbox_exporter.py index bad223329..161c9a9bb 100644 --- a/scripts/exporter/recalbox_exporter.py +++ b/scripts/exporter/recalbox_exporter.py @@ -221,12 +221,19 @@ class Exporter(BaseExporter): ours = str(entry.get("path") or entry.get("name", "")).split("/")[:-1] return not {d.casefold() for d in theirs} & {d.casefold() for d in ours} - def _bios_element(self, fe: NativeFile, native_id: str) -> str: + def _bios_element( + self, fe: NativeFile, native_id: str, also: tuple[NativeFile, ...] = () + ) -> str: + """``also`` are further additions at fe's path: Bios.cpp reads one + element per path, and its md5 and core lists take every one of them.""" + group = (fe, *also) + md5 = dict.fromkeys(h for f in group for h in f.hashes("md5")) + cores = dict.fromkeys(c for f in group for c in f.cores()) attrs = [f"path={quoteattr(self._path(fe))}"] - attrs.append(f'md5={quoteattr(",".join(fe.hashes("md5")))}') - attrs.append(f'core={quoteattr(",".join(fe.cores()))}') + attrs.append(f'md5={quoteattr(",".join(md5))}') + attrs.append(f'core={quoteattr(",".join(cores))}') - if not self.required_value(fe): + if not any(self.required_value(f) for f in group): attrs.append('mandatory="false"') elif fe.native("mandatory_declared", None) is True: attrs.append('mandatory="true"') @@ -343,11 +350,17 @@ class Exporter(BaseExporter): } ours: dict[str, tuple[NativeSystem, dict[str, tuple[NativeFile, str]]]] = {} for system in systems.values(): - writable = [fe for fe in system.files if self.writable(fe)] - if writable: + # Two additions at one path (two cores, two dumps) are one + # element: keyed by path, the second replaced the first and its + # md5 left the file while both were counted added. + at_path: dict[str, list[NativeFile]] = {} + for fe in system.files: + if self.writable(fe): + at_path.setdefault(self._path(fe), []).append(fe) + if at_path: ours[system.native_id] = (system, { - self._path(fe): (fe, self._bios_element(fe, system.native_id)) - for fe in writable + path: (group[0], self._bios_element(group[0], system.native_id, tuple(group[1:]))) + for path, group in at_path.items() }) def element(attrs: dict[str, str]) -> str: diff --git a/scripts/exporter/retrodeck_exporter.py b/scripts/exporter/retrodeck_exporter.py index b742b796c..b9a88a027 100644 --- a/scripts/exporter/retrodeck_exporter.py +++ b/scripts/exporter/retrodeck_exporter.py @@ -215,6 +215,7 @@ class Exporter(BaseExporter): ours: list[OrderedDict], additions: list[OrderedDict] | None = None, appended: set[tuple[str, str]] | None = None, + held_back: list[int] | None = None, ) -> list[OrderedDict]: """Correct the component's own list, never replace it. @@ -301,13 +302,19 @@ class Exporter(BaseExporter): for e in merged for system in (_systems_of(e) or [""]) } - for entry in additions or []: + for index, entry in enumerate(additions or []): key = (str(entry.get("filename", "")), str(entry.get("system", "")), _paths_key(entry)) if key[0] and key not in held: held.add(key) merged.append(entry) + elif held_back is not None: + held_back.append(index) return merged + def lands(self, fe: NativeFile) -> bool: + """An addition the list already held is not written a second time.""" + return self.writable(fe) and id(fe) not in getattr(self, "_held_back", set()) + def render( self, systems: dict[str, NativeSystem], @@ -317,6 +324,7 @@ class Exporter(BaseExporter): ) -> dict[str, str]: self._place_additions(systems) self._appended: set[int] = set() + self._held_back: set[int] = set() grouped = self._by_component(systems) produced: dict[str, str] = {} @@ -331,25 +339,28 @@ class Exporter(BaseExporter): continue entries = [self._entry(fe) for fe in files if fe.platform is not None] - additions = [self._entry(fe) for fe in files if fe.platform is None] + added = [fe for fe in files if fe.platform is None] + additions = [self._entry(fe) for fe in added] appended: set[tuple[str, str]] = set() + held_back: list[int] = [] for component_value in manifest.values(): if not isinstance(component_value, dict): continue holder = self._bios_holder(component_value) if holder is None: component_value["bios"] = self._merge( - None, entries, additions, appended + None, entries, additions, appended, held_back ) else: container, key = holder written = container.get(key) - merged = self._merge(written, entries, additions, appended) + merged = self._merge(written, entries, additions, appended, held_back) # A single object stays one while nothing is added to it. container[key] = ( merged[0] if isinstance(written, dict) and len(merged) == 1 else merged ) break + self._held_back.update(id(added[index]) for index in held_back) # A declared entry whose revision the platform does not hold is # written beside theirs: a new line, not a corrected value. self._appended.update( diff --git a/scripts/exporter/retropie_exporter.py b/scripts/exporter/retropie_exporter.py index 7e31af28a..8f9607365 100644 --- a/scripts/exporter/retropie_exporter.py +++ b/scripts/exporter/retropie_exporter.py @@ -336,6 +336,8 @@ class Exporter(BaseExporter): systems_of.setdefault(str(name).lower(), set()).add(fe.native_system) known = set(systems_of) self._skipped: dict[str, list[str]] = {} + # Rewritten, with a file of a system no clause lists left out. + self._partial: list[str] = [] self._named: set[int] = set() produced: dict[str, str] = {} @@ -400,12 +402,11 @@ class Exporter(BaseExporter): homes.setdefault(owners[0], []).append(fe) else: homeless = True - if homeless: - # No enumeration of that system to extend, and a sentence we - # would have to write ourselves is a documentation change, - # not a correction. - skip("whose help text lists no files of that system to add to", module_id) + # No enumeration of that system to extend, and a sentence we + # would have to write ourselves is a documentation change, not a + # correction. With nothing placed the package is left as is. if not homes: + skip("whose help text lists no files of that system to add to", module_id) continue added = sum(len(self._in_search_order(fes)) for fes in homes.values()) @@ -428,6 +429,8 @@ class Exporter(BaseExporter): f'rp_module_help="{new_help}"', 1, ) + if homeless: + self._partial.append(module_id) return produced @@ -454,12 +457,20 @@ class Exporter(BaseExporter): ) -> str: """Packages are the unit here, not file entries.""" skipped: dict[str, list[str]] = getattr(self, "_skipped", {}) + + def shown(modules: list[str]) -> str: + names = ", ".join(sorted(modules)[:4]) + return names + (f" and {len(modules) - 4} more" if len(modules) > 4 else "") + parts = [f"{len(produced)} packages updated"] + partial = getattr(self, "_partial", []) + if partial: + parts.append( + f"Updated in part: {shown(partial)}, whose help text lists no files " + "of one of their systems to add to" + ) for reason, modules in sorted(skipped.items()): - shown = ", ".join(sorted(modules)[:4]) - if len(modules) > 4: - shown += f" and {len(modules) - 4} more" - parts.append(f"Left as is: {shown}, {reason}") + parts.append(f"Left as is: {shown(modules)}, {reason}") return ". ".join(parts) def validate( diff --git a/scripts/exporter/systemdat_exporter.py b/scripts/exporter/systemdat_exporter.py index a42aa6c86..3bb02de04 100644 --- a/scripts/exporter/systemdat_exporter.py +++ b/scripts/exporter/systemdat_exporter.py @@ -175,6 +175,9 @@ class Exporter(BaseExporter): ours: dict[str, dict[str, str]] = {} added: dict[str, set[str]] = {} + # A rom name is one line: an addition whose name an earlier entry + # took is not written, and was counted added. + self._shadowed: set[int] = set() for system, files in self.exportable(systems): rendered: dict[str, str] = {} for fe in files: @@ -194,7 +197,11 @@ class Exporter(BaseExporter): sha1 = fe.record_hash("sha1") if sha1: parts.append(f"sha1 {sha1}") - rendered.setdefault(name, f"\trom ( {' '.join(parts)} )") + if name in rendered: + if fe.platform is None: + self._shadowed.add(id(fe)) + continue + rendered[name] = f"\trom ( {' '.join(parts)} )" if fe.platform is None: added.setdefault(system.native_id, set()).add(name) if rendered: @@ -241,7 +248,11 @@ class Exporter(BaseExporter): return {self.native_filename(): "\n".join(lines)} def lands(self, fe: NativeFile) -> bool: - return self.writable(fe) and id(fe) not in getattr(self, "_untitled", set()) + return ( + self.writable(fe) + and id(fe) not in getattr(self, "_untitled", set()) + and id(fe) not in getattr(self, "_shadowed", set()) + ) def validate( self, diff --git a/scripts/game_rules.py b/scripts/game_rules.py index 040884889..d9512ba34 100644 --- a/scripts/game_rules.py +++ b/scripts/game_rules.py @@ -247,6 +247,9 @@ def main() -> int: help="profiles the rules are checked against (default emulators)") parser.add_argument("--json", action="store_true", help="print the summary as JSON") args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["regions", "extras", "headers", "emulators_dir"]) regions = load_yaml(args.regions) extras = load_yaml(args.extras) diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 03ef16791..3934830a9 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -43,6 +43,7 @@ from common import ( require_directory, target_listing_line, suggest_names, + system_index, require_platform, load_target_config, archive_entry, @@ -164,7 +165,9 @@ class PackRecord: self.sources: dict[str, str] = {} -def _add_pack_member(zf: zipfile.ZipFile, source_path: str, arcname: str) -> None: +def _add_pack_member( + zf: zipfile.ZipFile | PackRecord, source_path: str, arcname: str +) -> None: """Add a file to the pack with metadata that does not depend on the build. ZipFile.write copies the source file's mtime into the member. For a file @@ -197,7 +200,9 @@ def _add_pack_member(zf: zipfile.ZipFile, source_path: str, arcname: str) -> Non shutil.copyfileobj(source, target, 1024 * 1024) -def _write_generated_member(zf: zipfile.ZipFile, arcname: str, text: str) -> None: +def _write_generated_member( + zf: zipfile.ZipFile | PackRecord, arcname: str, text: str +) -> None: """Write a pack member the builder composes rather than copies. READMEs, instruction notes and the manifest are produced at build time, @@ -581,7 +586,7 @@ def _data_directory_members( def _pack_data_directories( - zf: zipfile.ZipFile, + zf: zipfile.ZipFile | PackRecord, pack_systems: dict, data_registry: dict | None, platform_name: str, @@ -1476,7 +1481,9 @@ def generate_pack( def _extract_zip_to_archive( - source_zip: str, dest_prefix: str, target_zf: zipfile.ZipFile + source_zip: str, + dest_prefix: str, + target_zf: zipfile.ZipFile | PackRecord, ) -> None: """Extract contents of a source ZIP into target ZIP under dest_prefix.""" with zipfile.ZipFile(source_zip, "r") as src: @@ -1507,7 +1514,7 @@ def _extract_zip_to_archive( def _add_zip_to_pack( source_zip: str, dest_path: str, - target_zf: zipfile.ZipFile, + target_zf: zipfile.ZipFile | PackRecord, file_entry: dict | None = None, ) -> None: """Write a source ZIP into the pack, byte for byte. @@ -1555,7 +1562,7 @@ class DamagedArchiveError(OSError): def _pack_data_tree( - zf: zipfile.ZipFile, + zf: zipfile.ZipFile | PackRecord, local_cache: str, dd_dest: str, seen_destinations: set[str], @@ -2053,7 +2060,10 @@ def generate_system_pack( f"No emulators found for system(s): {', '.join(system_ids)}", file=sys.stderr, ) - print(suggest_names(system_ids, sorted(all_systems), "--list-systems"), + # --list-systems merges spellings that differ in case or separator: + # the hint counted 579 spellings for the 532 lines it prints. + print(suggest_names(system_ids, sorted(all_systems), "--list-systems", + listed=len(system_index(profiles))), file=sys.stderr) return None diff --git a/scripts/generate_site.py b/scripts/generate_site.py index a81e40523..7215ea016 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -14,7 +14,6 @@ Usage: from __future__ import annotations -from siterender import SECTION_LABELS, WIKI_SRC_DIR import argparse import csv import hashlib @@ -56,6 +55,7 @@ from common import ( load_platform_config, load_provenance_snapshots, native_layer, + require_directory, require_yaml, resolution_is_hash_exact, resolve_local_file, @@ -64,6 +64,7 @@ from common import ( unique_emulator_profiles, yaml_load, ) +from siterender import SECTION_LABELS, WIKI_SRC_DIR from artifacts import ArtifactLockBusy, artifact_lock from artifacts import json_text as _json_text from cross_reference import FileConsumers, entry_target, held_copy @@ -2330,7 +2331,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 " @@ -12393,6 +12394,10 @@ def main() -> None: args = parser.parse_args() if args.docs_dir is None: args.docs_dir = str(Path(args.site_dir) / STARLIGHT_CONTENT) + # Before any write: the public assets and the bootstraps went out first, + # and a mistyped folder stopped the run only after them. + require_directory(args.emulators_dir, "--emulators-dir") + require_directory(args.platforms_dir, "--platforms-dir") try: # Two runs over one docs/ delete each other's pages and scratch # files in the sweep, so the second is refused instead. diff --git a/scripts/identity.py b/scripts/identity.py index 745029f96..188e899d1 100644 --- a/scripts/identity.py +++ b/scripts/identity.py @@ -146,6 +146,9 @@ def main() -> int: "--strict", action="store_true", help="exit non-zero when any is found" ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["bios_dir"]) from common import require_directory # noqa: PLC0415 require_directory(args.bios_dir) diff --git a/scripts/list_platforms.py b/scripts/list_platforms.py index 82f8e4c31..88fa39319 100644 --- a/scripts/list_platforms.py +++ b/scripts/list_platforms.py @@ -26,7 +26,10 @@ def list_platforms() -> list[str]: def main() -> None: parser = argparse.ArgumentParser(allow_abbrev=False, description="List available platforms") - parser.parse_args() + args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, []) platforms = list_platforms() diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 05774c6e7..16e47ffba 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -20,6 +20,7 @@ import re import subprocess import sys import tempfile +import time from collections import Counter from collections.abc import Callable, Iterable, Iterator, Sequence from dataclasses import asdict, dataclass, field, replace @@ -80,6 +81,9 @@ from upstream import CompareResult, find_renamed DEFAULT_CACHE = upstream.CACHE_DIR ANON_QUOTA = 60 TRIAGE_PATH_SAMPLE = 5 +# A pass that asks the forges says where it is at this interval: an online +# --all --triage ran 15 minutes without a word and read as hung. +PROGRESS_SECONDS = 30 STATUS_ORDER = ( "ANCHORED", "UNCHECKED", "EXTERNAL", "BINARY", "SHIFTED", "RENAMED", "MOVED", @@ -4991,35 +4995,40 @@ def _print_extras_from( print(f" {line}") +class _Progress: + """A stderr line every PROGRESS_SECONDS while a pass asks the forges.""" + + def __init__(self, total: int, doing: str, quiet: bool) -> None: + self.total, self.doing, self.quiet = total, doing, quiet + self.last = time.monotonic() + + def step(self, done: int, name: str) -> None: + now = time.monotonic() + if not self.quiet and now - self.last >= PROGRESS_SECONDS: + print(f"{self.doing}: {done} of {self.total}, at {name}", file=sys.stderr, flush=True) + self.last = now + + +def _interrupted(done: int, total: int, what: str) -> None: + print(f"interrupted after {done} of {total} profiles: {what}", file=sys.stderr, flush=True) + + def _print_triage( args: argparse.Namespace, selected: dict, reports: list[ProfileReport] -) -> None: - """Profiles ranked by drift, worst first.""" +) -> bool: + """Profiles ranked by drift, worst first. False when Ctrl-C cut the + scoring short: the ranking then covers the profiles scored.""" ranked = [] sampled = 0 - for report in reports: - profile = selected[report.name] - version = None - commits = 0 - refused = "" - repo = select_repo(profile) - if report.head and repo is not None: - paths = _cited_paths(report) - try: - version = check_version(profile, repo, args.cache_dir, args.offline) - for path in paths[:TRIAGE_PATH_SAMPLE]: - commits += upstream.commits_touching( - repo, path, report.pin, args.cache_dir, args.offline, - head=report.head, - ) - except upstream.UpstreamError as exc: - # One forge refusing a request (Eden's answers 403 to commit - # lookups) leaves this profile unscored, not the whole ranking. - refused = "commit history unreachable" - print(f"{report.name}: {exc}", file=sys.stderr) - if len(paths) > TRIAGE_PATH_SAMPLE and not refused: - sampled += 1 - ranked.append((drift_score(report, version, commits), report, refused)) + progress = _Progress(len(reports), "scored", args.offline or len(reports) < 2) + complete = True + try: + for done, report in enumerate(reports): + progress.step(done, report.name) + sampled += _score(args, selected, report, ranked) + except KeyboardInterrupt: + _interrupted(len(ranked), len(reports), "the ranking covers those scored") + complete = False if sampled: # One line for the run: a notice per profile buried the ranking # under 254 lines of stderr. @@ -5037,6 +5046,37 @@ def _print_triage( pending += bool(report.needs_review() or report.uncheckable()) print(f"{score:5d} {report.name:30s} {state}") print(f"{len(ranked)} profiles, {pending} with something to review") + return complete + + +def _score( + args: argparse.Namespace, selected: dict, report: ProfileReport, ranked: list +) -> bool: + """Append one profile's drift score to ``ranked``. True when its commit + count was read on a sample of the paths it cites.""" + profile = selected[report.name] + version = None + commits = 0 + refused = "" + sampled = False + repo = select_repo(profile) + if report.head and repo is not None: + paths = _cited_paths(report) + try: + version = check_version(profile, repo, args.cache_dir, args.offline) + for path in paths[:TRIAGE_PATH_SAMPLE]: + commits += upstream.commits_touching( + repo, path, report.pin, args.cache_dir, args.offline, + head=report.head, + ) + except upstream.UpstreamError as exc: + # One forge refusing a request (Eden's answers 403 to commit + # lookups) leaves this profile unscored, not the whole ranking. + refused = "commit history unreachable" + print(f"{report.name}: {exc}", file=sys.stderr) + sampled = len(paths) > TRIAGE_PATH_SAMPLE and not refused + ranked.append((drift_score(report, version, commits), report, refused)) + return sampled def _refuse_flag_combinations(args: argparse.Namespace) -> None: @@ -5260,30 +5300,24 @@ def main() -> None: return reports = [] - for name, profile in selected.items(): - profile_path = Path(args.emulators_dir) / f"{name}.yml" - try: - report = _build_full_report(args, name, profile, profile_path, failed) - except upstream.RateLimitError as exc: - # A quota is one forge's: git.libretro.com answering 429 said - # nothing about the 600 profiles on GitHub. upstream stops asking - # that host, so the rest of the pass costs it nothing. - print(f"{name}: {exc}", file=sys.stderr) - report = ProfileReport( - name=name, entries=[], counts={}, - skipped=f"rate limited: {exc.host or exc}", - ) - failed.add(name) - reports.append(report) - continue - reports.append(report) - if writes: - try: - _apply_writes(args, name, profile, report) - except YamlWriteError as exc: - # The guard already left the file untouched, so keep going. - print(f"{name}: write refused: {exc}", file=sys.stderr) - failed.add(name) + quiet = args.offline or len(selected) < 2 + if not quiet: + print( + f"reading {len(selected)} profiles against their forges, a line every " + f"{PROGRESS_SECONDS} s. --offline answers from the cache alone.", + file=sys.stderr, flush=True, + ) + progress = _Progress(len(selected), "read", quiet) + complete = True + try: + for name, profile in selected.items(): + progress.step(len(reports), name) + _read_one(args, name, profile, reports, failed, writes) + except KeyboardInterrupt: + # What was read is still worth printing: a ranking of 200 profiles + # beats none after 15 minutes. + _interrupted(len(reports), len(selected), "the output covers those read") + complete = False # --changed-only narrows every output, not the text report alone: the # documented `--all --changed-only --json` listed every profile. @@ -5291,14 +5325,49 @@ def main() -> None: report for report in reports if not args.changed_only or report.needs_review() or report.name in failed ] - _emit(args, selected, reports, shown) + complete = _emit(args, selected, reports, shown) is not False and complete _refresh_source_paths(args) + if not complete: + raise SystemExit(130) if failed or (args.strict and any( report.needs_review() or report.uncheckable() for report in reports )): raise SystemExit(1) +def _read_one( + args: argparse.Namespace, + name: str, + profile: dict, + reports: list[ProfileReport], + failed: set[str], + writes: bool, +) -> None: + """Build one profile's report into ``reports``, then apply the writes.""" + profile_path = Path(args.emulators_dir) / f"{name}.yml" + try: + report = _build_full_report(args, name, profile, profile_path, failed) + except upstream.RateLimitError as exc: + # A quota is one forge's: git.libretro.com answering 429 said + # nothing about the 600 profiles on GitHub. upstream stops asking + # that host, so the rest of the pass costs it nothing. + print(f"{name}: {exc}", file=sys.stderr) + reports.append(ProfileReport( + name=name, entries=[], counts={}, + skipped=f"rate limited: {exc.host or exc}", + )) + failed.add(name) + return + reports.append(report) + if writes: + try: + _apply_writes(args, name, profile, report) + except YamlWriteError as exc: + # The guard already left the file untouched, so keep going. + print(f"{name}: write refused: {exc}", file=sys.stderr) + failed.add(name) + + # Profiles this run wrote. Their new pins name trees the site's record of # cited source paths does not hold yet. _WRITTEN: set[str] = set() @@ -5400,8 +5469,9 @@ def _emit( selected: dict, reports: list[ProfileReport], shown: list[ProfileReport], -) -> None: - """Write the reports in the form the flags ask for.""" +) -> bool | None: + """Write the reports in the form the flags ask for. False when the + triage was cut short.""" if args.as_json: print(json.dumps([report_to_dict(r) for r in shown], indent=2)) return @@ -5417,8 +5487,7 @@ def _emit( print(url) return if args.triage: - _print_triage(args, selected, shown) - return + return _print_triage(args, selected, shown) for report in shown: print(format_report(report, args.changed_only)) _print_extras(args, selected[report.name], report) diff --git a/scripts/provenance_report.py b/scripts/provenance_report.py index a54c1b0a6..16ec2f5f8 100644 --- a/scripts/provenance_report.py +++ b/scripts/provenance_report.py @@ -384,6 +384,9 @@ def main() -> int: "(the pipeline writes derivable.json)", ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "provenance_dir", "emulators_dir", "write_derivable"]) if args.write_derivable and not args.derive: parser.error("--write-derivable records what --derive finds: pass both") diff --git a/scripts/refresh_stale.py b/scripts/refresh_stale.py index bf65bb6ac..b50e472d8 100755 --- a/scripts/refresh_stale.py +++ b/scripts/refresh_stale.py @@ -148,18 +148,28 @@ def _run_check_freshness(areas: tuple[str, ...], extra: list[str]) -> list[dict] if areas and set(areas) != set(AUTO_AREAS + MANUAL_AREAS): cmd += ["--only", ",".join(areas)] cmd += extra - proc = subprocess.run( - cmd, cwd=REPO_ROOT, capture_output=True, text=True, check=False, timeout=1800 - ) - if proc.returncode not in (0, 1): + try: + proc = subprocess.run( + cmd, cwd=REPO_ROOT, capture_output=True, text=True, check=False, timeout=1800 + ) + except subprocess.TimeoutExpired as exc: + raise RuntimeError( + f"check_freshness.py did not finish in {exc.timeout:.0f} s: run " + f"`{' '.join(cmd[1:])} > report.json` alone, then pass --findings report.json" + ) from exc + # Exit 1 is the STALE rows, and also a crash: only a JSON list on stdout + # tells them apart, and the crash's own message is on stderr. + try: + findings = json.loads(proc.stdout) if proc.returncode in (0, 1) else None + except json.JSONDecodeError: + findings = None + if not isinstance(findings, list): tail = (proc.stderr or proc.stdout).strip().splitlines()[-3:] raise RuntimeError( - "check_freshness.py failed:\n" + "\n".join(tail or ["(no output)"]) + f"check_freshness.py failed (exit {proc.returncode}):\n" + + "\n".join(tail or ["(no output)"]) ) - try: - return json.loads(proc.stdout) - except json.JSONDecodeError as exc: - raise RuntimeError(f"check_freshness.py returned unreadable JSON: {exc}") from exc + return findings def load_findings(path: Path, areas: tuple[str, ...]) -> list[dict]: diff --git a/scripts/region_audit.py b/scripts/region_audit.py index eb02e2e6f..731e0d2a8 100644 --- a/scripts/region_audit.py +++ b/scripts/region_audit.py @@ -201,6 +201,9 @@ def main() -> int: ) parser.add_argument("--json", action="store_true", help="Machine-readable output") args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "emulators_dir"]) db = load_database(args.db) profiles = load_emulator_profiles(args.emulators_dir) diff --git a/scripts/region_check.py b/scripts/region_check.py index b7eff324f..6fa9a9019 100644 --- a/scripts/region_check.py +++ b/scripts/region_check.py @@ -36,6 +36,14 @@ import urllib.request from collections.abc import Callable, Iterable from pathlib import Path +# The probe tools do not import common.py, which states the 3.9 floor for the +# other scripts. Under 3.8 they stopped on a str.removesuffix AttributeError. +if sys.version_info < (3, 9): + sys.exit( + f"{Path(sys.argv[0]).name} needs Python 3.9 or newer, " + f"this is {sys.version.split()[0]}" + ) + sys.path.insert(0, str(Path(__file__).resolve().parent)) import yaml # noqa: E402 diff --git a/scripts/scraper/arcade_parent_importer.py b/scripts/scraper/arcade_parent_importer.py index a884b3188..59c885032 100644 --- a/scripts/scraper/arcade_parent_importer.py +++ b/scripts/scraper/arcade_parent_importer.py @@ -1122,7 +1122,7 @@ def main() -> int: which.add_argument("--all", action="store_true", help="every profile with an arcade source") parser.add_argument("--emulators-dir", default="emulators", help="profiles whose BIOS sets are read (default emulators)") - parser.add_argument("--database", default="database.json", + parser.add_argument("--db", default="database.json", help="collection index, to name BIOS sets the collection lacks") parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT, help=f"where the parent maps are written (default {DEFAULT_OUTPUT})") @@ -1133,7 +1133,7 @@ def main() -> int: args = parser.parse_args() profiles = load_emulator_profiles(args.emulators_dir, skip_aliases=False) - names = collection_names(Path(args.database)) + names = collection_names(Path(args.db)) failed = False for profile in sorted(PROFILES) if args.all else args.profile: if profile not in profiles: diff --git a/scripts/scraper/game_dat_importer.py b/scripts/scraper/game_dat_importer.py index 0968008bf..87522041c 100644 --- a/scripts/scraper/game_dat_importer.py +++ b/scripts/scraper/game_dat_importer.py @@ -14,6 +14,9 @@ games/catalogs/ and never in provenance/. No-Intro comes from the auto-datfile-generator release asset (Dat-o-Matic bans automated clients), Redump from redump.info's per-system datfile route. +The mirror is dantob's fork: hugo19941994's original has failed its daily +rebuild since Dat-o-Matic's 2026-07 re-layout (no-intro.zip last rebuilt +2026-07-08), and the fork carries the fix (d4a9d769, 2026-07-31). Every download is cached under .cache/game-dats/ with what the server said about it, so a second run reads nothing from the network and writes the same bytes. A DAT's region tag is a market label, never the value a core computes. @@ -49,8 +52,11 @@ SOURCES = ("no-intro", "redump") CACHE_DIR = Path(".cache") / "game-dats" OUTPUT_DIR = Path("games") / "catalogs" +NOINTRO_MIRROR = "https://github.com/dantob/auto-datfile-generator" +NOINTRO_RELEASE = "Daily_Rebuild" NOINTRO_RELEASE_API = ( - "https://api.github.com/repos/hugo19941994/auto-datfile-generator/releases/tags/Daily_Rebuild" + NOINTRO_MIRROR.replace("https://github.com/", "https://api.github.com/repos/") + + f"/releases/tags/{NOINTRO_RELEASE}" ) NOINTRO_ASSET = "no-intro.zip" REDUMP_DATFILE_URL = "https://redump.info/datfile/{code}" @@ -120,7 +126,7 @@ LICENCES: dict[str, dict] = { "website or its operators is required for such use." ), "mirror": { - "repo": "https://github.com/hugo19941994/auto-datfile-generator", + "repo": NOINTRO_MIRROR, "licence": "MIT", "scope": "the mirror's scripts only, the DATs it republishes stay under the terms above", }, diff --git a/scripts/scraper/targets/emudeck_targets_scraper.py b/scripts/scraper/targets/emudeck_targets_scraper.py index fa8fa75b2..6920e42c3 100644 --- a/scripts/scraper/targets/emudeck_targets_scraper.py +++ b/scripts/scraper/targets/emudeck_targets_scraper.py @@ -16,6 +16,7 @@ import sys import urllib.error import urllib.request from datetime import datetime, timezone +from pathlib import Path import yaml @@ -32,6 +33,7 @@ STEAMOS_API = ( WINDOWS_API = ( "https://api.github.com/repos/EmuDeck/emudeck-we/contents/functions/EmuScripts" ) +RETROARCH_TARGETS = Path(__file__).resolve().parents[3] / "platforms" / "targets" / "retroarch.yml" def _fetch(url: str) -> str: headers = {"User-Agent": "retrobios-scraper/1.0"} @@ -75,7 +77,7 @@ class Scraper(BaseTargetScraper): return self._revision def _fetch_cores_for_target( - self, api_url: str, label: str, arch: str = "x86_64" + self, api_url: str, label: str, retroarch_target: str ) -> list[str]: print(f" fetching {label} EmuScripts...", file=sys.stderr) scripts = _list_emuscripts(api_url) @@ -94,7 +96,7 @@ class Scraper(BaseTargetScraper): standalone_count = len(cores) # EmuDeck ships RetroArch = all its libretro cores are available if has_retroarch: - ra_cores = self._load_retroarch_cores(arch) + ra_cores = self._load_retroarch_cores(retroarch_target) for c in ra_cores: if c not in seen: seen.add(c) @@ -108,24 +110,21 @@ class Scraper(BaseTargetScraper): return sorted(cores) @staticmethod - def _load_retroarch_cores(arch: str) -> list[str]: - """Load RetroArch target cores for given architecture.""" - import os - - target_path = os.path.join("platforms", "targets", "retroarch.yml") - with open(target_path) as f: + def _load_retroarch_cores(target: str, path: Path = RETROARCH_TARGETS) -> list[str]: + """The buildbot's libretro cores for one RetroArch target, by name: + linux-x86_64 and windows-x86_64 share an architecture, not a list.""" + with path.open(encoding="utf-8") as f: data = yaml_load(f) or {} - # Find a target matching the architecture - for tinfo in data.get("targets", {}).values(): - if tinfo.get("architecture") == arch and tinfo.get("cores"): - return tinfo["cores"] - raise RuntimeError(f"no {arch} core list in {target_path}") + cores = ((data.get("targets") or {}).get(target) or {}).get("cores") + if not cores: + raise RuntimeError(f"no {target} core list in {path}") + return cores def fetch_targets(self) -> dict: steamos_cores = self._fetch_cores_for_target( - f"{STEAMOS_API}?ref={self.revision}", "SteamOS" + f"{STEAMOS_API}?ref={self.revision}", "SteamOS", "linux-x86_64" ) - windows_cores = self._fetch_cores_for_target(WINDOWS_API, "Windows") + windows_cores = self._fetch_cores_for_target(WINDOWS_API, "Windows", "windows-x86_64") targets: dict[str, dict] = {} if steamos_cores: diff --git a/scripts/slots.py b/scripts/slots.py index b261d6c74..8f62ed3e4 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -1009,6 +1009,9 @@ def main() -> int: "without it the run reports them and exits 0", ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "platforms_dir", "emulators_dir"]) import contextlib diff --git a/scripts/verify.py b/scripts/verify.py index 5f909d155..dcd9eb37a 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -41,8 +41,10 @@ from common import ( _foreign_tree, _owner_tokens, platform_arg, + work_from_repo_root, target_listing_line, suggest_names, + system_index, require_platform, archive_entry, archive_is_optional, @@ -129,29 +131,6 @@ DEFAULT_EMULATORS_DIR = "emulators" REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -def work_from_repo_root( - args: argparse.Namespace, parser: argparse.ArgumentParser, path_flags: list[str] -) -> None: - """Run from the repository root, whichever directory the command started in. - - database.json records every file from the root and the data registry - names its caches the same way, so a run from scripts/ ended on a - FileNotFoundError traceback. A path the caller typed is read from where - they typed it, a default from the root. - """ - if os.path.realpath(os.getcwd()) == os.path.realpath(REPO_ROOT): - return - for flag in path_flags: - value = getattr(args, flag, None) - if value and value != parser.get_default(flag): - setattr(args, flag, os.path.abspath(value)) - os.chdir(REPO_ROOT) - if getattr(args, "platform", None): - # Read before the move, the registry was not there to map - # "RetroArch" to its slug. - args.platform = platform_arg(args.platform) - - # Status model: aligned with Batocera BiosStatus (batocera-systems:967-969) @@ -1808,7 +1787,8 @@ def _print_undeclared_entry( ref_part = f" [{u['source_ref']}]" if u.get("source_ref") else "" print(f" validates {checks_label}{ref_part}") else: - print(f" [{'+'.join(checks)}]") + # A bare "[size]" under the entry said nothing to a reader. + print(f" validates {'+'.join(checks)}") def _split_undeclared(undeclared: list[dict]) -> tuple[list[dict], list[dict], list[dict]]: @@ -1920,10 +1900,15 @@ def _print_undeclared_section(result: dict, verbose: bool) -> None: u for u in bundled if u["required"] and u.get("category", "bios") != "game_data" ] - names = ", ".join(sorted({u["name"] for u in shown})) + named = sorted({u["name"] for u in shown}) + # The required firmware alone read as the whole count: 14 names + # for 154 entries, nothing saying the rest existed. + hidden = len({u["name"] for u in bundled}) - len(named) + more = f"{hidden} more, " if named else "" + items = named + ([f"{more}--verbose lists them"] if hidden else []) print( f" Bundled with their emulator, not collected: {len(bundled)}" - + (f" ({names})" if names else "") + + (f" ({', '.join(items)})" if items else "") ) if unplaced: print(f" Core files held but not packed: {len(unplaced)} " @@ -2611,7 +2596,10 @@ def verify_system( f"No emulators found for system(s): {', '.join(system_ids)}", file=sys.stderr, ) - print(suggest_names(system_ids, sorted(all_systems), "--list-systems"), + # --list-systems merges spellings that differ in case or separator: + # the hint counted 579 spellings for the 532 lines it prints. + print(suggest_names(system_ids, sorted(all_systems), "--list-systems", + listed=len(system_index(profiles))), file=sys.stderr) sys.exit(1) @@ -2821,7 +2809,7 @@ def _main(held: contextlib.ExitStack) -> None: parser = argparse.ArgumentParser(allow_abbrev=False, description="Platform-native BIOS verification") parser.add_argument("--platform", "-p", type=platform_arg, help="Platform name") parser.add_argument( - "--all", action="store_true", help="Verify all active platforms" + "--all", action="store_true", help="Verify every registered platform" ) parser.add_argument( "--emulator", "-e", help="Emulator profile name(s), comma-separated" @@ -3121,6 +3109,13 @@ def _overall_summary(group_results: list[tuple[dict, list[str]]]) -> str: named = [f"{name} {count}" for name, count in counts.items() if count] return f" ({', '.join(named)})" if named else "" + def on(groups: tuple[str, ...]) -> str: + """The platforms a file is missing on, by the shorter side.""" + if len(group_results) < 2 or len(groups) * 2 <= len(group_results): + return ", ".join(groups) + others = [" / ".join(g) for _, g in group_results if " / ".join(g) not in groups] + return "every platform" + (f" but {', '.join(others)}" if others else "") + parts = [] for severity, label in ((Severity.CRITICAL, "critical"), (Severity.WARNING, "warning"), (Severity.INFO, "info")): @@ -3139,18 +3134,26 @@ def _overall_summary(group_results: list[tuple[dict, list[str]]]) -> str: parts.append(f"{total} {'discrepancy' if total == 1 else 'discrepancies'}{where(counts)}") lines = [f"{platforms} platforms: {', '.join(parts)}"] - missing = { - " / ".join(group): sorted({ + # Grouped by file, not by platform: sfiii2a.zip missing everywhere was + # counted and printed once per platform, 24 files for 10 distinct ones. + holders: dict[str, list[str]] = {} + for r, group in group_results: + for name in sorted({ u["name"] for u in _core_required_missing(_split_undeclared( r.get("undeclared_files", []))[1]) - }) - for r, group in group_results - } - total = sum(len(names) for names in missing.values()) - if total: - detail = "; ".join(f"{group}: {', '.join(names)}" - for group, names in missing.items() if names) - lines.append(f"{total} core file(s) a core cannot run without are missing ({detail})") + }): + holders.setdefault(name, []).append(" / ".join(group)) + if holders: + by_platforms: dict[tuple[str, ...], list[str]] = {} + for name, groups in sorted(holders.items()): + by_platforms.setdefault(tuple(groups), []).append(name) + detail = "; ".join( + f"{', '.join(names)} ({on(groups)})" + for groups, names in sorted(by_platforms.items(), + key=lambda item: (-len(item[0]), item[1])) + ) + lines.append(f"{len(holders)} core file(s) a core cannot run without are " + f"missing: {detail}") for category, label in _NOT_FIRMWARE.items(): counts = { " / ".join(group): len((r.get("missing_not_firmware") or {}).get(category, [])) diff --git a/tests/scratchtree.py b/tests/scratchtree.py new file mode 100644 index 000000000..93ec3fdae --- /dev/null +++ b/tests/scratchtree.py @@ -0,0 +1,78 @@ +"""A disposable copy of the checkout, for tests that run a writer and expect +it to refuse. + +Run in the checkout, a regression made the very write the test guards +against: generate_site rewrote website/src/data/published-parts.json and the +FAQ page whatever --docs-dir said, and a pipeline whose lock probe failed +would regenerate database.json and install/. A file another session had +already changed listed the same name in `git diff --name-only` before and +after, so its edits were lost without a signal. + +The copy holds every file git tracks or would track, as the working tree has +it. bios/ and data/ (17 GB and 600 MB of inputs no such test writes) are +links to the checkout's. +""" + +from __future__ import annotations + +import atexit +import hashlib +import os +import shutil +import subprocess +import tempfile +from functools import cache +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +LINKED = ("bios", "data") + + +@cache +def scratch_checkout() -> Path: + """The copy, built once per test process and removed at its exit.""" + root = Path(tempfile.mkdtemp(prefix="checkout-", dir=REPO_ROOT / "tmp" / "tests")) + # A detached worktree with nothing checked out: the scripts that read + # HEAD (the site's bootstrap pin) find this commit. + subprocess.run( + ["git", "worktree", "add", "--quiet", "--detach", "--no-checkout", str(root), "HEAD"], + cwd=REPO_ROOT, capture_output=True, check=True, + ) + atexit.register(_remove, root) + listed = subprocess.run( + ["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], + cwd=REPO_ROOT, capture_output=True, check=True, + ).stdout.decode("utf-8") + for name in filter(None, listed.split("\0")): + source = REPO_ROOT / name + if name.split("/", 1)[0] in LINKED or not source.is_file() or source.is_symlink(): + continue + target = root / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + for name in LINKED: + if (REPO_ROOT / name).exists(): + (root / name).symlink_to(REPO_ROOT / name, target_is_directory=True) + return root + + +def _remove(root: Path) -> None: + subprocess.run(["git", "worktree", "remove", "--force", str(root)], + cwd=REPO_ROOT, capture_output=True, check=False) + shutil.rmtree(root, ignore_errors=True) + + +def snapshot(root: Path) -> dict[str, str]: + """The SHA-1 of every file under root, links not followed. A file + written with the bytes it held is no change: what is caught is a file + whose content a run replaced. Scratch space, caches, bytecode and lock + files are no output.""" + seen = {} + for directory, dirs, files in os.walk(root): + dirs[:] = [d for d in dirs if d != "__pycache__" + and Path(directory, d) not in (root / "tmp", root / ".cache")] + for name in files: + path = Path(directory) / name + if name != ".lock" and not path.is_symlink(): + seen[str(path.relative_to(root))] = hashlib.sha1(path.read_bytes()).hexdigest() + return seen diff --git a/tests/test_adler32_resolution.py b/tests/test_adler32_resolution.py index 5c6f2f502..c90aba144 100644 --- a/tests/test_adler32_resolution.py +++ b/tests/test_adler32_resolution.py @@ -9,6 +9,8 @@ corrected it later, every other reader of the resolution did not. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import shutil diff --git a/tests/test_agnostic_scan.py b/tests/test_agnostic_scan.py index 94d6d3680..254527ca3 100644 --- a/tests/test_agnostic_scan.py +++ b/tests/test_agnostic_scan.py @@ -12,6 +12,8 @@ BIOS root of platforms that do not run that emulator. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_agnostic_scan_mode.py b/tests/test_agnostic_scan_mode.py index 7e165aa02..c8db42d81 100644 --- a/tests/test_agnostic_scan_mode.py +++ b/tests/test_agnostic_scan_mode.py @@ -8,6 +8,8 @@ libretro core, while verify set the entry aside. Reads the repository. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_agnostic_scope.py b/tests/test_agnostic_scope.py index fac69e8e7..3a1d3d27a 100644 --- a/tests/test_agnostic_scope.py +++ b/tests/test_agnostic_scope.py @@ -9,6 +9,8 @@ was not where it looks. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_agnostic_substitute.py b/tests/test_agnostic_substitute.py index 8ba581e2f..8b30d53d7 100644 --- a/tests/test_agnostic_substitute.py +++ b/tests/test_agnostic_substitute.py @@ -7,6 +7,8 @@ the slot missing and the manifest omitted it. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import tempfile diff --git a/tests/test_amberelec_targets.py b/tests/test_amberelec_targets.py index 3724fd0d5..5abb32518 100644 --- a/tests/test_amberelec_targets.py +++ b/tests/test_amberelec_targets.py @@ -8,6 +8,8 @@ the firmware of cores its image never had. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_arcade_parents.py b/tests/test_arcade_parents.py index c03b4caa1..ca50d79a3 100644 --- a/tests/test_arcade_parents.py +++ b/tests/test_arcade_parents.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import json import tempfile diff --git a/tests/test_archive_choice.py b/tests/test_archive_choice.py index cee358bae..65cc92ddb 100644 --- a/tests/test_archive_choice.py +++ b/tests/test_archive_choice.py @@ -7,6 +7,8 @@ seven of the eight members Batocera checks were missing. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import shutil diff --git a/tests/test_archive_recipes.py b/tests/test_archive_recipes.py index cdfbd1f63..551cf5d9d 100644 --- a/tests/test_archive_recipes.py +++ b/tests/test_archive_recipes.py @@ -9,6 +9,8 @@ those members were never read there, and the archive matched no recipe. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import unittest diff --git a/tests/test_artifact_lock.py b/tests/test_artifact_lock.py index 72804b1cb..ae0f964af 100644 --- a/tests/test_artifact_lock.py +++ b/tests/test_artifact_lock.py @@ -7,6 +7,8 @@ ZIPs, which surfaces as BadZipFile far from its cause. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import subprocess import sys @@ -25,6 +27,7 @@ from common import ( # noqa: E402 hold_repository_outputs, publish_hold, ) +from tests.scratchtree import scratch_checkout # noqa: E402 def _tempdir(test: unittest.TestCase) -> str: @@ -115,11 +118,13 @@ class PackLockCliTest(unittest.TestCase): self.dir = _tempdir(self) def _run(self, argv: list[str]) -> subprocess.CompletedProcess: + # In a copy: a pipeline that missed the lock regenerated + # database.json and install/ wherever it ran. return subprocess.run( [sys.executable, *argv], capture_output=True, text=True, - cwd=str(REPO_ROOT), + cwd=str(scratch_checkout()), timeout=300, ) @@ -234,11 +239,12 @@ class PipelineHoldsItsFixedOutputs(unittest.TestCase): def test_a_second_pipeline_is_refused_whatever_its_output_dir(self): scratch = _tempdir(self) - with hold_repository_outputs(str(REPO_ROOT)): + checkout = scratch_checkout() + with hold_repository_outputs(str(checkout)): proc = subprocess.run( [sys.executable, "scripts/pipeline.py", "--offline", "--skip-docs", "--output-dir", scratch], - capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=120, + capture_output=True, text=True, cwd=str(checkout), timeout=120, ) self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) self.assertIn("another run is writing database.json", proc.stderr) @@ -260,11 +266,12 @@ class StandaloneWritersHonourTheRepositoryHold(unittest.TestCase): env = {k: v for k, v in os.environ.items() if k != "RETROBIOS_HELD_ARTIFACT_DIR"} return subprocess.run( [sys.executable, *argv], capture_output=True, text=True, - cwd=str(REPO_ROOT), timeout=300, env=env, check=False, + cwd=str(scratch_checkout()), timeout=300, env=env, check=False, ) def test_a_writer_of_a_fixed_output_is_refused(self): - with hold_repository_outputs(str(REPO_ROOT)): + # The writers run in a copy, where a missed lock writes nothing of ours. + with hold_repository_outputs(str(scratch_checkout())): for argv in ( ["scripts/generate_db.py", "--bios-dir", "bios", "--output", "database.json"], ["scripts/generate_pack.py", "--all", "--manifest", "--offline", @@ -369,7 +376,7 @@ class SiteGenerationHoldsDocs(unittest.TestCase): with artifact_lock(docs): result = subprocess.run( [sys.executable, "scripts/generate_site.py", "--docs-dir", docs], - cwd=REPO_ROOT, capture_output=True, text=True, timeout=600, + cwd=scratch_checkout(), capture_output=True, text=True, timeout=600, ) self.assertEqual(result.returncode, 1, result.stdout + result.stderr) self.assertIn("in use by another run", result.stderr) diff --git a/tests/test_atomic_artifacts.py b/tests/test_atomic_artifacts.py index f506b7573..51c652114 100644 --- a/tests/test_atomic_artifacts.py +++ b/tests/test_atomic_artifacts.py @@ -8,6 +8,8 @@ path in bios/: a scan running beside the copy hashed the truncated file. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import tempfile diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index a93c0a8c8..c5e4a8ecc 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import contextlib import hashlib @@ -136,6 +138,41 @@ class ReadmeRegressions(unittest.TestCase): self.assertNotIn("SyntaxError", proc.stderr) self.assertIn("generate_pack.py needs Python 3.9", proc.stderr) + def test_every_script_names_the_floor_under_an_older_interpreter(self): + """Only generate_pack.py and pipeline.py checked the version. verify.py, + slots.py, validate_pr.py and cross_reference.py started under 3.8 and + stopped halfway on a str.removeprefix AttributeError.""" + import shutil # noqa: PLC0415 + + python38 = shutil.which("python3.8") + if python38 is None: + if os.environ.get("CI"): + self.fail("CI must provide python3.8") + self.skipTest("no python3.8 on PATH") + commands = ( + ["verify.py", "--emulator", "handy"], + ["verify.py", "--platform", "batocera"], + ["validate_pr.py", "--json", "README.md"], + ["cross_reference.py", "--emulator", "handy"], + ["slots.py"], + ["identity.py"], + ["generate_site.py", "--help"], + ["campaign.py", "--help"], + ["region_check.py", "--help"], + ) + for command in commands: + with self.subTest(command=" ".join(command)): + # -E -s rather than -I: -I drops the script's folder from + # sys.path, so the imports would fail before any check. + proc = subprocess.run( + [python38, "-E", "-s", f"scripts/{command[0]}", *command[1:]], + cwd=ROOT, capture_output=True, text=True, timeout=60, + check=False, + ) + self.assertEqual(proc.returncode, 1, proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + self.assertIn(f"{command[0]} needs Python 3.9", proc.stderr) + def test_the_floor_interpreter_imports_every_script(self): """Every module under scripts/ is loaded by a real interpreter of the floor version, as the installer's 3.8 floor is proven by running it. @@ -218,6 +255,46 @@ class AModuleRunAloneHasItsScratchDirectory(unittest.TestCase): self.assertEqual(proc.stdout.split(), [scratch, scratch]) +class EveryTestModuleImportsTheIsolation(unittest.TestCase): + """tests/__init__.py keeps a run offline, its scratch under tmp/ and its + installer cache its own, but only `discover -s tests -t .` imported it: + `discover tests` ran with RETROBIOS_OFFLINE unset, scratch in /tmp and + the user's real cache. Each module now imports the package first.""" + + def test_the_package_import_comes_before_any_other(self): + missing = [] + for path in sorted((ROOT / "tests").glob("test_*.py")): + body = ast.parse(path.read_text(encoding="utf-8")).body + imports = [ + node for node in body + if isinstance(node, (ast.Import, ast.ImportFrom)) + and getattr(node, "module", None) != "__future__" + ] + first = imports[0] if imports else None + if not (isinstance(first, ast.Import) + and [alias.name for alias in first.names] == ["tests"]): + missing.append(path.name) + self.assertEqual(missing, []) + + def test_discover_without_a_top_level_runs_isolated(self): + env = {k: v for k, v in os.environ.items() + if k not in ("RETROBIOS_OFFLINE", "TMPDIR", "RETROBIOS_CACHE")} + proc = subprocess.run( + [sys.executable, "-c", + "import os, sys, tempfile, unittest\n" + "unittest.TestLoader().discover('tests', pattern='test_artifact_lock.py')\n" + "print(os.environ.get('RETROBIOS_OFFLINE'), tempfile.gettempdir()," + " os.environ.get('RETROBIOS_CACHE'))\n"], + cwd=ROOT, env=env, capture_output=True, text=True, timeout=120, + check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stderr) + offline, scratch, cache = proc.stdout.split() + self.assertEqual(offline, "1") + self.assertEqual(scratch, str(ROOT / "tmp" / "tests")) + self.assertEqual(cache, str(ROOT / "tmp" / "tests" / "cache")) + + class ReleaseSigningRegressions(unittest.TestCase): """A checksum list published beside its own artifacts proves nothing. @@ -2834,6 +2911,50 @@ class DeclaredDependenciesMatchTheDocumentedWorkflows(unittest.TestCase): ) +class TheAnnotationsAreChecked(unittest.TestCase): + """Every signature carried type hints and nothing read them: six pack + helpers declared `zipfile.ZipFile` while every caller also passed a + PackRecord, and 990 such contradictions went through CI green. mypy now + runs in CI against mypy-baseline.txt, the errors the code carried when the + check started.""" + + BASELINE = ROOT / "mypy-baseline.txt" + ENTRY = re.compile(r"(?P[^:]+\.py):0: error: .+ \[[a-z-]+\]") + + def test_ci_holds_the_code_to_the_baseline(self): + body = (ROOT / ".github" / "workflows" / "validate.yml").read_text( + encoding="utf-8" + ) + self.assertIn("mypy-baseline filter", body) + self.assertNotIn("--allow-unsynced", body) + + def test_every_baseline_entry_names_a_file_that_exists(self): + stale = [] + for line in self.BASELINE.read_text(encoding="utf-8").splitlines(): + match = self.ENTRY.fullmatch(line) + self.assertIsNotNone(match, f"unreadable baseline entry: {line}") + if not (ROOT / match["path"]).is_file(): + stale.append(match["path"]) + self.assertEqual(stale, [], "baseline entries for files that are gone") + + def test_the_code_reports_no_error_outside_the_baseline(self): + for module in ("mypy", "mypy_baseline"): + try: + __import__(module) + except ImportError: + self.skipTest(f"{module} is not installed (pip install -e '.[dev]')") + report = subprocess.run( + [sys.executable, "-m", "mypy"], + cwd=ROOT, capture_output=True, text=True, check=False, + ) + result = subprocess.run( + [sys.executable, "-m", "mypy_baseline", "filter", "--no-colors"], + cwd=ROOT, input=report.stdout, capture_output=True, text=True, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stdout) + + class TheProjectInstallsWithPip(unittest.TestCase): """pyproject declared a dev extra that pip could not install: without a [build-system] and an explicit module list, setuptools' flat-layout @@ -4991,9 +5112,34 @@ class VerifyAllSummaryNamesWhatIsWrong(unittest.TestCase): [(clean, ["RetroArch"]), (warned, ["RetroDECK"]), (core_miss, ["ROCKNIX"])] ) self.assertIn("1 warning (RetroDECK 1)", summary) - self.assertIn("ROCKNIX: n64.psh", summary) + self.assertIn("n64.psh (ROCKNIX)", summary) self.assertIn("0 critical,", summary) + def test_a_file_missing_on_several_platforms_is_counted_once(self): + """sfiii2a.zip missing on 15 platforms read '24 core file(s)' for 10 + distinct files, the name printed 15 times on one line.""" + sys.path.insert(0, str(ROOT / "scripts")) + import verify + + def missing(*names: str) -> dict: + return {"severity_counts": {}, "undeclared_files": [ + {"name": name, "required": True, "in_repo": False, + "category": "bios", "in_pack": True} for name in names + ]} + + summary = verify._overall_summary([ + (missing("sfiii2a.zip"), ["Batocera"]), + (missing("sfiii2a.zip", "ROM"), ["RetroPie"]), + (missing("sfiii2a.zip"), ["RomM"]), + (missing(), ["MinUI"]), + ]) + self.assertIn( + "2 core file(s) a core cannot run without are missing: " + "sfiii2a.zip (every platform but MinUI); ROM (RetroPie)", + summary, + ) + self.assertEqual(summary.count("sfiii2a.zip"), 1) + class OneOutputFormPerRun(unittest.TestCase): @@ -5956,6 +6102,20 @@ class TheRepairCommandWritesTheFileNamed(unittest.TestCase): load_database(target) self.assertIn(f"--output {target}", str(caught.exception)) + def test_a_typo_beside_the_database_names_the_database(self): + """--db databse.json beside database.json was told to rescan the whole + collection into a second file named after the typo.""" + from scripts.common import DatabaseUnreadable, load_database + + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory: + (Path(directory) / "database.json").write_text("{}") + typo = Path(directory) / "databse.json" + with self.assertRaises(DatabaseUnreadable) as caught: + load_database(str(typo)) + message = str(caught.exception) + self.assertIn(f"Did you mean {typo.with_name('database.json')}?", message) + self.assertNotIn("generate_db.py", message) + class VerifyPacksRefusesWhatItCannotDo(unittest.TestCase): """The --verify-packs refusals lived in the --all-variants checks, which diff --git a/tests/test_autorun.py b/tests/test_autorun.py index ad15f7421..1600b99d1 100644 --- a/tests/test_autorun.py +++ b/tests/test_autorun.py @@ -7,6 +7,8 @@ a function returning canned CI runs. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import json diff --git a/tests/test_batocera_cores.py b/tests/test_batocera_cores.py index 159912e07..eeeff5203 100644 --- a/tests/test_batocera_cores.py +++ b/tests/test_batocera_cores.py @@ -9,6 +9,8 @@ under it: amiberry, fsuae and vice were never in a target list. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_batocera_md5_list.py b/tests/test_batocera_md5_list.py index 223d2add5..cc207dd7d 100644 --- a/tests/test_batocera_md5_list.py +++ b/tests/test_batocera_md5_list.py @@ -9,6 +9,8 @@ a list, and verify accepts any listed md5. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_bios_companions.py b/tests/test_bios_companions.py index 6f0bbe0d8..de9d9c45b 100644 --- a/tests/test_bios_companions.py +++ b/tests/test_bios_companions.py @@ -9,6 +9,8 @@ the Batocera BIOS folder. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_bios_layout.py b/tests/test_bios_layout.py index e447e43b9..5a9f73d66 100644 --- a/tests/test_bios_layout.py +++ b/tests/test_bios_layout.py @@ -7,6 +7,8 @@ Oric and Tangerine: one machine, two folders, two pages. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import unittest from pathlib import Path diff --git a/tests/test_bundled_identity.py b/tests/test_bundled_identity.py index 77eb121f1..d05fb9e18 100644 --- a/tests/test_bundled_identity.py +++ b/tests/test_bundled_identity.py @@ -13,6 +13,8 @@ entry is written with the hash of the file the emulator ships. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import unittest diff --git a/tests/test_cache_independent_counts.py b/tests/test_cache_independent_counts.py index 69a52ec15..62def66ae 100644 --- a/tests/test_cache_independent_counts.py +++ b/tests/test_cache_independent_counts.py @@ -10,6 +10,8 @@ directory cache is not a collection. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_campaign.py b/tests/test_campaign.py index 5966dd8d7..f1f41b7cb 100644 --- a/tests/test_campaign.py +++ b/tests/test_campaign.py @@ -10,6 +10,8 @@ resume and rerun on a changed core. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_case_insensitive_platforms.py b/tests/test_case_insensitive_platforms.py index 1eeb9be6c..168460fcc 100644 --- a/tests/test_case_insensitive_platforms.py +++ b/tests/test_case_insensitive_platforms.py @@ -7,6 +7,8 @@ went red. The flag lived in retrobat.yml and vanished at the next scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_casefold_cache.py b/tests/test_casefold_cache.py index 9105f9cac..3e6d50631 100644 --- a/tests/test_casefold_cache.py +++ b/tests/test_casefold_cache.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_check_freshness.py b/tests/test_check_freshness.py index 829062f66..c1bf68952 100644 --- a/tests/test_check_freshness.py +++ b/tests/test_check_freshness.py @@ -8,6 +8,8 @@ report is folded, since each of those decides whether a row says STALE. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import sys @@ -316,8 +318,23 @@ class CoreResolutionTests(unittest.TestCase): record.write_text(cf.json.dumps({"blobs": {"a": "1", "b": "1"}})) cf.record_core_info({"a": "2", "b": "2"}, ["a"], record) self.assertEqual(cf.load_core_info_record(record), {"a": "2", "b": "1"}) - with self.assertRaises(ValueError): - cf.record_core_info({"a": "2"}, ["ghost"], record) + with self.assertRaises(cf.InputMissing) as raised: + cf.record_core_info({"a": "2", "ghosts": "1"}, ["ghost"], record) + self.assertIn("Did you mean: ghosts?", str(raised.exception)) + + def test_record_errors_are_one_line(self): + """A mistyped core and an empty offline cache ended on a traceback.""" + import subprocess # noqa: PLC0415 + + with tempfile.TemporaryDirectory() as tmp: + proc = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / "check_freshness.py"), "--offline", + "--cache-dir", tmp, "--record-coreinfo"], + capture_output=True, text=True, check=False, timeout=120, + ) + self.assertEqual(proc.returncode, 1) + self.assertNotIn("Traceback", proc.stderr) + self.assertIn("run without --offline", proc.stderr) def test_offline_without_a_cached_listing_is_skipped(self): """Offline, the row read UNKNOWN and failed the run as an upstream @@ -460,10 +477,28 @@ class CatalogParsingTests(unittest.TestCase): row = cf._check_redump_games(False) self.assertEqual((row.status, row.items), (cf.STALE, ["sony-playstation"])) self.assertEqual(cf._check_redump_games(True).status, cf.SKIPPED) - with mock.patch.object(cf, "_game_catalogs", return_value=[{"fetched": "2026-11-03"}]): - for rebuilt, status in (("2026-07-08", cf.OK), ("2026-11-04", cf.STALE)): + with mock.patch.object(cf, "_game_catalogs", return_value=[{"fetched": "2026-11-03"}]), \ + mock.patch.object(cf, "_today", return_value="2026-11-06"): + for rebuilt, status in (("2026-11-02", cf.OK), ("2026-11-04", cf.STALE)): with mock.patch.object(cf, "_no_intro_asset_date", return_value=rebuilt): self.assertEqual(cf._check_nointro_games("", False).status, status) + + def test_a_mirror_that_stopped_rebuilding_is_stale(self): + """The mirror failed every daily rebuild from 2026-07-08, and both + No-Intro rows read OK: an import newer than a dead pack is no + sign of a fresh catalog.""" + with mock.patch.object(cf, "_today", return_value="2026-11-06"), \ + mock.patch.object(cf, "_no_intro_asset_date", return_value="2026-07-08"), \ + mock.patch.object(cf, "_game_catalogs", return_value=[{"fetched": "2026-11-03"}]), \ + mock.patch.object(cf, "_snapshot", return_value={"imported_at": "2026-08-08"}): + for row in (cf._check_nointro_games("", False), cf._check_no_intro("", False)): + self.assertEqual(row.status, cf.STALE) + self.assertIn("2026-07-08, 121 days ago", row.detail) + + def test_the_freshness_check_reads_the_importer_mirror(self): + self.assertEqual(cf.NO_INTRO_MIRROR, cf.game_dat_importer.NOINTRO_MIRROR) + self.assertIn(cf.NO_INTRO_MIRROR.removeprefix("https://github.com/"), + cf.game_dat_importer.NOINTRO_RELEASE_API) self.assertIn("no-intro games", [f.subject for f in cf.check_catalogs("", True)]) diff --git a/tests/test_check_release.py b/tests/test_check_release.py index 50a305f7c..c821b7bab 100644 --- a/tests/test_check_release.py +++ b/tests/test_check_release.py @@ -7,6 +7,8 @@ whose checksums every user's `sha256sum --check` rejected. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_checks_have_values.py b/tests/test_checks_have_values.py index 91c1fe841..8d9cea7a3 100644 --- a/tests/test_checks_have_values.py +++ b/tests/test_checks_have_values.py @@ -6,6 +6,8 @@ one. validate_schemas now refuses it. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import sys import unittest diff --git a/tests/test_cited_lines.py b/tests/test_cited_lines.py index bbf380f4f..1cf8794e5 100644 --- a/tests/test_cited_lines.py +++ b/tests/test_cited_lines.py @@ -7,6 +7,8 @@ does not hold the file. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_contributing_text.py b/tests/test_contributing_text.py index 593ad04fa..3e16e4838 100644 --- a/tests/test_contributing_text.py +++ b/tests/test_contributing_text.py @@ -6,6 +6,8 @@ the repository, five on the site, under two different titles. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_core_data_trees.py b/tests/test_core_data_trees.py index f9de142f5..2fb6e7f3e 100644 --- a/tests/test_core_data_trees.py +++ b/tests/test_core_data_trees.py @@ -10,6 +10,8 @@ reader now gets the tree, or the profile says its package installs it. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_core_file_fills_declaration.py b/tests/test_core_file_fills_declaration.py index 1f9260057..499f67dd2 100644 --- a/tests/test_core_file_fills_declaration.py +++ b/tests/test_core_file_fills_declaration.py @@ -8,6 +8,8 @@ frontend sees a file at the path. verify kept it MISSING, so the pack said from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import contextlib import io diff --git a/tests/test_core_probe.py b/tests/test_core_probe.py index 60a8e1fc8..1c866ffc7 100644 --- a/tests/test_core_probe.py +++ b/tests/test_core_probe.py @@ -7,6 +7,8 @@ probe on it: staging, the worker process, strace, the record and its schema. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import contextlib import hashlib diff --git a/tests/test_core_size_refusal.py b/tests/test_core_size_refusal.py index 32f5615bc..8c4a437f6 100644 --- a/tests/test_core_size_refusal.py +++ b/tests/test_core_size_refusal.py @@ -10,6 +10,8 @@ read one rule (validation.core_size_refusal). from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_coverage_cache.py b/tests/test_coverage_cache.py index 8c2b56175..8dd05038b 100644 --- a/tests/test_coverage_cache.py +++ b/tests/test_coverage_cache.py @@ -9,6 +9,8 @@ tests change an input and expect a fresh computation. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_cross_reference_sources.py b/tests/test_cross_reference_sources.py index 67de9cbfa..6f96e2d8e 100644 --- a/tests/test_cross_reference_sources.py +++ b/tests/test_cross_reference_sources.py @@ -9,6 +9,8 @@ a directory called ROM). from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_crypto_aes.py b/tests/test_crypto_aes.py index fd399d15f..1e86433f1 100644 --- a/tests/test_crypto_aes.py +++ b/tests/test_crypto_aes.py @@ -7,6 +7,8 @@ contributor with none of them. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import sys import unittest diff --git a/tests/test_crypto_unique_data.py b/tests/test_crypto_unique_data.py index b2e2d6ae8..a7428f68f 100644 --- a/tests/test_crypto_unique_data.py +++ b/tests/test_crypto_unique_data.py @@ -18,6 +18,8 @@ Layouts, from Azahar src/core/hw/unique_data.cpp and src/core/file_sys/otp.cpp: from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import struct import sys diff --git a/tests/test_crypto_verify.py b/tests/test_crypto_verify.py index 53e8e1ab7..a6f160bdd 100644 --- a/tests/test_crypto_verify.py +++ b/tests/test_crypto_verify.py @@ -14,6 +14,8 @@ constants rather than against itself. The AES vector is from NIST SP 800-38A. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import shutil import sys diff --git a/tests/test_data_dir_refs.py b/tests/test_data_dir_refs.py index df5d00bac..9793ea24d 100644 --- a/tests/test_data_dir_refs.py +++ b/tests/test_data_dir_refs.py @@ -6,6 +6,8 @@ verify only said "not cached" and refresh_data_dirs had nothing to fetch. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_dataclass_defaults.py b/tests/test_dataclass_defaults.py index 53019b75d..fd9fbca22 100644 --- a/tests/test_dataclass_defaults.py +++ b/tests/test_dataclass_defaults.py @@ -7,6 +7,8 @@ TypeError on the first loop. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import unittest from pathlib import Path diff --git a/tests/test_db_aliases.py b/tests/test_db_aliases.py index 1c456e65c..0c51d415f 100644 --- a/tests/test_db_aliases.py +++ b/tests/test_db_aliases.py @@ -8,6 +8,8 @@ disk ROM then served the CoCo one. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import tempfile diff --git a/tests/test_declared_names.py b/tests/test_declared_names.py index 42868fe4b..630c60aa3 100644 --- a/tests/test_declared_names.py +++ b/tests/test_declared_names.py @@ -8,6 +8,8 @@ set, enriched with every spelling the database knows. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_dedup.py b/tests/test_dedup.py index d0badfbae..0b6d31790 100644 --- a/tests/test_dedup.py +++ b/tests/test_dedup.py @@ -9,6 +9,8 @@ are both load-bearing. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import os diff --git a/tests/test_derivations.py b/tests/test_derivations.py index 580bf32e5..061205b0b 100644 --- a/tests/test_derivations.py +++ b/tests/test_derivations.py @@ -7,6 +7,8 @@ is two held ROMs end to end. The gap report listed them as acquisition targets. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import shutil diff --git a/tests/test_deterministic_zip.py b/tests/test_deterministic_zip.py index 359f508c2..293f84b91 100644 --- a/tests/test_deterministic_zip.py +++ b/tests/test_deterministic_zip.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import tempfile diff --git a/tests/test_directory_entries.py b/tests/test_directory_entries.py index e8472f32f..3bd952006 100644 --- a/tests/test_directory_entries.py +++ b/tests/test_directory_entries.py @@ -9,6 +9,8 @@ reported missing while 3 584 collected files stayed out of every pack. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import sys diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index cdd6c3ed1..dbdf0c8b5 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -7,6 +7,8 @@ code moves, and nothing else notices. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import hashlib import inspect diff --git a/tests/test_doc_surfaces.py b/tests/test_doc_surfaces.py index b7a53ee06..75daca3d0 100644 --- a/tests/test_doc_surfaces.py +++ b/tests/test_doc_surfaces.py @@ -9,6 +9,8 @@ tool prints, and a size is the one the release record holds. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import contextlib import re diff --git a/tests/test_download.py b/tests/test_download.py index 154f780ca..8923a2134 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -9,6 +9,8 @@ checksum of each layout and the staging location. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import functools import hashlib diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 894b43a44..d876b56d4 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -17,6 +17,8 @@ Covers: from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_elec_scrapers.py b/tests/test_elec_scrapers.py index aeace915c..0ee2f1950 100644 --- a/tests/test_elec_scrapers.py +++ b/tests/test_elec_scrapers.py @@ -9,6 +9,8 @@ upstream files. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_emuelec_targets.py b/tests/test_emuelec_targets.py index c8f911433..77e8b2ec1 100644 --- a/tests/test_emuelec_targets.py +++ b/tests/test_emuelec_targets.py @@ -9,6 +9,8 @@ firmware of cores its image never had. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_emulator_pack_counts.py b/tests/test_emulator_pack_counts.py index 10faa9154..654ff5c20 100644 --- a/tests/test_emulator_pack_counts.py +++ b/tests/test_emulator_pack_counts.py @@ -9,6 +9,8 @@ names and ten "missing". from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import subprocess import sys diff --git a/tests/test_entry_categories.py b/tests/test_entry_categories.py index 0f971c5d9..bd1e849d2 100644 --- a/tests/test_entry_categories.py +++ b/tests/test_entry_categories.py @@ -6,6 +6,8 @@ from what the cores the platform runs open, never from the entry's name. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_export_counts.py b/tests/test_export_counts.py index f0abfc261..79734c9bd 100644 --- a/tests/test_export_counts.py +++ b/tests/test_export_counts.py @@ -9,6 +9,8 @@ from a file other than the one the hash describes. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_export_native_layer.py b/tests/test_export_native_layer.py index 6e93642d5..7c9446218 100644 --- a/tests/test_export_native_layer.py +++ b/tests/test_export_native_layer.py @@ -9,6 +9,8 @@ libretro's System.dat (N88EXT0-3.ROM) as if libretro declared them. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_export_report.py b/tests/test_export_report.py index 1b25ab45e..e3127fe9f 100644 --- a/tests/test_export_report.py +++ b/tests/test_export_report.py @@ -7,6 +7,8 @@ file from and no list to check the diff against. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import tempfile diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 8dc24b4ff..d4181744c 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -14,6 +14,8 @@ and skip when it has not been populated (python scripts/export_native.py from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import difflib import io import json @@ -801,14 +803,20 @@ class SearchOrder(unittest.TestCase): longest = ('rp_module_id="xm7"\nrp_module_help="Copy bios files ' + ", ".join(f"n{i}.rom" for i in range(12)) + ' and n12.rom to $biosdir"\n') exporter = discover_exporters()["retropie"]() - produced = exporter.render( + written = exporter.render( systems, None, {"scriptmodules/libretrocores/lr-flycast.sh": script, "scriptmodules/emulators/xm7.sh": longest}, - )["scriptmodules/libretrocores/lr-flycast.sh"] + ) + produced = written["scriptmodules/libretrocores/lr-flycast.sh"] self.assertIn("Dreamcast BIOS file dc_boot.bin to", produced) self.assertIn("BIOS files naomi.zip, awbios.zip and naomi2.zip to", produced) self.assertNotIn("other.zip", produced) + # Rewritten for naomi2.zip, other.zip left out: the summary said + # "Left as is" of a package in the diff. + outcome = exporter.outcome(systems, written) + self.assertIn("Updated in part: lr-flycast,", outcome) + self.assertNotIn("Left as is: lr-flycast", outcome) def test_cores_that_disagree_keep_the_best_rank(self): """Nothing is dropped on the rank here, so the best one wins. @@ -827,6 +835,70 @@ class SearchOrder(unittest.TestCase): self.assertEqual(entry.priority, 2) +class TwoAdditionsAtOnePath(unittest.TestCase): + """Two cores declaring two dumps of one file the platform lacks: both + were counted added while the render wrote one. Recalbox's md5 list takes + both, a System.dat name and a RetroDECK path hold one.""" + + A, B = "615a" + "0" * 28, "ef49" + "0" * 28 + + def _addition(self, md5: str, core: str, system: str = "dos") -> NativeFile: + return NativeFile( + name="MT32_CONTROL.ROM", destination="mt32/MT32_CONTROL.ROM", native_system=system, + truth={"name": "MT32_CONTROL.ROM", "path": "mt32/MT32_CONTROL.ROM", "md5": md5, + "crc32": md5[:8], "required": True, "_cores": [core]}, + runners=[(False, core)], + ) + + def _added(self, exporter, systems) -> list[str]: + return [c["name"] for c in landed_changes(exporter, systems).records + if c["kind"] == "added"] + + def test_recalbox_writes_both_md5_in_one_element(self): + systems = {"dos": NativeSystem("dos", files=[ + self._addition(self.A, "dosbox_pure"), self._addition(self.B, "dosbox_core")])} + exporter = discover_exporters()["recalbox"]() + produced = exporter.render(systems, Report(), {})["es_bios.xml"] + elements = [e for e in ET.fromstring(produced).iter("bios")] + self.assertEqual(len(elements), 1) + self.assertEqual(elements[0].get("md5").split(","), [self.A, self.B]) + self.assertEqual(elements[0].get("core"), "libretro/dosbox_pure,libretro/dosbox_core") + self.assertEqual(exporter.validate(systems, {"es_bios.xml": produced}), []) + self.assertEqual(len(self._added(exporter, systems)), 2) + + def test_system_dat_counts_the_line_it_wrote(self): + systems = {"DOS": NativeSystem("DOS", files=[ + self._addition(self.A, "dosbox_pure", "DOS"), + self._addition(self.B, "dosbox_core", "DOS")])} + exporter = discover_exporters()["retroarch"]() + original = 'clrmamepro (\n\tname "System"\n)\n\ngame (\n\tname "System"\n' \ + '\tcomment "System"\n\n\tcomment "DOS"\n\trom ( name a.bin size 1 crc 00000000 )\n)\n' + produced = exporter.render(systems, Report(), {"System.dat": original})["System.dat"] + self.assertEqual(produced.count("MT32_CONTROL.ROM"), 1) + self.assertEqual(len(self._added(exporter, systems)), 1) + + def test_retrodeck_counts_the_entry_it_appended(self): + theirs = NativeFile( + name="scph1001.bin", destination="scph1001.bin", native_system="psx", + platform={"name": "scph1001.bin", "md5": "1" * 32, "component": "duckstation"}) + + def addition(md5: str) -> NativeFile: + return NativeFile( + name="scph5501.bin", destination="scph5501.bin", native_system="psx", + truth={"name": "scph5501.bin", "md5": md5, "required": True, + "_cores": ["duckstation"]}, + runners=[(True, "duckstation")]) + + systems = {"psx": NativeSystem("psx", files=[theirs, addition(self.A), addition(self.B)])} + manifest = json.dumps({"duckstation": {"name": "DuckStation", "bios": [ + {"filename": "scph1001.bin", "md5": "1" * 32, "system": "psx"}]}}) + exporter = discover_exporters()["retrodeck"]() + produced = exporter.render(systems, Report(), {"duckstation/component_manifest.json": manifest}) + bios = json.loads(produced["duckstation/component_manifest.json"])["duckstation"]["bios"] + self.assertEqual([e["filename"] for e in bios], ["scph1001.bin", "scph5501.bin"]) + self.assertEqual(self._added(exporter, systems), ["scph5501.bin"]) + + class AdditionOrder(unittest.TestCase): """What we add is listed the way the code looks for it.""" diff --git a/tests/test_extra_identity.py b/tests/test_extra_identity.py index a0761e39c..1f1faca65 100644 --- a/tests/test_extra_identity.py +++ b/tests/test_extra_identity.py @@ -8,6 +8,8 @@ Galaksija ROM and a PS2 ROM2.BIN. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_extract_targets.py b/tests/test_extract_targets.py index 9ba78bf4b..59c8022f8 100644 --- a/tests/test_extract_targets.py +++ b/tests/test_extract_targets.py @@ -9,6 +9,8 @@ have vanished with it. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_fbneo_parser.py b/tests/test_fbneo_parser.py index 4a16c2c2b..583e75140 100644 --- a/tests/test_fbneo_parser.py +++ b/tests/test_fbneo_parser.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import tempfile import unittest from pathlib import Path diff --git a/tests/test_file_consumers.py b/tests/test_file_consumers.py index 1d382916f..87b2495ee 100644 --- a/tests/test_file_consumers.py +++ b/tests/test_file_consumers.py @@ -8,6 +8,8 @@ homonym tests/test_game_data_homonyms.py forbids the resolver to serve. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_fileless_audit.py b/tests/test_fileless_audit.py index 7d469ab4f..8409bb19c 100644 --- a/tests/test_fileless_audit.py +++ b/tests/test_fileless_audit.py @@ -8,6 +8,8 @@ directory, and only a manual reading found it. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_finalize_pack_names.py b/tests/test_finalize_pack_names.py index fe7c8e686..7dff70d1c 100644 --- a/tests/test_finalize_pack_names.py +++ b/tests/test_finalize_pack_names.py @@ -7,6 +7,8 @@ attached to misterfpga by substring and checked against the full list: from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import tempfile diff --git a/tests/test_find_pinned_revision.py b/tests/test_find_pinned_revision.py index 473633fd5..974010a55 100644 --- a/tests/test_find_pinned_revision.py +++ b/tests/test_find_pinned_revision.py @@ -6,6 +6,8 @@ today's upstream no longer carries, but the bytes are in the project's history. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import unittest diff --git a/tests/test_flycast_layout.py b/tests/test_flycast_layout.py index 81e60b699..3bbd01358 100644 --- a/tests/test_flycast_layout.py +++ b/tests/test_flycast_layout.py @@ -9,6 +9,8 @@ and put every file one directory too deep. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import unittest from pathlib import Path diff --git a/tests/test_foreign_tree.py b/tests/test_foreign_tree.py index 47056e553..b8260e8af 100644 --- a/tests/test_foreign_tree.py +++ b/tests/test_foreign_tree.py @@ -9,6 +9,8 @@ to upstream PCSX2. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_foreign_tree_names.py b/tests/test_foreign_tree_names.py index 360060add..258401f6b 100644 --- a/tests/test_foreign_tree_names.py +++ b/tests/test_foreign_tree_names.py @@ -9,6 +9,8 @@ served while data/bluemsx held the 8 KB one. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import shutil diff --git a/tests/test_frontend_digest.py b/tests/test_frontend_digest.py index 14b260385..a4ce6a5cd 100644 --- a/tests/test_frontend_digest.py +++ b/tests/test_frontend_digest.py @@ -8,6 +8,8 @@ the pack and the manifest while verify, comparing md5, called it OK. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_game_catalogs.py b/tests/test_game_catalogs.py index 306d78844..ef3873ed3 100644 --- a/tests/test_game_catalogs.py +++ b/tests/test_game_catalogs.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import subprocess import sys diff --git a/tests/test_game_data_homonyms.py b/tests/test_game_data_homonyms.py index 6e5cc144f..c39d95e43 100644 --- a/tests/test_game_data_homonyms.py +++ b/tests/test_game_data_homonyms.py @@ -12,6 +12,8 @@ carries `unsourceable:` until its own bytes are held. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import re diff --git a/tests/test_game_headers.py b/tests/test_game_headers.py index 6a3c63087..1b495bb66 100644 --- a/tests/test_game_headers.py +++ b/tests/test_game_headers.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import sys diff --git a/tests/test_game_regions.py b/tests/test_game_regions.py index 97f660eae..b3b76cf6d 100644 --- a/tests/test_game_regions.py +++ b/tests/test_game_regions.py @@ -8,6 +8,8 @@ can lose its backing, then on the repository's own files. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import copy import json import sys diff --git a/tests/test_games_index.py b/tests/test_games_index.py index b7894b42f..07c05e459 100644 --- a/tests/test_games_index.py +++ b/tests/test_games_index.py @@ -7,6 +7,8 @@ so a test pins the join and not the state of the data. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import shutil import sys diff --git a/tests/test_gap_settlement.py b/tests/test_gap_settlement.py index 683044985..917507662 100644 --- a/tests/test_gap_settlement.py +++ b/tests/test_gap_settlement.py @@ -7,6 +7,8 @@ it. RetroArch names Galaksija's 4 KiB galaksija/rom1.bin, so DOSBox's from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_handheld_launchers.py b/tests/test_handheld_launchers.py index ff8d793d9..633d6259a 100644 --- a/tests/test_handheld_launchers.py +++ b/tests/test_handheld_launchers.py @@ -10,6 +10,8 @@ the pad reaching the console, a compositor covering it) is not exercised. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import importlib.util import json diff --git a/tests/test_hash_merge.py b/tests/test_hash_merge.py index 079dfe4e5..8b63de4b3 100644 --- a/tests/test_hash_merge.py +++ b/tests/test_hash_merge.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import tempfile import unittest diff --git a/tests/test_hashing.py b/tests/test_hashing.py index d6c6b71e8..6237e4330 100644 --- a/tests/test_hashing.py +++ b/tests/test_hashing.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import shutil import sys import tempfile diff --git a/tests/test_hidden_extras.py b/tests/test_hidden_extras.py index 222b018fa..452c3205c 100644 --- a/tests/test_hidden_extras.py +++ b/tests/test_hidden_extras.py @@ -9,6 +9,8 @@ ROCKNIX's higan SGB1.sfc/program.rom under the file SGB1.sfc. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_identity.py b/tests/test_identity.py index a90f9796b..fce2c7ca9 100644 --- a/tests/test_identity.py +++ b/tests/test_identity.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import shutil import sys import tempfile diff --git a/tests/test_install.py b/tests/test_install.py index 086d9b697..9effcdd49 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -1,6 +1,8 @@ """Tests for install.py platform detection and config parsing.""" from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import functools import hashlib diff --git a/tests/test_install_app.py b/tests/test_install_app.py index 94d190236..34a4e499d 100644 --- a/tests/test_install_app.py +++ b/tests/test_install_app.py @@ -8,6 +8,8 @@ menu) is not exercised. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import io import json diff --git a/tests/test_install_cores.py b/tests/test_install_cores.py index 55fcbfbf0..7594890fe 100644 --- a/tests/test_install_cores.py +++ b/tests/test_install_cores.py @@ -8,6 +8,8 @@ which cores keep each system, the rule a pack built for a target follows. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import json diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 2ea164a30..840b948e2 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -7,6 +7,8 @@ honours Retry-After, and large files that resume where they stopped. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import errno import hashlib diff --git a/tests/test_install_from.py b/tests/test_install_from.py index ff28c52c7..9b91ed553 100644 --- a/tests/test_install_from.py +++ b/tests/test_install_from.py @@ -11,6 +11,8 @@ never moved, and checked again once written. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import copy import errno diff --git a/tests/test_install_games.py b/tests/test_install_games.py index 117970364..8388202d0 100644 --- a/tests/test_install_games.py +++ b/tests/test_install_games.py @@ -8,6 +8,8 @@ index, published by scripts/generate_games_index.py. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import functools import gzip diff --git a/tests/test_install_offline.py b/tests/test_install_offline.py index 08d9194d2..2eeb18530 100644 --- a/tests/test_install_offline.py +++ b/tests/test_install_offline.py @@ -9,6 +9,8 @@ installs again from disk, --from-pack, every file checked by its hash. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_install_selection.py b/tests/test_install_selection.py index d0cc9dac3..d96329ae7 100644 --- a/tests/test_install_selection.py +++ b/tests/test_install_selection.py @@ -9,6 +9,8 @@ to one answer. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import functools import hashlib diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index 988b7234e..a7584e71b 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -9,6 +9,8 @@ installs it). from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import contextlib import errno diff --git a/tests/test_lakka_targets.py b/tests/test_lakka_targets.py index 387ac1502..e4fb1cb31 100644 --- a/tests/test_lakka_targets.py +++ b/tests/test_lakka_targets.py @@ -7,6 +7,8 @@ platform outright. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import unittest from scripts.scraper.targets import lakka_targets_scraper as lakka diff --git a/tests/test_large_file_cache.py b/tests/test_large_file_cache.py index 909ede2cf..bd4dd01d5 100644 --- a/tests/test_large_file_cache.py +++ b/tests/test_large_file_cache.py @@ -8,6 +8,8 @@ that then replaced the cache entry. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import io import json diff --git a/tests/test_launcher_aliases.py b/tests/test_launcher_aliases.py index bd4d5331d..ab7795a02 100644 --- a/tests/test_launcher_aliases.py +++ b/tests/test_launcher_aliases.py @@ -16,6 +16,8 @@ Two checks read the committed data: from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import unittest from collections import defaultdict diff --git a/tests/test_launcher_layer.py b/tests/test_launcher_layer.py index e765fe27e..a7f99c230 100644 --- a/tests/test_launcher_layer.py +++ b/tests/test_launcher_layer.py @@ -8,6 +8,8 @@ and a fixed path the launcher passes (Batocera's x16emu shape). from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import inspect import os diff --git a/tests/test_launchers.py b/tests/test_launchers.py index 515d242ee..12a7d06ff 100644 --- a/tests/test_launchers.py +++ b/tests/test_launchers.py @@ -8,6 +8,8 @@ is checked against the revision itself. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import dataclasses import json import sys diff --git a/tests/test_launchers_emudeck.py b/tests/test_launchers_emudeck.py index 33d7596fe..1dd106f6c 100644 --- a/tests/test_launchers_emudeck.py +++ b/tests/test_launchers_emudeck.py @@ -8,6 +8,8 @@ fixture's own lines, and a construct that is gone must stop the scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import textwrap import unittest diff --git a/tests/test_launchers_recalbox.py b/tests/test_launchers_recalbox.py index 5c806f757..381920bc9 100644 --- a/tests/test_launchers_recalbox.py +++ b/tests/test_launchers_recalbox.py @@ -8,6 +8,8 @@ is checked against the tag itself. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_launchers_retrobat.py b/tests/test_launchers_retrobat.py index 4a5d80fa6..4d1591452 100644 --- a/tests/test_launchers_retrobat.py +++ b/tests/test_launchers_retrobat.py @@ -8,6 +8,8 @@ the fixture's own lines, and a construct that is gone must stop the scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import textwrap import unittest diff --git a/tests/test_launchers_retrodeck.py b/tests/test_launchers_retrodeck.py index a4af6844f..96401837c 100644 --- a/tests/test_launchers_retrodeck.py +++ b/tests/test_launchers_retrodeck.py @@ -8,6 +8,8 @@ cites. A construct that is gone or unknown must stop the scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import textwrap import unittest diff --git a/tests/test_launchers_retropie.py b/tests/test_launchers_retropie.py index 1412ae5e0..0e5ef9c66 100644 --- a/tests/test_launchers_retropie.py +++ b/tests/test_launchers_retropie.py @@ -8,6 +8,8 @@ cites. A construct that is gone or unknown must stop the scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import dataclasses import json import sys diff --git a/tests/test_launchers_rocknix.py b/tests/test_launchers_rocknix.py index b49097ae3..22596256b 100644 --- a/tests/test_launchers_rocknix.py +++ b/tests/test_launchers_rocknix.py @@ -9,6 +9,8 @@ configs differ, and a construct that is gone must stop the scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import textwrap import unittest diff --git a/tests/test_libretro_layout.py b/tests/test_libretro_layout.py index 76b089802..6b0aa29fb 100644 --- a/tests/test_libretro_layout.py +++ b/tests/test_libretro_layout.py @@ -9,6 +9,8 @@ Sys from system/dolphin-emu/Sys and keeps User in the save directory. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_libretro_slugs.py b/tests/test_libretro_slugs.py index 1b52ce124..1d571b002 100644 --- a/tests/test_libretro_slugs.py +++ b/tests/test_libretro_slugs.py @@ -9,6 +9,8 @@ duplicate system carrying the quasi88 group. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_maker_folder_names.py b/tests/test_maker_folder_names.py index 8410ac58e..cffe035af 100644 --- a/tests/test_maker_folder_names.py +++ b/tests/test_maker_folder_names.py @@ -11,6 +11,8 @@ remains. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import os diff --git a/tests/test_mame_devices.py b/tests/test_mame_devices.py index 4b34edde0..05aa8f36e 100644 --- a/tests/test_mame_devices.py +++ b/tests/test_mame_devices.py @@ -9,6 +9,8 @@ filed under the systems of the machines that can load it, never guessed. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import json import tempfile diff --git a/tests/test_mame_parser.py b/tests/test_mame_parser.py index 288e2afd8..c163e845b 100644 --- a/tests/test_mame_parser.py +++ b/tests/test_mame_parser.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import tempfile import unittest diff --git a/tests/test_mame_ref_audit.py b/tests/test_mame_ref_audit.py index 69d60ae44..7f6113739 100644 --- a/tests/test_mame_ref_audit.py +++ b/tests/test_mame_ref_audit.py @@ -7,6 +7,8 @@ found nineteen stale refs in one driver where profile_sync had flagged five. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_manifest_entry.py b/tests/test_manifest_entry.py index 3505bbf26..ad18dfe2d 100644 --- a/tests/test_manifest_entry.py +++ b/tests/test_manifest_entry.py @@ -9,6 +9,8 @@ an empty repo_path, and the installer refused the whole manifest. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import json import sys diff --git a/tests/test_md5_pack_context.py b/tests/test_md5_pack_context.py index d3b572206..bac9ef345 100644 --- a/tests/test_md5_pack_context.py +++ b/tests/test_md5_pack_context.py @@ -8,6 +8,8 @@ the bare file names under a pack called Standalone. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import subprocess import sys diff --git a/tests/test_measurements.py b/tests/test_measurements.py index 692b409f3..e38e3ad2a 100644 --- a/tests/test_measurements.py +++ b/tests/test_measurements.py @@ -8,6 +8,8 @@ against HEAD. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import copy import html import json diff --git a/tests/test_minui_platform.py b/tests/test_minui_platform.py index 8807c22f4..5e6ed3ef3 100644 --- a/tests/test_minui_platform.py +++ b/tests/test_minui_platform.py @@ -11,6 +11,8 @@ profiles, placed below that folder. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_name_tiebreak.py b/tests/test_name_tiebreak.py index 29cd9566b..9270a30c2 100644 --- a/tests/test_name_tiebreak.py +++ b/tests/test_name_tiebreak.py @@ -7,6 +7,8 @@ without `validation: [size]` keeps its informative status. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_native_mode.py b/tests/test_native_mode.py index 100412e98..51bf58895 100644 --- a/tests/test_native_mode.py +++ b/tests/test_native_mode.py @@ -10,6 +10,8 @@ it, and these tests make the agreement checkable instead of reviewable. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import json diff --git a/tests/test_nec_filing.py b/tests/test_nec_filing.py index 68335af0d..329cf7f08 100644 --- a/tests/test_nec_filing.py +++ b/tests/test_nec_filing.py @@ -9,6 +9,8 @@ no PC-88 emulator reads. Reads the repository. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_no_case_collisions.py b/tests/test_no_case_collisions.py index c9821676c..a724cfca2 100644 --- a/tests/test_no_case_collisions.py +++ b/tests/test_no_case_collisions.py @@ -1,5 +1,7 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import unittest from collections import defaultdict diff --git a/tests/test_no_game_data.py b/tests/test_no_game_data.py index 54e835c57..2bb59c2fc 100644 --- a/tests/test_no_game_data.py +++ b/tests/test_no_game_data.py @@ -9,6 +9,8 @@ full manifest. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import functools import hashlib diff --git a/tests/test_np2_fonts.py b/tests/test_np2_fonts.py index fe417bff0..0ff2196dd 100644 --- a/tests/test_np2_fonts.py +++ b/tests/test_np2_fonts.py @@ -9,6 +9,8 @@ which fontpc98_read cannot parse, and FONT.ROM was never placed under its name. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import shutil diff --git a/tests/test_optional_dependencies.py b/tests/test_optional_dependencies.py index 1ef0b7a41..feba5a1ee 100644 --- a/tests/test_optional_dependencies.py +++ b/tests/test_optional_dependencies.py @@ -7,6 +7,8 @@ fifteen errors instead of skipped schema checks. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import sys import unittest diff --git a/tests/test_owner_resolution.py b/tests/test_owner_resolution.py index f135bd1fa..70141c51a 100644 --- a/tests/test_owner_resolution.py +++ b/tests/test_owner_resolution.py @@ -9,6 +9,8 @@ armsx2's pack ships without a reader, and MAME's neogeo.zip without MAME. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_pack_conformance_dirs.py b/tests/test_pack_conformance_dirs.py index f17d77eb0..659564f6c 100644 --- a/tests/test_pack_conformance_dirs.py +++ b/tests/test_pack_conformance_dirs.py @@ -8,6 +8,8 @@ all and never judged. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import sys import tempfile diff --git a/tests/test_pack_core_extras.py b/tests/test_pack_core_extras.py index 266763d14..9e659369b 100644 --- a/tests/test_pack_core_extras.py +++ b/tests/test_pack_core_extras.py @@ -9,6 +9,8 @@ builder's in the same run. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_pack_counts.py b/tests/test_pack_counts.py index 1d12c5423..55f34e029 100644 --- a/tests/test_pack_counts.py +++ b/tests/test_pack_counts.py @@ -10,6 +10,8 @@ installer fetches and leaves out the data directories a pack also carries. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import io import json diff --git a/tests/test_pack_destinations.py b/tests/test_pack_destinations.py index 65bd1277d..cf41ce6a3 100644 --- a/tests/test_pack_destinations.py +++ b/tests/test_pack_destinations.py @@ -9,6 +9,8 @@ it and counted one more file from the cores. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_pack_failures.py b/tests/test_pack_failures.py index 5e73ac684..9ec1e7d1d 100644 --- a/tests/test_pack_failures.py +++ b/tests/test_pack_failures.py @@ -7,6 +7,8 @@ keep serving from an older run, left the run green. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import os import sys diff --git a/tests/test_pack_file_names.py b/tests/test_pack_file_names.py index 30bea04c0..cf5c4de43 100644 --- a/tests/test_pack_file_names.py +++ b/tests/test_pack_file_names.py @@ -7,6 +7,8 @@ verify reported the same scope OK. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_pack_grouping.py b/tests/test_pack_grouping.py index d24674c4a..5a45a6740 100644 --- a/tests/test_pack_grouping.py +++ b/tests/test_pack_grouping.py @@ -8,6 +8,8 @@ ruled undecidable, and the keep overrode the drop. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_pack_integrity.py b/tests/test_pack_integrity.py index 57bc09dc1..a7e7c704e 100644 --- a/tests/test_pack_integrity.py +++ b/tests/test_pack_integrity.py @@ -14,6 +14,8 @@ date pack stays a failure. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import glob import os import subprocess diff --git a/tests/test_pack_member_identity.py b/tests/test_pack_member_identity.py index 63d15f47e..5eeb7d64b 100644 --- a/tests/test_pack_member_identity.py +++ b/tests/test_pack_member_identity.py @@ -8,6 +8,8 @@ recognised by their members. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import io import json diff --git a/tests/test_pack_probe.py b/tests/test_pack_probe.py index de96601cd..a729dc933 100644 --- a/tests/test_pack_probe.py +++ b/tests/test_pack_probe.py @@ -11,6 +11,8 @@ on disk. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import json @@ -297,12 +299,19 @@ class RecordsNameNoLocalPath(unittest.TestCase): self.assertEqual(summary, "Copy it from your own copy of the game into /jazz2/Source/. " "Saves go to .") - def test_no_committed_record_holds_an_absolute_path(self) -> None: - path = REPO_ROOT / "measurements" / "pack-probes.jsonl" + def test_no_committed_record_holds_a_local_path(self) -> None: + """Ten records cut at 400 characters inside the scratch path kept + its tail, which no absolute-path pattern sees.""" absolute = re.compile(r"(?-])/(?:home|root|tmp|mnt|Users|srv)/") - records = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()] - found = [r["id"] for r in records - if absolute.search((r.get("source") or {}).get("installer_summary", ""))] + # The tail of a scratch folder: a worktree, or the probes' own tmp/ + # subfolders. + scratch = re.compile(r"worktrees/|\btmp/(?:pack_probe|tests|campaign)\b") + found = [ + f"{path.name}:{number}" + for path in sorted((REPO_ROOT / "measurements").glob("*.jsonl")) + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1) + if absolute.search(line) or scratch.search(line) + ] self.assertEqual(found, []) diff --git a/tests/test_pack_readme.py b/tests/test_pack_readme.py index 5a56085cf..83748d59c 100644 --- a/tests/test_pack_readme.py +++ b/tests/test_pack_readme.py @@ -8,6 +8,8 @@ does not have, while the registry names the path. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_path_conflict_status.py b/tests/test_path_conflict_status.py index 5d7c60690..653064812 100644 --- a/tests/test_path_conflict_status.py +++ b/tests/test_path_conflict_status.py @@ -8,6 +8,8 @@ from the collection read as covered in the pack report and as missing in verify. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_path_null_copies.py b/tests/test_path_null_copies.py index eb2a405ea..d3b583c5e 100644 --- a/tests/test_path_null_copies.py +++ b/tests/test_path_null_copies.py @@ -8,6 +8,8 @@ Reads the repository: skips when the database is absent. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_path_suffix.py b/tests/test_path_suffix.py index 865996528..62ee65d56 100644 --- a/tests/test_path_suffix.py +++ b/tests/test_path_suffix.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import shutil import sys diff --git a/tests/test_pin_build.py b/tests/test_pin_build.py index c4f7a4568..ecd3731bc 100644 --- a/tests/test_pin_build.py +++ b/tests/test_pin_build.py @@ -8,6 +8,8 @@ file, and check_freshness reads it again where the platform now stands. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_platform_inheritance.py b/tests/test_platform_inheritance.py index 10473c882..d4ba642f7 100644 --- a/tests/test_platform_inheritance.py +++ b/tests/test_platform_inheritance.py @@ -8,6 +8,8 @@ scraper and install block. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import shutil import sys import tempfile diff --git a/tests/test_platform_keys.py b/tests/test_platform_keys.py index c11b8788e..95be6420a 100644 --- a/tests/test_platform_keys.py +++ b/tests/test_platform_keys.py @@ -8,6 +8,8 @@ counted as declared: 76 Batocera entries added names such as boot0.rom. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_port.py b/tests/test_port.py index 5c873e84b..afb8662c7 100644 --- a/tests/test_port.py +++ b/tests/test_port.py @@ -8,6 +8,8 @@ route of the port, and the other launchers, are in test_handheld_launchers. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import importlib.util import io diff --git a/tests/test_profile_checks.py b/tests/test_profile_checks.py index e4fb44c97..a7de1e04a 100644 --- a/tests/test_profile_checks.py +++ b/tests/test_profile_checks.py @@ -9,6 +9,8 @@ check left out lets through a file the emulator refuses. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import tempfile diff --git a/tests/test_profile_names.py b/tests/test_profile_names.py index c5ec1a778..da172b185 100644 --- a/tests/test_profile_names.py +++ b/tests/test_profile_names.py @@ -7,6 +7,8 @@ own name that way. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_profile_paths.py b/tests/test_profile_paths.py index 3df44b9ce..eea16aea9 100644 --- a/tests/test_profile_paths.py +++ b/tests/test_profile_paths.py @@ -8,6 +8,8 @@ app's own code joins that folder to its data root. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import unittest from pathlib import Path diff --git a/tests/test_profile_repositories.py b/tests/test_profile_repositories.py index c00f94a83..76e63fe00 100644 --- a/tests/test_profile_repositories.py +++ b/tests/test_profile_repositories.py @@ -11,6 +11,8 @@ and skips. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 95879c00a..6711b143e 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import contextlib import io @@ -5981,6 +5983,55 @@ class ARoleIsReadPerPart(unittest.TestCase): self.assertEqual(list(_prose_runs("read at boot (:507), devilutionx.mpq")), []) +class AnOnlinePassSaysWhereItIs(unittest.TestCase): + """`--all --triage` online printed nothing for 15 minutes, and Ctrl-C + left no ranking at all, while --offline answered in three.""" + + def _main(self, *argv: str, stop_after: int | None = None): + calls = [] + + def build(_args, name, _profile, _path, _failed): + if stop_after is not None and len(calls) == stop_after: + raise KeyboardInterrupt + calls.append(name) + return ProfileReport(name=name, entries=[], counts={}) + + out, err = io.StringIO(), io.StringIO() + saved = sys.argv + try: + sys.argv = ["profile_sync.py", "--all", "--triage", *argv] + with mock.patch.object(profile_sync, "_build_full_report", side_effect=build), \ + mock.patch.object(profile_sync, "_check_quota"), \ + contextlib.redirect_stdout(out), contextlib.redirect_stderr(err), \ + self.assertRaises(SystemExit) as caught: + profile_sync.main() + raise SystemExit(0) + finally: + sys.argv = saved + return caught.exception.code, out.getvalue(), err.getvalue() + + def test_ctrl_c_prints_the_ranking_of_what_was_read(self): + code, out, err = self._main("--offline", stop_after=3) + self.assertEqual(code, 130) + self.assertEqual(out.splitlines()[-1], "3 profiles, 0 with something to review") + self.assertIn("interrupted after 3 of", err) + + def test_an_online_pass_names_its_size_and_the_offline_mode(self): + _code, _out, err = self._main("--limit", "2") + self.assertIn("reading 2 profiles against their forges", err) + self.assertIn("--offline answers from the cache alone", err) + + def test_a_progress_line_at_each_interval(self): + clock = iter([0.0, 10.0, profile_sync.PROGRESS_SECONDS + 1, 40.0]) + err = io.StringIO() + with mock.patch.object(profile_sync.time, "monotonic", lambda: next(clock)), \ + contextlib.redirect_stderr(err): + progress = profile_sync._Progress(9, "read", quiet=False) + for done, name in enumerate(("a", "b", "c")): + progress.step(done, name) + self.assertEqual(err.getvalue(), "read: 1 of 9, at b\n") + + class TheTriageCountsWhatBlocksThePin(unittest.TestCase): """Prose written under an older pin refuses the recale and the bump, yet the triage, --changed-only and --strict read "0 to review" on blastem, diff --git a/tests/test_prose_style.py b/tests/test_prose_style.py index d089e9f45..9081f77f4 100644 --- a/tests/test_prose_style.py +++ b/tests/test_prose_style.py @@ -8,6 +8,8 @@ not write them. This module names every place one comes back. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import os import re diff --git a/tests/test_provenance.py b/tests/test_provenance.py index d76d3a9b4..4dd467c74 100644 --- a/tests/test_provenance.py +++ b/tests/test_provenance.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import tempfile import unittest diff --git a/tests/test_refresh_data_dirs.py b/tests/test_refresh_data_dirs.py index b7fcd5617..604eba7ba 100644 --- a/tests/test_refresh_data_dirs.py +++ b/tests/test_refresh_data_dirs.py @@ -9,6 +9,8 @@ empty JSON document. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import json import sys diff --git a/tests/test_refresh_failures.py b/tests/test_refresh_failures.py index 11e2bfc27..7e2942913 100644 --- a/tests/test_refresh_failures.py +++ b/tests/test_refresh_failures.py @@ -8,6 +8,8 @@ failed, the cache left at the old version. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import shutil diff --git a/tests/test_refresh_stale.py b/tests/test_refresh_stale.py index e92887a66..b262fcc9e 100644 --- a/tests/test_refresh_stale.py +++ b/tests/test_refresh_stale.py @@ -8,10 +8,14 @@ by the refresh of the original its export patches. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + +import subprocess import sys import tempfile import unittest from pathlib import Path +from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT / "scripts")) @@ -279,5 +283,31 @@ class SavedFindings(unittest.TestCase): rs.load_findings(path, ("data",)) +class AFailedCheckIsNamed(unittest.TestCase): + """A crash of check_freshness exits 1 like a STALE row: its traceback, + on stderr, was dropped for "unreadable JSON", and a timeout ended on + a traceback of refresh_stale itself.""" + + def _run(self, raises: Exception | None = None, **result): + proc = subprocess.CompletedProcess([], **result) if result else None + with mock.patch.object(rs.subprocess, "run", side_effect=raises, return_value=proc): + return rs._run_check_freshness(("coreinfo",), []) # noqa: SLF001 + + def test_a_crash_shows_its_stderr(self): + with self.assertRaises(RuntimeError) as raised: + self._run(returncode=1, stdout="", stderr="Traceback\nRuntimeError: no listing\n") + self.assertIn("RuntimeError: no listing", str(raised.exception)) + self.assertNotIn("JSON", str(raised.exception)) + + def test_stale_rows_are_read(self): + self.assertEqual(self._run(returncode=1, stdout='[{"area": "coreinfo"}]', stderr=""), + [{"area": "coreinfo"}]) + + def test_a_timeout_says_what_to_run(self): + with self.assertRaises(RuntimeError) as raised: + self._run(raises=subprocess.TimeoutExpired(["x"], 1800)) + self.assertIn("--findings", str(raised.exception)) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_region.py b/tests/test_region.py index d0611a975..208c5bdd0 100644 --- a/tests/test_region.py +++ b/tests/test_region.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import os diff --git a/tests/test_region_agnostic_scan.py b/tests/test_region_agnostic_scan.py index 35175f38a..f6a7eda39 100644 --- a/tests/test_region_agnostic_scan.py +++ b/tests/test_region_agnostic_scan.py @@ -9,6 +9,8 @@ ROCKNIX. Reads the repository: skips when the database is absent. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_region_check.py b/tests/test_region_check.py index 83e924129..5fd073ada 100644 --- a/tests/test_region_check.py +++ b/tests/test_region_check.py @@ -3,6 +3,8 @@ records settle.""" from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_region_groups.py b/tests/test_region_groups.py index 6330b1aca..4ac1a0c66 100644 --- a/tests/test_region_groups.py +++ b/tests/test_region_groups.py @@ -7,6 +7,8 @@ Enterprise group drop the German Enterprise ROM under --region us. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_region_held.py b/tests/test_region_held.py index cb384cb44..3c50a8754 100644 --- a/tests/test_region_held.py +++ b/tests/test_region_held.py @@ -8,6 +8,8 @@ Japanese one, and the system shipped no BIOS at all. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import shutil diff --git a/tests/test_region_platform_index.py b/tests/test_region_platform_index.py index fbb1a59ea..12d206dd3 100644 --- a/tests/test_region_platform_index.py +++ b/tests/test_region_platform_index.py @@ -8,6 +8,8 @@ voided the [europe] tag Genesis Plus GX and PicoDrive give it, and from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_region_required_only.py b/tests/test_region_required_only.py index f361b33b7..b3a2cfdef 100644 --- a/tests/test_region_required_only.py +++ b/tests/test_region_required_only.py @@ -8,6 +8,8 @@ the optional winner: the slot shipped empty. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import sys import unittest diff --git a/tests/test_release_packs.py b/tests/test_release_packs.py index a32e3a2a9..e3d54147a 100644 --- a/tests/test_release_packs.py +++ b/tests/test_release_packs.py @@ -13,6 +13,8 @@ platform has a permanent page at get// instead. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import json diff --git a/tests/test_required_only_archives.py b/tests/test_required_only_archives.py index c8866e41b..417d642db 100644 --- a/tests/test_required_only_archives.py +++ b/tests/test_required_only_archives.py @@ -6,6 +6,8 @@ its _Required pack held them anyway: the archive loop never read `required`. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import sys import tempfile diff --git a/tests/test_required_only_twins.py b/tests/test_required_only_twins.py index c985096a9..cb08788fd 100644 --- a/tests/test_required_only_twins.py +++ b/tests/test_required_only_twins.py @@ -11,6 +11,8 @@ the manifest without a trace. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_resolution_evidence.py b/tests/test_resolution_evidence.py index 588a18be4..2d2c2c4e1 100644 --- a/tests/test_resolution_evidence.py +++ b/tests/test_resolution_evidence.py @@ -10,6 +10,8 @@ predicate is that check. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_retired_files.py b/tests/test_retired_files.py index b6387f1d3..fee66c0ab 100644 --- a/tests/test_retired_files.py +++ b/tests/test_retired_files.py @@ -8,6 +8,8 @@ contribution of a file the collection had held. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import tempfile diff --git a/tests/test_retrodeck_scraper_errors.py b/tests/test_retrodeck_scraper_errors.py index 2ab272d9d..7d2f2b282 100644 --- a/tests/test_retrodeck_scraper_errors.py +++ b/tests/test_retrodeck_scraper_errors.py @@ -7,6 +7,8 @@ PS2 BIOS and the pcsx2 standalone core, exit code 0. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import json import sys diff --git a/tests/test_romfolders_card.py b/tests/test_romfolders_card.py index 2a106976e..615cd941d 100644 --- a/tests/test_romfolders_card.py +++ b/tests/test_romfolders_card.py @@ -7,6 +7,8 @@ schema and their own declared sources. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_romfolders_es.py b/tests/test_romfolders_es.py index 828966f63..e3aa43084 100644 --- a/tests/test_romfolders_es.py +++ b/tests/test_romfolders_es.py @@ -9,6 +9,8 @@ naming a declared source, and a second dump giving the same bytes. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_romm_hash_rule.py b/tests/test_romm_hash_rule.py index 922d83f9f..17ec065e8 100644 --- a/tests/test_romm_hash_rule.py +++ b/tests/test_romm_hash_rule.py @@ -8,6 +8,8 @@ a wrong declared size passed here and failed there. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import sys diff --git a/tests/test_romset_recipes.py b/tests/test_romset_recipes.py index 88c666ce2..769345f22 100644 --- a/tests/test_romset_recipes.py +++ b/tests/test_romset_recipes.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import sys diff --git a/tests/test_save_root.py b/tests/test_save_root.py index e0a58c5ea..67a2a42d1 100644 --- a/tests/test_save_root.py +++ b/tests/test_save_root.py @@ -11,6 +11,8 @@ directory the frontend's config names. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import functools import hashlib import http.server diff --git a/tests/test_schema_semantic_gate.py b/tests/test_schema_semantic_gate.py index 9ccc9692c..5d6fd9983 100644 --- a/tests/test_schema_semantic_gate.py +++ b/tests/test_schema_semantic_gate.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import json import sys diff --git a/tests/test_scraper_contract.py b/tests/test_scraper_contract.py index cf9e04c20..e4ca795b4 100644 --- a/tests/test_scraper_contract.py +++ b/tests/test_scraper_contract.py @@ -10,6 +10,8 @@ exercised is a promise to contributors backed by nothing. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import importlib import io import json @@ -642,21 +644,26 @@ class UnreadableReleaseStopsTheScrape(unittest.TestCase): def _refuse(*_args, **_kwargs): raise urllib.error.HTTPError("https://api.github.com/x", 403, "rate limited", {}, None) + # Read at a release tag: the pin is the tag, not a branch commit. + TAG_PINNED = {"amberelec", "batocera", "bizhawk", "emuelec", "knulli", "recalbox", + "rocknix", "romm"} + + @staticmethod + def _registry_scrapers() -> list[str]: + """Every platform scraper the registry names: a hand-written list + left rocknix, retrodeck, minui, nextui and myminui unchecked.""" + import yaml # noqa: PLC0415 + + registry = yaml.safe_load((REPO_ROOT / "platforms" / "_registry.yml").read_text()) + return sorted({entry["scraper"] for entry in registry["platforms"].values() + if entry.get("scraper")}) + + def test_the_tag_pinned_scrapers_are_registered(self): + self.assertLessEqual(self.TAG_PINNED, set(self._registry_scrapers())) + def test_every_pinned_scraper_raises(self): - for name in ( - "scraper.bizhawk_scraper", - "scraper.romm_scraper", - "scraper.batocera_scraper", - "scraper.recalbox_scraper", - "scraper.libretro_scraper", - "scraper.retrobat_scraper", - "scraper.emudeck_scraper", - "scraper.misterfpga_scraper", - "scraper.retropie_scraper", - "scraper.amberelec_scraper", - "scraper.emuelec_scraper", - "scraper.knulli_scraper", - ): + for scraper in self._registry_scrapers(): + name = f"scraper.{scraper}_scraper" module = importlib.import_module(name) with self.subTest(scraper=name), mock.patch( "urllib.request.urlopen", self._refuse @@ -679,16 +686,16 @@ class UnreadableReleaseStopsTheScrape(unittest.TestCase): def answer(request, timeout=None): return Answer(json.dumps({"sha": sha}).encode()) - for name in ( - "scraper.libretro_scraper", - "scraper.retrobat_scraper", - "scraper.emudeck_scraper", - "scraper.misterfpga_scraper", - "scraper.retropie_scraper", - ): + branch_read = [s for s in self._registry_scrapers() if s not in self.TAG_PINNED] + self.assertGreater(len(branch_read), 8) + for scraper in branch_read: + name = f"scraper.{scraper}_scraper" module = importlib.import_module(name) with self.subTest(scraper=name), mock.patch("urllib.request.urlopen", answer): - self.assertRegex(module.Scraper().url, f"/{sha}(/|$)") + instance = module.Scraper() + # RetroDECK reads a tree of manifests and keeps the commit apart. + recorded = getattr(instance, "revision", "") or instance.url + self.assertRegex(recorded, f"(^|/){sha}(/|$)") def test_the_export_reads_the_recorded_tarball(self): from export_native import _tarball_at diff --git a/tests/test_sega_filing.py b/tests/test_sega_filing.py index 0fd10af1e..939fe0d40 100644 --- a/tests/test_sega_filing.py +++ b/tests/test_sega_filing.py @@ -9,6 +9,8 @@ Reads the repository. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_shared_groups.py b/tests/test_shared_groups.py index 94c0219ec..f418b570b 100644 --- a/tests/test_shared_groups.py +++ b/tests/test_shared_groups.py @@ -9,6 +9,8 @@ it back as a required file of RetroArch, Lakka and RetroPie. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_shipped_content.py b/tests/test_shipped_content.py index f7beb3708..2e49ee6c8 100644 --- a/tests/test_shipped_content.py +++ b/tests/test_shipped_content.py @@ -15,6 +15,8 @@ path each entry comes from. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import os diff --git a/tests/test_site_arcade_boards.py b/tests/test_site_arcade_boards.py index 6f836cd8a..0f22664c4 100644 --- a/tests/test_site_arcade_boards.py +++ b/tests/test_site_arcade_boards.py @@ -11,6 +11,8 @@ link or carry their reason below. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import tempfile import unittest diff --git a/tests/test_site_availability.py b/tests/test_site_availability.py index 12fa9052c..46efd86b6 100644 --- a/tests/test_site_availability.py +++ b/tests/test_site_availability.py @@ -8,6 +8,8 @@ held across the site while the gaps page listed them as known absences. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_site_bootstrap.py b/tests/test_site_bootstrap.py index ee41e51bb..3b736cad1 100644 --- a/tests/test_site_bootstrap.py +++ b/tests/test_site_bootstrap.py @@ -9,6 +9,8 @@ that long, so each copy reads install.py from the commit it was built from. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import os diff --git a/tests/test_site_built_gate.py b/tests/test_site_built_gate.py index c1957ab2f..1734cf873 100644 --- a/tests/test_site_built_gate.py +++ b/tests/test_site_built_gate.py @@ -15,6 +15,8 @@ Skips when the site is not built. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import sys diff --git a/tests/test_site_components.py b/tests/test_site_components.py index 376724742..ee73fbb92 100644 --- a/tests/test_site_components.py +++ b/tests/test_site_components.py @@ -18,6 +18,8 @@ node and skip without it. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import shutil diff --git a/tests/test_site_console_folders.py b/tests/test_site_console_folders.py index 514667a43..9d3a895ce 100644 --- a/tests/test_site_console_folders.py +++ b/tests/test_site_console_folders.py @@ -12,6 +12,8 @@ three samples committed under "FBAlpha 2012" and four release assets under from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import subprocess diff --git a/tests/test_site_content_data.py b/tests/test_site_content_data.py index a9c0f2d40..e95382dc1 100644 --- a/tests/test_site_content_data.py +++ b/tests/test_site_content_data.py @@ -8,6 +8,8 @@ needs no Python change, and a mistake stops the build with its name. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import re import shutil diff --git a/tests/test_site_copy.py b/tests/test_site_copy.py index 47507aa5f..5d2bb9662 100644 --- a/tests/test_site_copy.py +++ b/tests/test_site_copy.py @@ -12,6 +12,8 @@ print the measures of every page held here. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import re import statistics diff --git a/tests/test_site_cores.py b/tests/test_site_cores.py index 16f42673a..9bc8c3d70 100644 --- a/tests/test_site_cores.py +++ b/tests/test_site_cores.py @@ -7,6 +7,8 @@ where its pack is built from 321. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_site_css.py b/tests/test_site_css.py index 99cf0132a..e9dde975b 100644 --- a/tests/test_site_css.py +++ b/tests/test_site_css.py @@ -25,6 +25,8 @@ crumb separator free to end a line alone. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import unittest from pathlib import Path diff --git a/tests/test_site_density_cards.py b/tests/test_site_density_cards.py index 6bea1ba41..c932bcc08 100644 --- a/tests/test_site_density_cards.py +++ b/tests/test_site_density_cards.py @@ -10,6 +10,8 @@ sits in closed folds, every fact still on the page. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import types diff --git a/tests/test_site_emulator_page.py b/tests/test_site_emulator_page.py index be1165ee1..9ffcb5842 100644 --- a/tests/test_site_emulator_page.py +++ b/tests/test_site_emulator_page.py @@ -5,6 +5,8 @@ No network and no database: pages are rendered from small profiles. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_site_emulators.py b/tests/test_site_emulators.py index e8421df90..102eef7ac 100644 --- a/tests/test_site_emulators.py +++ b/tests/test_site_emulators.py @@ -27,6 +27,8 @@ Linux, Windows and Android commands, for MAME4droid an Android-only app. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_site_entry.py b/tests/test_site_entry.py index 3806438ba..bb9acc096 100644 --- a/tests/test_site_entry.py +++ b/tests/test_site_entry.py @@ -32,6 +32,8 @@ Linux handheld rows sent the reader to a menu. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import sys diff --git a/tests/test_site_entry_tables.py b/tests/test_site_entry_tables.py index 4065bf8f6..ae4e01d0b 100644 --- a/tests/test_site_entry_tables.py +++ b/tests/test_site_entry_tables.py @@ -10,6 +10,8 @@ every name and hash back. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_site_exports.py b/tests/test_site_exports.py index 4d230afc9..22fb6a020 100644 --- a/tests/test_site_exports.py +++ b/tests/test_site_exports.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import os diff --git a/tests/test_site_funnel.py b/tests/test_site_funnel.py index 72a96fbf6..e6b113638 100644 --- a/tests/test_site_funnel.py +++ b/tests/test_site_funnel.py @@ -7,6 +7,8 @@ reader two levels deeper without saying so. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import os import re diff --git a/tests/test_site_images.py b/tests/test_site_images.py index 21d28c0f0..ee0cb5355 100644 --- a/tests/test_site_images.py +++ b/tests/test_site_images.py @@ -8,6 +8,8 @@ CDN. These tests hold the replacements in place. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import sys diff --git a/tests/test_site_inputs.py b/tests/test_site_inputs.py index 57ee16fa4..031ae8b10 100644 --- a/tests/test_site_inputs.py +++ b/tests/test_site_inputs.py @@ -9,6 +9,8 @@ directory, so a get/ page said "No ZIP" for a setup the API marked as released. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import json import os diff --git a/tests/test_site_insights.py b/tests/test_site_insights.py index c9f9b2af9..ac0fe71c8 100644 --- a/tests/test_site_insights.py +++ b/tests/test_site_insights.py @@ -1,6 +1,8 @@ """The readings of scripts/site_insights.py, on synthetic figures.""" from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import datetime as dt import sys import unittest diff --git a/tests/test_site_integration.py b/tests/test_site_integration.py index a3371662a..d2f58137c 100644 --- a/tests/test_site_integration.py +++ b/tests/test_site_integration.py @@ -9,6 +9,8 @@ Node's compile cache into /tmp. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import glob import json import re diff --git a/tests/test_site_nav.py b/tests/test_site_nav.py index af5731578..934c97fd4 100644 --- a/tests/test_site_nav.py +++ b/tests/test_site_nav.py @@ -21,6 +21,8 @@ are built, the pages and the site. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import struct diff --git a/tests/test_site_picker.py b/tests/test_site_picker.py index 7ff891551..d725f57df 100644 --- a/tests/test_site_picker.py +++ b/tests/test_site_picker.py @@ -9,6 +9,8 @@ list the page shows is not what the command it prints installs. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import importlib.util import json import random diff --git a/tests/test_site_platform_density.py b/tests/test_site_platform_density.py index d636261ba..92db586da 100644 --- a/tests/test_site_platform_density.py +++ b/tests/test_site_platform_density.py @@ -10,6 +10,8 @@ role of each reference named by one word that a legend defines once. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import html import re import sys diff --git a/tests/test_site_platform_hashes.py b/tests/test_site_platform_hashes.py index 7d131796b..bee30587a 100644 --- a/tests/test_site_platform_hashes.py +++ b/tests/test_site_platform_hashes.py @@ -8,6 +8,8 @@ checking a dump against the page was told the wrong hash. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from unittest import mock diff --git a/tests/test_site_privacy.py b/tests/test_site_privacy.py index 906d39ba7..9bf1e0808 100644 --- a/tests/test_site_privacy.py +++ b/tests/test_site_privacy.py @@ -6,6 +6,8 @@ page looking different. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import unittest from pathlib import Path diff --git a/tests/test_site_reference.py b/tests/test_site_reference.py index 36f130fde..a7619f931 100644 --- a/tests/test_site_reference.py +++ b/tests/test_site_reference.py @@ -31,6 +31,8 @@ it has not been generated. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import csv import io import json @@ -1464,6 +1466,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```", diff --git a/tests/test_site_render.py b/tests/test_site_render.py index 9628bd5ed..09d25c7b1 100644 --- a/tests/test_site_render.py +++ b/tests/test_site_render.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_site_search.py b/tests/test_site_search.py index 976d8f860..242466820 100644 --- a/tests/test_site_search.py +++ b/tests/test_site_search.py @@ -19,6 +19,8 @@ built, the site. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import re import sys diff --git a/tests/test_site_system_links.py b/tests/test_site_system_links.py index 417ddd940..c9b132c01 100644 --- a/tests/test_site_system_links.py +++ b/tests/test_site_system_links.py @@ -11,6 +11,8 @@ Reads the repository: skips when the database is absent. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_site_system_names.py b/tests/test_site_system_names.py index 2eeae53a4..170c12f9d 100644 --- a/tests/test_site_system_names.py +++ b/tests/test_site_system_names.py @@ -10,6 +10,8 @@ console folders, so the ids to name are the platforms' and the profiles'. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_site_system_sections.py b/tests/test_site_system_sections.py index 5029e4033..8be186cd0 100644 --- a/tests/test_site_system_sections.py +++ b/tests/test_site_system_sections.py @@ -14,6 +14,8 @@ Reads the repository: skips when the database is absent. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import sys diff --git a/tests/test_site_systems.py b/tests/test_site_systems.py index c1c0946c1..f725d5c96 100644 --- a/tests/test_site_systems.py +++ b/tests/test_site_systems.py @@ -30,6 +30,8 @@ three times before the first file. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import types diff --git a/tests/test_site_validation.py b/tests/test_site_validation.py index e7ec40fb4..b33ffbf7a 100644 --- a/tests/test_site_validation.py +++ b/tests/test_site_validation.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import re diff --git a/tests/test_site_weight.py b/tests/test_site_weight.py index 8918eba61..64ae626b0 100644 --- a/tests/test_site_weight.py +++ b/tests/test_site_weight.py @@ -9,6 +9,8 @@ is looked up by content, an emulator card's anchor sits on its row. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_site_wiki.py b/tests/test_site_wiki.py index cb0e17e7d..71efb0388 100644 --- a/tests/test_site_wiki.py +++ b/tests/test_site_wiki.py @@ -31,6 +31,8 @@ setup guides or the generator that writes the page a link opens. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import sys diff --git a/tests/test_site_write.py b/tests/test_site_write.py index d2c7342d2..e10709c1f 100644 --- a/tests/test_site_write.py +++ b/tests/test_site_write.py @@ -11,6 +11,8 @@ delete on the critical path and is why these tests exist. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_slot.py b/tests/test_slot.py index 46dfd09be..65819a4ae 100644 --- a/tests/test_slot.py +++ b/tests/test_slot.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_slots.py b/tests/test_slots.py index 417f91f16..1c966fad3 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import re import shutil diff --git a/tests/test_slots_shipped_file.py b/tests/test_slots_shipped_file.py index c0865fb88..2b6dc03d3 100644 --- a/tests/test_slots_shipped_file.py +++ b/tests/test_slots_shipped_file.py @@ -11,6 +11,8 @@ Reads the repository: skips when the database or the files are absent. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import copy import hashlib import os diff --git a/tests/test_source_ref_targets.py b/tests/test_source_ref_targets.py index 611f0c739..5bf8eeee3 100644 --- a/tests/test_source_ref_targets.py +++ b/tests/test_source_ref_targets.py @@ -12,6 +12,8 @@ mirror pages its tree, and read as one page it could not confirm a path. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_split_extras.py b/tests/test_split_extras.py index 0f8c3c0ea..08690f001 100644 --- a/tests/test_split_extras.py +++ b/tests/test_split_extras.py @@ -7,6 +7,8 @@ and in no part (4507 of RetroArch's 5430). from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_split_names.py b/tests/test_split_names.py index 250579cfb..98751f575 100644 --- a/tests/test_split_names.py +++ b/tests/test_split_names.py @@ -10,6 +10,8 @@ SHA256SUMS.txt. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import tempfile diff --git a/tests/test_split_pack.py b/tests/test_split_pack.py index 2ae04dcde..48983f8fc 100644 --- a/tests/test_split_pack.py +++ b/tests/test_split_pack.py @@ -14,6 +14,8 @@ parts extracted into one folder are the pack. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import os diff --git a/tests/test_standalone_links.py b/tests/test_standalone_links.py index 9d8044fcc..7ea47c50f 100644 --- a/tests/test_standalone_links.py +++ b/tests/test_standalone_links.py @@ -12,6 +12,8 @@ naomi.zip took the place of MAME's. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import textwrap import unittest diff --git a/tests/test_standalone_mode.py b/tests/test_standalone_mode.py index 5e885c2c1..82d5d22b5 100644 --- a/tests/test_standalone_mode.py +++ b/tests/test_standalone_mode.py @@ -10,6 +10,8 @@ standalone_cores dropped them from its pack. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_standalone_probe.py b/tests/test_standalone_probe.py index 6a51f731c..c9eda2181 100644 --- a/tests/test_standalone_probe.py +++ b/tests/test_standalone_probe.py @@ -3,6 +3,8 @@ framebuffer reader, the settings it writes and the oracle it judges with.""" from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import struct import sys import tempfile diff --git a/tests/test_system_ids.py b/tests/test_system_ids.py index 1b8098c58..48736036f 100644 --- a/tests/test_system_ids.py +++ b/tests/test_system_ids.py @@ -9,6 +9,8 @@ channelf.zip and SNES DSP sets to colecovision/. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_system_pack_extras.py b/tests/test_system_pack_extras.py index 29c23c8ff..069844ee0 100644 --- a/tests/test_system_pack_extras.py +++ b/tests/test_system_pack_extras.py @@ -7,6 +7,8 @@ names, while the split packs, built per system, carried them. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_system_pack_parity.py b/tests/test_system_pack_parity.py index 4cf8edc5f..9925a4310 100644 --- a/tests/test_system_pack_parity.py +++ b/tests/test_system_pack_parity.py @@ -10,6 +10,8 @@ or trees and credited a profile's file at a path holding another profile's. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import shutil diff --git a/tests/test_system_selection.py b/tests/test_system_selection.py index c7016c539..4ddafd5b0 100644 --- a/tests/test_system_selection.py +++ b/tests/test_system_selection.py @@ -10,6 +10,8 @@ libretro pack. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import re import subprocess @@ -23,6 +25,7 @@ sys.path.insert(0, str(REPO_ROOT / "scripts")) import common # noqa: E402 import generate_pack as builder # noqa: E402 +from tests.scratchtree import scratch_checkout, snapshot # noqa: E402 class SpellingsAgree(unittest.TestCase): @@ -219,7 +222,7 @@ class AMissingDatabaseIsNamed(unittest.TestCase): with self.subTest(script=script): proc = subprocess.run( [sys.executable, f"scripts/{script}", "--db", "nosuch.json", *extra], - capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=600, + capture_output=True, text=True, cwd=str(scratch_checkout()), timeout=600, check=False, ) self.assertNotEqual(proc.returncode, 0, proc.stdout[-500:]) @@ -254,7 +257,7 @@ class AMissingBiosDirIsRefused(unittest.TestCase): 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, + capture_output=True, text=True, cwd=str(scratch_checkout()), timeout=600, check=False, ) self.assertNotEqual(proc.returncode, 0) @@ -307,14 +310,15 @@ class AMissingProfileDirIsRefused(unittest.TestCase): "dat": str(root / "empty.dat")} taking = _scripts_taking("--emulators-dir") self.assertGreater(len(taking), 15) - tracked = ["git", "diff", "--name-only"] - before = subprocess.run(tracked, cwd=REPO_ROOT, capture_output=True, text=True).stdout + # In a copy: run in the checkout, a regression wrote there first. + checkout = scratch_checkout() + before = snapshot(checkout) for path in taking: extra = [arg.format(**values) for arg in self.EXTRA.get(path.stem, [])] with self.subTest(script=path.stem): proc = subprocess.run( [*_command(path), "--emulators-dir", "nosuch", *extra], - capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=600, + capture_output=True, text=True, cwd=str(checkout), timeout=600, check=False, env={**os.environ, "RETROBIOS_OFFLINE": "1"}, ) self.assertNotEqual(proc.returncode, 0, proc.stdout[-500:]) @@ -326,9 +330,11 @@ class AMissingProfileDirIsRefused(unittest.TestCase): ] self.assertEqual(written, []) # Read as empty, generate_site rewrote published-parts.json and the - # FAQ, and sitesources source-paths.json. - after = subprocess.run(tracked, cwd=REPO_ROOT, capture_output=True, text=True).stdout - self.assertEqual(after, before) + # FAQ, and sitesources source-paths.json. The copy compares the + # files themselves: a name `git diff` already listed hid a rewrite. + after = snapshot(checkout) + self.assertEqual(sorted(k for k in before.keys() | after.keys() + if before.get(k) != after.get(k)), []) class AMissingPlatformsDirIsRefused(unittest.TestCase): @@ -367,7 +373,7 @@ class AMissingPlatformsDirIsRefused(unittest.TestCase): with self.subTest(script=path.stem): proc = subprocess.run( [*_command(path), "--platforms-dir", "nosuch", *extra], - capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=600, + capture_output=True, text=True, cwd=str(scratch_checkout()), timeout=600, check=False, env={**os.environ, "RETROBIOS_OFFLINE": "1"}, ) self.assertNotEqual(proc.returncode, 0, proc.stdout[-500:]) @@ -375,6 +381,28 @@ class AMissingPlatformsDirIsRefused(unittest.TestCase): self.assertRegex(proc.stderr, r"ERROR: .*nosuch") +class TheCopyKeepsTheCheckoutOutOfReach(unittest.TestCase): + """The refusal tests above run their writers in a copy.""" + + def test_a_script_in_the_copy_writes_to_the_copy(self): + checkout = scratch_checkout() + proc = subprocess.run( + [sys.executable, "-c", "import sys; sys.path.insert(0, 'scripts'); " + "import generate_site; print(generate_site._REPO_ROOT)"], + capture_output=True, text=True, cwd=str(checkout), timeout=300, check=True, + ) + self.assertEqual(Path(proc.stdout.strip()), checkout.resolve()) + + def test_a_rewrite_of_a_file_is_seen(self): + checkout = scratch_checkout() + page = checkout / "wiki" / "index.md" + before = snapshot(checkout) + original = page.read_bytes() + self.addCleanup(page.write_bytes, original) + page.write_bytes(original + b"\n") + self.assertNotEqual(snapshot(checkout), before) + + class AMissingInputFileIsNamed(unittest.TestCase): """The documented `--from-md5-file missing.txt` ended on a FileNotFoundError at the bottom of a stack trace.""" diff --git a/tests/test_target_names.py b/tests/test_target_names.py index 877899c08..cb08e96b2 100644 --- a/tests/test_target_names.py +++ b/tests/test_target_names.py @@ -7,6 +7,8 @@ and `--verify-packs --target nx` looked for a pack nobody had built. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import sys import tempfile diff --git a/tests/test_target_scrapers.py b/tests/test_target_scrapers.py index 6b05e356c..d55033cff 100644 --- a/tests/test_target_scrapers.py +++ b/tests/test_target_scrapers.py @@ -8,7 +8,10 @@ packs built for it shrank without an error. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json +import os import sys import unittest import urllib.error @@ -589,12 +592,42 @@ class EmuDeckScriptNamesHaveOneRule(unittest.TestCase): with mock.patch.object( emudeck_targets_scraper, "_list_emuscripts", return_value=listing ), mock.patch.object(scraper, "_load_retroarch_cores", return_value=["mgba"]): - cores = scraper._fetch_cores_for_target("unused", "Windows") + cores = scraper._fetch_cores_for_target("unused", "Windows", "windows-x86_64") standalone = {emulator_of_script(name) for name in listing} - {None, "retroarch"} self.assertEqual(standalone, {"mupen64plus_next", "pcsx2", "xemu"}) self.assertEqual(cores, sorted(standalone | {"mgba"})) +class EmuDeckWindowsReadsTheWindowsBuild(unittest.TestCase): + """The RetroArch list was the first target of the architecture: + EmuDeck's Windows target carried fsuae, omicron and video_processor, + which the buildbot publishes for Linux only.""" + + def test_each_target_gets_its_own_retroarch_list(self): + seen: list[str] = [] + + def load(target: str) -> list[str]: + seen.append(target) + return [target] + + scraper = emudeck_targets_scraper.Scraper(revision="0" * 40) + with mock.patch.object( + emudeck_targets_scraper, "_list_emuscripts", return_value=["emuDeckRetroArch.sh"] + ), mock.patch.object(scraper, "_load_retroarch_cores", side_effect=load): + data = scraper.fetch_targets() + self.assertEqual(seen, ["linux-x86_64", "windows-x86_64"]) + self.assertEqual(data["targets"]["windows"]["cores"], ["windows-x86_64"]) + + def test_the_list_is_read_by_target_name_from_any_directory(self): + cwd = os.getcwd() + self.addCleanup(os.chdir, cwd) + os.chdir(Path(__file__).parent) + load = emudeck_targets_scraper.Scraper._load_retroarch_cores + windows, linux = load("windows-x86_64"), load("linux-x86_64") + self.assertNotIn("video_processor", windows) + self.assertIn("video_processor", linux) + + class HashScrapersRefuseAnUnknownVersion(unittest.TestCase): """mame_hash_scraper wrote core_version "unknown" after a failed API call and cached it for a day, while the pipeline step still read OK.""" diff --git a/tests/test_target_systems.py b/tests/test_target_systems.py index c4896a123..ecb7adc6c 100644 --- a/tests/test_target_systems.py +++ b/tests/test_target_systems.py @@ -9,6 +9,8 @@ builder still shipped its ROMs as ep128emu extras. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_target_verify.py b/tests/test_target_verify.py index ee42f854b..586c9c7dc 100644 --- a/tests/test_target_verify.py +++ b/tests/test_target_verify.py @@ -10,6 +10,8 @@ out of a targeted --all run, and the check refuses it as the build does. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import contextlib diff --git a/tests/test_tool_messages.py b/tests/test_tool_messages.py index c2e59f6e9..d297c7419 100644 --- a/tests/test_tool_messages.py +++ b/tests/test_tool_messages.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import os @@ -85,6 +87,152 @@ class AMistypedTargetGetsTheClosestName(unittest.TestCase): +class TheSystemHintCountsTheListing(unittest.TestCase): + """`--system sony-playstaton` said "--list-systems lists all 579" and + --list-systems printed 532: the hint counted spellings, the listing + merges the ones that differ in case or separator.""" + + def test_both_tools_count_the_lines_the_listing_prints(self): + listing = subprocess.run( + [sys.executable, "scripts/verify.py", "--list-systems"], + cwd=REPO_ROOT, capture_output=True, text=True, timeout=300, check=True, + ) + printed = sum(1 for line in listing.stdout.splitlines() if "emulator" in line) + for script in ("verify.py", "generate_pack.py"): + with self.subTest(script=script), tempfile.TemporaryDirectory() as out: + argv = [sys.executable, f"scripts/{script}", "--system", "sony-playstaton"] + if script == "generate_pack.py": + argv += ["--output-dir", out] + proc = subprocess.run( + argv, cwd=REPO_ROOT, capture_output=True, text=True, timeout=300, + check=False, + ) + self.assertEqual(proc.returncode, 1, proc.stderr) + self.assertIn(f"--list-systems lists all {printed}.", proc.stderr) + + +class ADamagedProfileNamesItself(unittest.TestCase): + """One misindented key in an unrelated profile stopped `verify.py + --emulator handy` and generate_pack on a 37-line yaml traceback, the + faulty file named near its end.""" + + def test_one_line_names_the_file_and_line(self): + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as folder: + profiles = Path(folder) + (profiles / "handy.yml").write_text( + (REPO_ROOT / "emulators" / "handy.yml").read_text(encoding="utf-8"), + encoding="utf-8", + ) + (profiles / "bad.yml").write_text( + "emulator: Bad\nfiles:\n - name: a\n size: 3\n", encoding="utf-8" + ) + for script, extra in (("verify.py", []), + ("generate_pack.py", ["--output-dir", folder])): + with self.subTest(script=script): + proc = subprocess.run( + [sys.executable, f"scripts/{script}", "--emulator", "handy", + "--emulators-dir", folder, *extra], + cwd=REPO_ROOT, capture_output=True, text=True, timeout=300, + check=False, + ) + self.assertEqual(proc.returncode, 1, proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + self.assertEqual(proc.stderr.strip().splitlines(), [ + f"ERROR: {profiles / 'bad.yml'}:4: did not find expected '-' " + "indicator. Check it with: python scripts/validate_schemas.py " + "--source-only" + ]) + + def test_a_damaged_file_is_not_an_absent_one(self): + """Ten callers skip a platform whose file is absent (except + FileNotFoundError): a damaged one has to stop them instead.""" + self.assertFalse(issubclass(common.SourceUnreadable, FileNotFoundError)) + + +class TheVerdictClosesARedirectedReport(unittest.TestCase): + """`verify.py --emulator fbneo --strict > log 2>&1` put the --strict + ERROR on the first line of the log: stdout redirected is block-buffered, + stderr is not, and the report it closes came after.""" + + def test_stderr_stays_after_what_stdout_wrote_first(self): + program = ( + "import sys\n" + "sys.path.insert(0, 'scripts')\n" + "from common import run_cli\n" + "def main():\n" + " print('report')\n" + " print('ERROR: verdict', file=sys.stderr)\n" + " return 1\n" + "run_cli(main)\n" + ) + env = {k: v for k, v in os.environ.items() if k != "PYTHONUNBUFFERED"} + proc = subprocess.run( + [sys.executable, "-c", program], cwd=REPO_ROOT, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, timeout=60, check=False, env=env, + ) + self.assertEqual(proc.returncode, 1) + self.assertEqual(proc.stdout.splitlines(), ["report", "ERROR: verdict"]) + + +class TheAllHelpNamesWhatItCovers(unittest.TestCase): + """`verify.py --help` kept 'Verify all active platforms' after the + archived status was dropped: --all covers every registered platform, + and a reader looked for an --include-archived that no longer exists.""" + + def test_no_help_text_speaks_of_active_platforms(self): + proc = subprocess.run( + [sys.executable, "scripts/verify.py", "--help"], cwd=REPO_ROOT, + capture_output=True, text=True, timeout=60, check=True, + ) + self.assertIn("Verify every registered platform", proc.stdout) + stale = [path.name for path in sorted((REPO_ROOT / "scripts").rglob("*.py")) + if "active platform" in path.read_text(encoding="utf-8")] + self.assertEqual(stale, []) + + +class ReadOnlyToolsRunFromAnyFolder(unittest.TestCase): + """Only verify.py and generate_pack.py moved to the repository root. + From another folder slots, region_audit, provenance_report and + bundled_only said database.json was missing and to rebuild it, + identity and list_platforms named a missing folder, and game_rules + ended on a FileNotFoundError traceback.""" + + COMMANDS = ( + ["slots.py", "--platform", "retroarch"], + ["identity.py"], + ["cross_reference.py", "--emulator", "handy"], + ["list_platforms.py"], + ["region_audit.py"], + ["provenance_report.py"], + ["bundled_only.py"], + ["game_rules.py"], + ) + + def test_each_reads_the_repository_from_a_subfolder(self): + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as folder: + for command in self.COMMANDS: + with self.subTest(command=command[0]): + proc = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / command[0]), + *command[1:]], + cwd=folder, capture_output=True, text=True, timeout=300, + check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stderr[-500:]) + self.assertNotIn("Traceback", proc.stderr) + self.assertEqual(os.listdir(folder), []) + + def test_a_typed_path_is_read_from_where_it_was_typed(self): + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as folder: + proc = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / "slots.py"), + "--db", "nosuch.json"], + cwd=folder, capture_output=True, text=True, timeout=300, check=False, + ) + self.assertEqual(proc.returncode, 1) + self.assertIn(str(Path(folder) / "nosuch.json"), proc.stderr) + + class AMistypedTargetAcrossAllPlatforms(unittest.TestCase): """`verify --all --target swich` ended on "no platform has it" after 17 INFO lines, two of which held the suggestion.""" @@ -455,6 +603,25 @@ class OneSpellingPerOption(unittest.TestCase): missing.append(f"{path.relative_to(REPO_ROOT)}:{node.lineno} {long}") self.assertEqual(missing, []) + def test_one_long_name_per_option(self): + """arcade_parent_importer took --database where every other script + takes --db, and refused `--db database.json`.""" + import ast # noqa: PLC0415 + + renamed = {"--database": "--db"} + found = [] + for path in sorted((REPO_ROOT / "scripts").rglob("*.py")): + tree = ast.parse(path.read_text(encoding="utf-8")) + for node in ast.walk(tree): + if (isinstance(node, ast.Call) + and getattr(node.func, "attr", None) == "add_argument"): + found.extend( + f"{path.relative_to(REPO_ROOT)}:{node.lineno} {a.value}" + for a in node.args + if isinstance(a, ast.Constant) and a.value in renamed + ) + self.assertEqual(found, []) + class ThePipelineRefreshesTheCommittedExport(unittest.TestCase): """Step 2e wrote to /upstream while exports/ is the copy the diff --git a/tests/test_torrentzip.py b/tests/test_torrentzip.py index e8e3e61d5..b4986f629 100644 --- a/tests/test_torrentzip.py +++ b/tests/test_torrentzip.py @@ -1,6 +1,8 @@ """Tests for the TorrentZip builder against real MAME romsets.""" from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_truth_data_dir_readers.py b/tests/test_truth_data_dir_readers.py index f58978830..26818c367 100644 --- a/tests/test_truth_data_dir_readers.py +++ b/tests/test_truth_data_dir_readers.py @@ -9,6 +9,8 @@ revision, and hash_conflict did not see LRPS2's value. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_truth_diff.py b/tests/test_truth_diff.py index 949be7c7d..dcce0f7b3 100644 --- a/tests/test_truth_diff.py +++ b/tests/test_truth_diff.py @@ -9,6 +9,8 @@ discrepancy that does not exist. Content decides, as everywhere else here. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import pathlib import subprocess import sys diff --git a/tests/test_truth_layout.py b/tests/test_truth_layout.py index 50a23053b..d319b42f6 100644 --- a/tests/test_truth_layout.py +++ b/tests/test_truth_layout.py @@ -8,6 +8,8 @@ out on RetroBat, EmuDeck and RetroDECK. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import random diff --git a/tests/test_twin_declaration_status.py b/tests/test_twin_declaration_status.py index 4540adaaa..1cbb92c67 100644 --- a/tests/test_twin_declaration_status.py +++ b/tests/test_twin_declaration_status.py @@ -9,6 +9,8 @@ refusal read first left "excluded" behind the file the second one ships. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_twin_declarations.py b/tests/test_twin_declarations.py index 4acbddf79..9ab464d93 100644 --- a/tests/test_twin_declarations.py +++ b/tests/test_twin_declarations.py @@ -9,6 +9,8 @@ frontend hashes bios/disk.rom for xroar and finds the PC-88 ROM. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_undeclared_dedup.py b/tests/test_undeclared_dedup.py index b7fa6cde5..9fc2c12bf 100644 --- a/tests/test_undeclared_dedup.py +++ b/tests/test_undeclared_dedup.py @@ -9,6 +9,8 @@ dropped from the pack. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_unplaced_extras.py b/tests/test_unplaced_extras.py index aab69f59d..cea234522 100644 --- a/tests/test_unplaced_extras.py +++ b/tests/test_unplaced_extras.py @@ -8,6 +8,8 @@ listed them neither in files nor in omitted_files. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_upstream.py b/tests/test_upstream.py index 01817f52b..9132905be 100644 --- a/tests/test_upstream.py +++ b/tests/test_upstream.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import http.client import os diff --git a/tests/test_upstream_index.py b/tests/test_upstream_index.py index be389491d..a765dea57 100644 --- a/tests/test_upstream_index.py +++ b/tests/test_upstream_index.py @@ -8,6 +8,8 @@ and a RetroArch target pack lost LRPS2's files. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_validate_pr.py b/tests/test_validate_pr.py index d122ffc66..ddb73a493 100644 --- a/tests/test_validate_pr.py +++ b/tests/test_validate_pr.py @@ -8,6 +8,8 @@ it is an untrusted-input boundary with nothing behind it. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import tempfile diff --git a/tests/test_validate_pr_flow.py b/tests/test_validate_pr_flow.py index cab20c4b8..d76259158 100644 --- a/tests/test_validate_pr_flow.py +++ b/tests/test_validate_pr_flow.py @@ -7,6 +7,8 @@ cannot hold has to be an action the contributor can take. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import subprocess diff --git a/tests/test_validate_pr_hashes.py b/tests/test_validate_pr_hashes.py index 280756681..110613039 100644 --- a/tests/test_validate_pr_hashes.py +++ b/tests/test_validate_pr_hashes.py @@ -8,6 +8,8 @@ were lost the same way. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_validated_choice.py b/tests/test_validated_choice.py index f9aba6707..58df7e6a2 100644 --- a/tests/test_validated_choice.py +++ b/tests/test_validated_choice.py @@ -8,6 +8,8 @@ across 14 destinations on six platforms. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import hashlib import sys diff --git a/tests/test_validation_scope.py b/tests/test_validation_scope.py index 423f7b81b..31a313db6 100644 --- a/tests/test_validation_scope.py +++ b/tests/test_validation_scope.py @@ -8,6 +8,8 @@ test fails if either hands the index the whole profile set again. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import unittest from pathlib import Path diff --git a/tests/test_variant_choice.py b/tests/test_variant_choice.py index 2638e8657..48a187652 100644 --- a/tests/test_variant_choice.py +++ b/tests/test_variant_choice.py @@ -9,6 +9,8 @@ never swapped in. Both sides now read find_validated_variant. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import re diff --git a/tests/test_variant_identity.py b/tests/test_variant_identity.py index 1b1f99b46..1af95d7a1 100644 --- a/tests/test_variant_identity.py +++ b/tests/test_variant_identity.py @@ -9,6 +9,8 @@ database. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import unittest diff --git a/tests/test_verify_all_dir.py b/tests/test_verify_all_dir.py index 6e0077338..cc2b325ee 100644 --- a/tests/test_verify_all_dir.py +++ b/tests/test_verify_all_dir.py @@ -7,6 +7,8 @@ the run, and one registered only in the copy was never checked. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import subprocess import sys diff --git a/tests/test_verify_data_dir_notice.py b/tests/test_verify_data_dir_notice.py index b9e4a71f9..9d2b2d08d 100644 --- a/tests/test_verify_data_dir_notice.py +++ b/tests/test_verify_data_dir_notice.py @@ -9,6 +9,8 @@ verify --emulator already named its uncached trees. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import os diff --git a/tests/test_verify_emulator_evidence.py b/tests/test_verify_emulator_evidence.py index b8f498c37..9247d6caf 100644 --- a/tests/test_verify_emulator_evidence.py +++ b/tests/test_verify_emulator_evidence.py @@ -14,6 +14,8 @@ the other by flagging the divergence. This report agreed with neither. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_verify_listing.py b/tests/test_verify_listing.py index 00c43b592..5d6a208f9 100644 --- a/tests/test_verify_listing.py +++ b/tests/test_verify_listing.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import sys import unittest @@ -241,6 +243,62 @@ class BundledGameDataIsNotMissing(unittest.TestCase): out.getvalue()) +class TheBundledLineSaysWhatItLeavesOut(unittest.TestCase): + """'Bundled with their emulator, not collected: 154' listed 14 names, + the required firmware, and nothing said 140 more existed.""" + + def test_the_hidden_names_are_counted(self): + import contextlib # noqa: PLC0415 + import io # noqa: PLC0415 + + sys.path.insert(0, str(REPO_ROOT / "scripts")) + import verify # noqa: PLC0415 + + result = {"undeclared_files": [ + {"name": name, "emulator": "e", "required": required, "in_repo": False, + "bundled": True, "in_pack": False} + for name, required in (("fw.bin", True), ("a.frag", False), ("b.dll", False)) + ]} + with contextlib.redirect_stdout(io.StringIO()) as quiet: + verify._print_undeclared_section(result, verbose=False) + self.assertIn( + "Bundled with their emulator, not collected: 3 " + "(fw.bin, 2 more, --verbose lists them)", + quiet.getvalue(), + ) + with contextlib.redirect_stdout(io.StringIO()) as verbose: + verify._print_undeclared_section(result, verbose=True) + self.assertIn( + "Bundled with their emulator, not collected: 3 (a.frag, b.dll, fw.bin)", + verbose.getvalue(), + ) + result["undeclared_files"] = result["undeclared_files"][1:] + with contextlib.redirect_stdout(io.StringIO()) as unnamed: + verify._print_undeclared_section(result, verbose=False) + self.assertIn("Bundled with their emulator, not collected: 2 (--verbose lists them)", + unnamed.getvalue()) + + +class TheCheckLineSaysWhatItIs(unittest.TestCase): + """Without --verbose each missing core file was followed by a bare + '[size]' line, nine of them in verify --all.""" + + def test_the_checks_read_as_the_verbose_line(self): + import contextlib # noqa: PLC0415 + import io # noqa: PLC0415 + + sys.path.insert(0, str(REPO_ROOT / "scripts")) + import verify # noqa: PLC0415 + + entry = {"name": "FBASIC30.ROM", "emulator": "XM7", "checks": ["size"]} + with contextlib.redirect_stdout(io.StringIO()) as out: + verify._print_undeclared_entry(entry, "MISSING (required)", False, set()) + self.assertEqual(out.getvalue().splitlines(), [ + " MISSING (required): XM7 needs FBASIC30.ROM", + " validates size", + ]) + + class CoreCopiesWithoutAGapEntryAreCounted(unittest.TestCase): """verify --platform recalbox --system nec-pc-engine printed no Core files line while the pack held six geargrafx syscard copies: the gap diff --git a/tests/test_verify_pack_choice.py b/tests/test_verify_pack_choice.py index 8da4070a7..96c6e2d22 100644 --- a/tests/test_verify_pack_choice.py +++ b/tests/test_verify_pack_choice.py @@ -8,6 +8,8 @@ regional one. A pack built with --source truth answered SKIP and exit 0. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import json import os diff --git a/tests/test_verify_release_asset.py b/tests/test_verify_release_asset.py index 161732f53..d4cc4ac16 100644 --- a/tests/test_verify_release_asset.py +++ b/tests/test_verify_release_asset.py @@ -11,6 +11,8 @@ counts the file missing as before. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_version_basis.py b/tests/test_version_basis.py index bb0f01511..55e1bb9af 100644 --- a/tests/test_version_basis.py +++ b/tests/test_version_basis.py @@ -8,6 +8,8 @@ to a basis, and hold a pin advance to reading the version again. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import copy import hashlib diff --git a/tests/test_which_pack.py b/tests/test_which_pack.py index 0323b4dcb..6fd282a40 100644 --- a/tests/test_which_pack.py +++ b/tests/test_which_pack.py @@ -6,6 +6,8 @@ published and carries 3916 files the RetroArch one does not. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_workflow_paths.py b/tests/test_workflow_paths.py index 119013586..1181abb82 100644 --- a/tests/test_workflow_paths.py +++ b/tests/test_workflow_paths.py @@ -7,6 +7,8 @@ until some unrelated file moved. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import fnmatch import re diff --git a/tests/test_zipped_member.py b/tests/test_zipped_member.py index da0d63eee..cc369fbd0 100644 --- a/tests/test_zipped_member.py +++ b/tests/test_zipped_member.py @@ -6,6 +6,8 @@ level1.rom went out as OK where verify said "not found inside ZIP". from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/wiki/reading-reports.md b/wiki/reading-reports.md index a3bfd6765..844d03a78 100644 --- a/wiki/reading-reports.md +++ b/wiki/reading-reports.md @@ -110,7 +110,7 @@ RetroDECK: 2007/2008 OK, 1 untested [md5] Core files: 1764 in pack, 1 required missing, 26 optional missing (--verbose lists them) Core files held but not packed: 154 (142 installed with its emulator, 8 read below a save folder per system and no system runs the core there, 4 a packed file occupies its directory) MISSING (required): FinalBurn Neo needs sfiii2a.zip (1 file) - Bundled with their emulator, not collected: 20 + Bundled with their emulator, not collected: 20 (--verbose lists them) Game data: 5099 in pack, 30 missing (--verbose lists them) Game data held but not packed: 30 (installed with its emulator) No external files: 78 cores (--verbose lists them) diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 3345889d1..9e32c6481 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -222,8 +222,14 @@ disambiguated by a hash suffix on purpose. output or formatting. 5. **Run the full suite.** After adding your test, run `python -m unittest - discover -s tests -t . -v` to verify nothing else broke. `-t .` imports - `tests/__init__.py`, which keeps the suite offline. + discover -s tests -t . -v` to verify nothing else broke. + +A new module starts with `import tests` right after +`from __future__ import annotations`. The package import is what keeps a run +offline, puts its scratch under `tmp/` and gives it its own installer cache, +however the module is started: `discover tests`, `python -m unittest +tests.test_e2e` or `python -m tests.test_e2e`. A test fails on any module +without it. A fix comes with a test that fails without it. Write the test first, watch it fail on the current code, then fix: a test written after the fix may never @@ -488,6 +494,19 @@ wired to pull requests alone would guard the road nobody takes. The test job with BIOS validation and auto-labeling too. Those two read pull request context and stay behind an event guard. +The schema job also runs mypy over `install.py` and `scripts/`. The code +carried 993 type errors when the check started, so they sit in +`mypy-baseline.txt` and only a new one fails. An error the change removes fails +too until the baseline is synced, so the file never hides a later error that +reads the same: + +```bash +pip install -e ".[dev]" +mypy > tmp/mypy.txt +mypy-baseline filter < tmp/mypy.txt # what CI runs +mypy-baseline sync < tmp/mypy.txt # after a fix, commit the new baseline +``` + A push series collapses to the tip, so what a green run states is that the head of main passes, not every commit under it. diff --git a/wiki/tools.md b/wiki/tools.md index 608f60318..763189ae2 100644 --- a/wiki/tools.md +++ b/wiki/tools.md @@ -513,7 +513,10 @@ A part opening with `source` or `upstream`, in a source_ref or in prose, is read in that repository. The word belongs to the part, not to its path: one profile can cite the same file under both. `--triage` prints a header, ranks worst first and closes on the number of profiles with something to -review. +review. Online, a pass over many profiles takes several minutes or more: it +names its size on stderr, then writes a progress line every 30 seconds. +Ctrl-C prints the output for the profiles already read and exits 130. +`--offline` answers from the cache alone. A single cited line is often not distinctive, so the anchor widens by steps of ±3, ±6, ±12, ±25 and ±50 lines until it is unique. Three further @@ -1416,7 +1419,7 @@ Further options, by script: | `scraper/mame_hash_scraper.py` | `--release mameNNNN --profile FILE --source-commit SHA --core NAME --built-by PLATFORM` writes the profile of a MAME build a platform compiles. `--devices --listxml FILE` documents in `mame.yml` every device set with dumped ROMs that the release's `-listxml` names (`isdevice="yes"`), with `--dry-run` to preview | | `scraper/romset_dat_importer.py` | `--cache-dir` (downloaded listxml and DATs) | | `scraper/game_dat_importer.py` | `--source no-intro\|redump` (repeatable), `--pack FILE` imports a local ZIP, directory or DAT, `--refresh` asks the servers again, `--cache-dir`, `--output-dir`, `--dry-run` | -| `scraper/arcade_parent_importer.py` | `--profile NAME` (one core version), `--emulators-dir`, `--database` (BIOS roots checked against it by archive name), `--output-dir`, `--cache-dir`, `--offline`, `--dry-run` | +| `scraper/arcade_parent_importer.py` | `--profile NAME` (one core version), `--emulators-dir`, `--db` (BIOS roots checked against it by archive name), `--output-dir`, `--cache-dir`, `--offline`, `--dry-run` | | `scraper/romfolders` | `--sources DIR` (default `platforms/romfolders/_sources`), `--output-dir`, `--dry-run` | | `game_rules.py` | `--regions`, `--extras`, `--headers` (files checked against the profiles and the header readers), `--emulators-dir`, `--json` | | `generate_games_index.py` | `--output-dir` (default `install/games`), `--check`, `--quiet` | @@ -1659,7 +1662,7 @@ names the profiles read. | Catalog | Acquisition | Notes | |---------|------------|-------| | Redump | direct fetch from redump.info, one `/bios/` route per system | 4 BIOS DATs. redump.org serves BIOS DATs dated 2010 to 2022, redump.info the current ones | -| No-Intro | local pack, imported with `dat_pack_importer` | Dat-o-Matic serves its DATs through a browser session, and the `hugo19941994/auto-datfile-generator` mirror rebuilds daily | +| No-Intro | local pack, imported with `dat_pack_importer` | Dat-o-Matic serves its DATs through a browser session, and the `dantob/auto-datfile-generator` mirror rebuilds daily. `check_freshness` reads a pack older than a week as a stopped mirror | | TOSEC | local pack, imported with `dat_pack_importer` | annual pack from tosecdev.org, 138 Firmware DATs | The join is by SHA1, then by MD5 + size. A match sets the `provenance` field