chore: merge the platform quirks branch

# Conflicts:
#	install.ps1
#	install.sh
#	install/launchers/index.json
#	wiki/testing-guide.md
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 20:56:04 +01:00
commit 20291e3573
15 files changed
+360 -19

No files matched your search

+1
View File
@@ -65,6 +65,7 @@ on:
- "scripts/sitesources.py"
- "scripts/sitesteps.py"
- "scripts/sitestarlight.py"
- "scripts/sitesteps.py"
- "scripts/sitewrite.py"
- "scripts/slot.py"
- "scripts/slots.py"
+1 -1
View File
@@ -16,7 +16,7 @@
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
# the site's build rewrites this line there (scripts/sitebootstrap.py).
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
$defaultInstallSha256 = "6bd77b283f20cad55d53df8a2e7fe8ef177e821f4dfade37e2d12af2fa3f3852"
$defaultInstallSha256 = "8e76cb7414e0a427b22c617a918db13c6481082ae4bf55e4399e25532b6a81ab"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+79
View File
@@ -3132,6 +3132,72 @@ def fold_case_twins(files: list[dict]) -> tuple[list[dict], list[str]]:
return kept, folded
# The characters FAT and exFAT refuse in a name, as Windows does on any drive
# (Linux 7475ea8b, fs/fat/namei_vfat.c:201-207 and fs/exfat/nls.c:396-403;
# "Naming Files, Paths, and Namespaces" in the Win32 documentation). The
# slash, the backslash and the controls never reach a destination
# (_safe_relative_path). RetroDECK names three files zxs:48.rom,
# zxs:if1-2.rom and snes:st018.data.rom (its retroarch component manifest):
# on an exFAT card each write failed, on every run.
FAT_REFUSED = re.compile(r'[<>:"|?*]')
FAT_REFUSED_SHOWN = '< > : " | ? *'
def holds_fat_refused_names(directory: Path) -> bool:
"""Whether a name holding a colon can be created there.
Probed on the nearest existing directory, as keeps_case is: an ext4
card on a Steam Deck holds one, an exFAT card refuses it with EINVAL.
Windows refuses it on every drive. A probe refused for another cause
(a folder this user cannot write) decides nothing.
"""
if os.name == "nt":
return False
probe_dir = directory
while not probe_dir.is_dir() and probe_dir != probe_dir.parent:
probe_dir = probe_dir.parent
try:
handle, name = tempfile.mkstemp(prefix=".retrobios-name-", suffix=":",
dir=str(probe_dir))
except OSError as exc:
return exc.errno != errno.EINVAL
os.close(handle)
os.unlink(name)
return True
def split_fat_refused(files: list[dict]) -> tuple[list[dict], list[str]]:
"""The entries whose destination a FAT, exFAT or Windows folder can
hold, and the destinations it cannot."""
kept = [entry for entry in files if not FAT_REFUSED.search(str(entry.get("dest", "")))]
refused = [str(entry.get("dest", "")) for entry in files
if FAT_REFUSED.search(str(entry.get("dest", "")))]
return kept, refused
# Android's media scanner marks a folder holding a .nomedia file hidden, and
# every folder below it (MediaProvider a183e2b9, FileUtils.java:1587-1629
# isDirectoryHidden, ModernMediaScanner.java:845-846 and 1017-1020). Without
# one, the samples, overlays and videos of a BIOS folder in shared storage
# showed in the phone's gallery and music apps. RetroArch writes none
# (libretro-common/file/file_path_io.c:194 only skips one).
NOMEDIA = ".nomedia"
def hide_from_media_scanner(folder: Path) -> bool:
"""Leave an empty .nomedia in folder, True when this call wrote it."""
marker = folder / NOMEDIA
if not _is_dir(folder) or _exists(marker):
return False
try:
marker.touch()
except OSError as exc:
print(f" {NOMEDIA} not written in {shown_path(folder)}: {exc.strerror}",
file=sys.stderr)
return False
return True
def installed_cores(dirs: list[Path]) -> list[str]:
"""The cores present in these directories, by the name the buildbot uses.
@@ -15609,6 +15675,15 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
f" {_plural(len(folded), 'file')} left out: {bios_path} does not keep "
f"case and holds another file under the same name ({shown})"
)
if any(FAT_REFUSED.search(str(entry.get("dest", ""))) for entry in files) \
and not holds_fat_refused_names(bios_path):
files, refused = split_fat_refused(files)
shown = ", ".join(refused[:3]) + (" ..." if len(refused) > 3 else "")
print(
f" {_plural(len(refused), 'file')} left out: the file system of "
f"{shown_path(bios_path)} cannot hold a name with any of "
f"{FAT_REFUSED_SHOWN} ({screen_text(shown)})"
)
game_data_cut = 0
if args.no_game_data and not manifest.get("categorized"):
@@ -16359,6 +16434,10 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
failures.append((where, f"{NOT_KEPT} ({reason})"))
total_errors += 1
if os_type == "android" and not args.check and hide_from_media_scanner(bios_path):
print(f" Added {NOMEDIA} to {shown_path(bios_path)}: the gallery and music "
"apps leave its images, sounds and videos out")
# Standalone copies
if (
manifest.get("standalone_copies")
+1 -1
View File
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
# curl or wget stop on a cut transfer before the hash is read: the site's
# build rewrites this line there (scripts/sitebootstrap.py).
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
DEFAULT_INSTALL_SHA256="6bd77b283f20cad55d53df8a2e7fe8ef177e821f4dfade37e2d12af2fa3f3852"
DEFAULT_INSTALL_SHA256="8e76cb7414e0a427b22c617a918db13c6481082ae4bf55e4399e25532b6a81ab"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 1126086,
"sha256": "6bd77b283f20cad55d53df8a2e7fe8ef177e821f4dfade37e2d12af2fa3f3852",
"size": 1129605,
"sha256": "8e76cb7414e0a427b22c617a918db13c6481082ae4bf55e4399e25532b6a81ab",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1179711,
"sha256": "184146fc9785a1aee6b69f0fae3fac5100dc8cb5558a6f369a5cc1ad3301377d"
"size": 1183230,
"sha256": "2ea59a9d6b82a06d58154b2e0efdd7d40372e373340fa4f16f33262e2fe0695e"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 1126086,
"sha256": "6bd77b283f20cad55d53df8a2e7fe8ef177e821f4dfade37e2d12af2fa3f3852",
"size": 1129605,
"sha256": "8e76cb7414e0a427b22c617a918db13c6481082ae4bf55e4399e25532b6a81ab",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 1126086,
"sha256": "6bd77b283f20cad55d53df8a2e7fe8ef177e821f4dfade37e2d12af2fa3f3852",
"size": 1129605,
"sha256": "8e76cb7414e0a427b22c617a918db13c6481082ae4bf55e4399e25532b6a81ab",
"executable": false
},
{
+1 -1
View File
@@ -2599,7 +2599,7 @@
],
"total_files": 8338,
"total_size": 7080104487,
"pack_files": 14921,
"pack_files": 14922,
"pack_size": 7222137851,
"total_omitted": 37,
"omitted_files": [
+18 -1
View File
@@ -416,6 +416,17 @@ def platform_list_version(config: dict, platform: str) -> str:
PACK_DOCUMENTS = ("README.txt", "manifest.json")
def pack_markers(
platform_name: str, target_name: str | None, platforms_dir: str
) -> tuple[str, ...]:
"""The empty files a pack carries for the systems it is unzipped on,
as named before flattening: the .nomedia of a pack for Android."""
from common import list_available_targets
names = [t["name"] for t in list_available_targets(platform_name, platforms_dir)]
return (MEDIA_MARKER,) if lands_on_android(names, target_name) else ()
def core_data_trees(
config: dict,
profiles: dict[str, dict],
@@ -1477,6 +1488,10 @@ def generate_pack(
),
)
_write_generated_member(zf, "README.txt", readme_text)
for marker in pack_markers(platform_name, target_name, platforms_dir):
_write_generated_member(
zf, _flat(f"{base_dest}/{marker}" if base_dest else marker, base_dest, flatten), ""
)
_report_pack_counts(
zip_path, file_status, total_files, core_count, source,
@@ -5147,7 +5162,7 @@ def generate_manifest(
"total_size": total_size,
"pack_files": (
len(manifest_files) + len(data_sizes) + len(PACK_DOCUMENTS)
+ len(rename_notes)
+ len(rename_notes) + len(pack_markers(platform_name, target_name, platforms_dir))
),
"pack_size": total_size + sum(data_sizes),
"total_omitted": len(omitted_by_destination),
@@ -5719,6 +5734,8 @@ def verify_and_finalize_packs(
return all_ok
from packpaths import ( # noqa: E402,F401
MEDIA_MARKER,
lands_on_android,
_archive_destination,
_path_parents,
_has_path_conflict,
+17
View File
@@ -192,6 +192,23 @@ def _register_path(dest: str, seen_files: set[str], seen_parents: set[str]) -> N
for parent in _path_parents(dest):
seen_parents.add(parent)
# Android's media scanner hides a folder holding a .nomedia file, and every
# folder below it (MediaProvider a183e2b9, FileUtils.java:1587-1629
# isDirectoryHidden, ModernMediaScanner.java:845-846). RetroArch writes none
# (libretro-common/file/file_path_io.c:194 only skips one), so a pack
# unzipped into RetroArch Android's system folder put its samples, overlays
# and videos in the gallery and music apps.
MEDIA_MARKER = ".nomedia"
def lands_on_android(target_names: Iterable[str], target: str | None) -> bool:
"""Whether a pack can be unzipped on Android: its target is an Android
one, or, unfiltered, the platform files one."""
if target:
return target.startswith("android")
return any(name.startswith("android") for name in target_names)
def _flat(arcname: str, prefix: str, flatten: bool) -> str:
"""Strip base_destination prefix from ZIP arcname when flattening."""
if flatten and prefix and arcname.startswith(prefix + "/"):
+4 -4
View File
@@ -10,7 +10,7 @@ from collections import Counter
from auto_fetch import DEFAULT_BIOS_DIR
from artifacts import _build_timestamp
from packpaths import _has_path_conflict
from packpaths import MEDIA_MARKER, _has_path_conflict
from ziptools import build_zip_contents_index
from ziptools import check_inside_zip
from nativemode import digest_algorithm
@@ -170,7 +170,7 @@ def verify_pack(
# (bios/INSTRUCTIONS_x.txt, games/keen6/INSTRUCTIONS_y.txt), so
# they are told by their own name, not by a root prefix.
base = name.rsplit("/", 1)[-1]
if base.startswith(("INSTRUCTIONS_", "RENAMED_")) or name in (
if base.startswith(("INSTRUCTIONS_", "RENAMED_")) or base == MEDIA_MARKER or name in (
"manifest.json",
"README.txt",
):
@@ -539,7 +539,7 @@ def _structural_errors(zf: zipfile.ZipFile, zip_set: set) -> list[str]:
if "GraphicMods" not in info.filename and info.filename not in (
"manifest.json",
"README.txt",
):
) and info.filename.rsplit("/", 1)[-1] != MEDIA_MARKER:
errors.append(f"zero-byte: {info.filename}")
return errors
@@ -746,7 +746,7 @@ def verify_pack_against_platform(
is_flat = bool(base_dest) and not any(
n.startswith(base_dest + "/")
for n in zip_set
if n not in ("README.txt", "manifest.json") and not n.endswith("/")
if n not in ("README.txt", "manifest.json", MEDIA_MARKER) and not n.endswith("/")
)
errors.extend(_structural_errors(zf, zip_set))
+9 -2
View File
@@ -4313,9 +4313,16 @@ class DataDirectoriesAreInstalled(unittest.TestCase):
manifest = json.loads((REPO_ROOT / "install" / "retroarch.json").read_text())
dirs = manifest.get("data_directories") or []
files = sum(len(d["files"]) for d in dirs)
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import generate_pack
written = generate_pack.PACK_DOCUMENTS + generate_pack.pack_markers(
"retroarch", None, str(REPO_ROOT / "platforms"))
self.assertEqual(written, ("README.txt", "manifest.json", ".nomedia"))
self.assertEqual(
manifest["pack_files"] - manifest["total_files"] - files,
2, "the pack's two documents are the only files the installer lacks",
manifest["pack_files"] - manifest["total_files"] - files, len(written),
"the pack's two documents and its .nomedia are the only files the "
"installer lacks",
)
+135
View File
@@ -0,0 +1,135 @@
"""What an install does for the device and file system it lands on."""
from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import contextlib
import errno
import hashlib
import io
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(REPO_ROOT / "tests"))
import install # noqa: E402
from test_install_selection import FakeRepository # noqa: E402
class AndroidMediaScanner(unittest.TestCase):
"""Without a .nomedia the BIOS folder's samples, overlays and videos
showed in the phone's gallery and music apps."""
def setUp(self):
self.repo = FakeRepository().__enter__()
self.addCleanup(self.repo.__exit__)
def _run(self, os_name: str, *options: str):
proc = subprocess.run(self.repo.command("--yes", "--no-input", *options),
env={**self.repo.env(), "RETROBIOS_OS": os_name},
capture_output=True, text=True, timeout=60,
stdin=subprocess.DEVNULL)
return self.repo, proc
def test_an_android_install_writes_one(self):
repo, proc = self._run("android")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
marker = repo.dest / ".nomedia"
self.assertTrue(marker.is_file())
self.assertEqual(marker.stat().st_size, 0)
self.assertIn("Added .nomedia", proc.stdout)
def test_a_second_run_leaves_it_and_says_nothing(self):
self._run("android")
_, proc = self._run("android")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertNotIn("Added .nomedia", proc.stdout)
# The marker is not a file of the list: the check finds all in place.
_, check = self._run("android", "--check")
self.assertEqual(check.returncode, 0, check.stdout + check.stderr)
def test_a_check_and_a_dry_run_write_nothing(self):
for options in (("--check",), ("--dry-run",)):
with self.subTest(options=options):
self._run("android", *options)
self.assertFalse((self.repo.dest / ".nomedia").exists())
def test_other_systems_get_none(self):
repo, proc = self._run("linux")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertFalse((repo.dest / ".nomedia").exists())
class NamesAFatCardCannotHold(unittest.TestCase):
"""RetroDECK names three files with a colon (zxs:48.rom): on an exFAT
card each write failed with EINVAL, on every run."""
def test_the_probe_reads_einval_as_a_refusal(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
refused = OSError(errno.EINVAL, "Invalid argument")
closed = PermissionError(errno.EACCES, "Permission denied")
with mock.patch.object(install.os, "name", "posix"):
self.assertTrue(install.holds_fat_refused_names(Path(tmp) / "a" / "b"))
with mock.patch.object(install.tempfile, "mkstemp", side_effect=refused):
self.assertFalse(install.holds_fat_refused_names(Path(tmp)))
with mock.patch.object(install.tempfile, "mkstemp", side_effect=closed):
self.assertTrue(install.holds_fat_refused_names(Path(tmp)))
self.assertEqual(os.listdir(tmp), [])
def test_windows_refuses_them_on_any_drive(self):
with mock.patch.object(install.os, "name", "nt"):
self.assertFalse(install.holds_fat_refused_names(Path(".")))
def _install(self, holds: bool) -> tuple[str, list[str]]:
root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, root, True)
old, dest = root / "old", root / "dest"
old.mkdir()
files = []
for name, payload in (("zxs:48.rom", b"zx 48"), ("gba_bios.bin", b"gba")):
(old / name.replace(":", "_")).write_bytes(payload)
files.append({"dest": name, "sha1": hashlib.sha1(payload).hexdigest(),
"sha256": hashlib.sha256(payload).hexdigest(),
"size": len(payload), "repo_path": f"bios/{name}"})
listed = root / "list.json"
listed.write_text(json.dumps({"manifest_version": 2, "platform": "retrodeck",
"files": files}))
out = io.StringIO()
argv = ["install.py", "--platform", "retrodeck", "--dest", str(dest),
"--manifest", str(listed), "--from", str(old), "--no-input"]
with mock.patch.object(install, "holds_fat_refused_names", return_value=holds), \
mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch("sys.argv", argv), contextlib.redirect_stdout(out), \
contextlib.redirect_stderr(io.StringIO()):
try:
install.main()
except SystemExit as exc:
self.assertIn(exc.code, (0, None), out.getvalue())
self.addCleanup(install.LOCAL.reset)
return (" ".join(out.getvalue().split()),
sorted(p.name for p in dest.iterdir()) if dest.is_dir() else [])
def test_the_names_are_left_out_and_said(self):
said, installed = self._install(holds=False)
self.assertEqual(installed, ["gba_bios.bin"])
self.assertIn("1 file left out: the file system of", said)
self.assertIn('cannot hold a name with any of < > : " | ? * (zxs:48.rom)', said)
def test_a_folder_that_holds_them_gets_them(self):
said, installed = self._install(holds=True)
self.assertEqual(installed, ["gba_bios.bin", "zxs:48.rom"])
self.assertNotIn("left out", said)
if __name__ == "__main__":
unittest.main()
+54
View File
@@ -301,6 +301,60 @@ RECORD = {
}
class APackForAndroidHidesItsMedia(PackCountFixture):
"""RetroArch Android writes no .nomedia, and its system folder held the
samples, overlays and videos the gallery and music apps then showed."""
def _pack(self, target: str | None) -> dict[str, int]:
targets = self.platforms / "targets"
targets.mkdir(exist_ok=True)
(targets / "demo.yml").write_text(yaml.dump({"platform": "demo", "targets": {
"android-arm64-v8a": {"architecture": "aarch64", "cores": ["demo"]},
"linux-x86_64": {"architecture": "x86_64", "cores": ["demo"]},
}}))
out = self.root / f"dist-{target}"
out.mkdir()
zip_path = builder.generate_pack(
"demo", str(self.platforms), self.db, str(self.bios), str(out),
emulators_dir=str(self.emulators), emu_profiles=self.profiles,
data_registry=self.registry, offline=True, target_name=target,
)
with zipfile.ZipFile(zip_path) as archive:
return {m.filename: m.file_size for m in archive.infolist()}
def test_the_whole_pack_and_an_android_one_carry_it(self):
for target in (None, "android-arm64-v8a"):
with self.subTest(target=target):
self.assertEqual(self._pack(target).get(".nomedia"), 0)
def test_a_pack_for_another_target_does_not(self):
self.assertNotIn(".nomedia", self._pack("linux-x86_64"))
def test_the_manifest_counts_it(self):
members = self._pack(None)
manifest = builder.generate_manifest(
"demo", str(self.platforms), self.db, str(self.bios),
str(self.platforms / "_registry.yml"),
emulators_dir=str(self.emulators), emu_profiles=self.profiles,
offline=True, data_registry=self.registry,
)
self.assertEqual(manifest["pack_files"], len(set(members) | {"manifest.json"}))
def test_the_integrity_check_takes_it_for_a_marker(self):
import packverify
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w") as archive:
archive.writestr(".nomedia", b"")
archive.writestr("empty.bin", b"")
with zipfile.ZipFile(buffer) as archive:
errors = packverify._structural_errors(archive, set(archive.namelist()))
self.assertEqual(errors, ["zero-byte: empty.bin"])
def test_a_platform_without_android_gets_none(self):
self.assertEqual(builder.pack_markers("demo", None, str(self.platforms)), ())
class TheTableDescribesTheRelease(unittest.TestCase):
"""The Download link gives the last release, not what main would build.
+15
View File
@@ -1473,6 +1473,15 @@ elsewhere. A custom BIOS directory is still named directly:
python install.py --platform retroarch --dest /storage/emulated/0/RetroArch/system
```
An install on Android leaves an empty `.nomedia` in the BIOS folder. Android's
media scanner hides a folder holding one, and every folder below it
(MediaProvider, `FileUtils.java` `isDirectoryHidden`), so the samples,
overlays and videos of the BIOS folder stay out of the gallery and music
apps. RetroArch writes none. A run that finds one already there leaves it,
and `--check` and `--dry-run` write nothing. The RetroArch pack, unzipped on
Android as on other systems, carries the same empty `.nomedia` beside
`README.txt`. A pack built for a target other than Android does not.
## On a handheld { #on-a-handheld-the-retrobios-port }
RetroBIOS also runs on the handheld itself, in the
@@ -2010,6 +2019,12 @@ opened from Windows, an exFAT card), one overwrites the other. The installer
checks the folder first, keeps one file per name there and lists the ones it
left out.
FAT, exFAT and every Windows drive also refuse `< > : " | ? *` in a name
(Linux `fs/fat/namei_vfat.c:201-207`, `fs/exfat/nls.c:396-403`). RetroDECK's
list names three files with a colon, like `zxs:48.rom`. Before writing such a
name, the installer creates one in the BIOS folder: on an ext4 card it goes
in, on an exFAT card the installer leaves those files out and names them.
### Packs and their parts
`scripts/download.py --list` and `scripts/download.sh --list` print the packs of
+16
View File
@@ -169,6 +169,22 @@ Running `verify.py --emulator <core> --verbose` shows the emulator-level ground
It reaches only as far as the profiles do. Files no profile states a value for are platform-only, and nothing can be said about their content. The footer of the platform report gives that ratio as `Ground truth: N/M files have emulator validation`.
## Why do BIOS pictures and sounds show in a phone's gallery?
Android's media scanner indexes every image, sound and video in shared
storage, and the RetroArch BIOS folder holds 3,842 of them (FBNeo samples,
overlays, PPSSPP assets, sdlpal's videos). An empty file named `.nomedia` in
the BIOS folder hides it and every folder below it. The installer writes one
on Android and the RetroArch pack carries one: see [Android](installer.md#android).
Copied by hand without either, the folder needs that empty file created by hand.
## Why were files left out on an exFAT card?
FAT, exFAT and Windows refuse `< > : " | ? *` in a name, and RetroDECK's list
names three files with a colon, like `zxs:48.rom`. The installer leaves those
out on such a card and names them. An ext4 card holds them. See
[Upper and lower case](installer.md#upper-and-lower-case).
## Next steps
- [Reference](index.md), every reference page in one list
+1 -1
View File
@@ -59,7 +59,7 @@ Every module in `tests/`, by the area it guards.
| Packs and manifests | synthetic + real packs | `test_artifact_lock`, `test_atomic_artifacts`, `test_batocera_cores`, `test_core_data_trees`, `test_deterministic_zip`, `test_emulator_pack_counts`, `test_finalize_pack_names`, `test_large_file_cache`, `test_libretro_layout`, `test_manifest_entry`, `test_minui_platform`, `test_no_game_data`, `test_pack_conformance_dirs`, `test_pack_core_extras`, `test_pack_counts`, `test_pack_destinations`, `test_pack_failures`, `test_pack_file_names`, `test_pack_grouping`, `test_pack_integrity`, `test_pack_member_identity`, `test_pack_readme`, `test_platform_inheritance`, `test_platform_keys`, `test_release_packs`, `test_required_only_archives`, `test_required_only_twins`, `test_save_root`, `test_split_extras`, `test_split_names`, `test_split_pack`, `test_system_pack_extras`, `test_system_pack_parity`, `test_torrentzip`, `test_which_pack` |
| Slots and regions | synthetic + profiles | `test_region`, `test_region_agnostic_scan`, `test_region_groups`, `test_region_held`, `test_region_platform_index`, `test_region_required_only`, `test_slot`, `test_slots`, `test_slots_shipped_file` |
| Hardware targets and systems | synthetic + target files | `test_amberelec_targets`, `test_emuelec_targets`, `test_extract_targets`, `test_lakka_targets`, `test_libretro_slugs`, `test_system_ids`, `test_system_selection`, `test_target_names`, `test_target_scrapers`, `test_target_systems`, `test_target_verify` |
| Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_app`, `test_install_from`, `test_install_games`, `test_install_offline`, `test_install_selection`, `test_install_tui`, `test_pypi_package` |
| Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_filesystems`, `test_install_app`, `test_install_from`, `test_install_games`, `test_install_offline`, `test_install_selection`, `test_install_tui`, `test_pypi_package` |
| Handheld launchers | a pty as the console, fake installer and firmware tools, stubbed PortMaster dialogs | `test_handheld_launchers`, `test_port` |
| Running cores | parsers on fixed inputs, a libretro core built from C in the test when a compiler exists | `test_core_probe` |
| Measurement records | synthetic record files, the committed `measurements/` against HEAD, the **Proof** line naming the BIOS a core ran, and that no page shows a Tested block, a region outcome or a time per frame | `test_measurements` |