mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
Batocera starts MAME with -rompath <romDirname>;/userdata/bios/mame; /userdata/bios, /userdata/roms/mame and the softlist folder added for MESS systems, and -samplepath, -artpath and -crosshairpath below /userdata/bios/mame (generators/mame/mameGenerator.py:129-143, 189 at batocera-43.1, MAME_BIOS in mamePaths.py). No extractor read that generator. It now goes through the generic reader KNULLI uses for its fork of the same file, keeping the command line search settings: the Macintosh boot disk the command names is a file. Rescraped at batocera-43.1: the six records are the only change. Manifests: Batocera drops samples/bbc.zip at the BIOS root, read by neither build. mame/samples/bbc.zip, which its list declares, serves both. The sets stay at the BIOS root, which the rompath lists.
1020 lines
50 KiB
Python
1020 lines
50 KiB
Python
"""A platform's launcher moves where an emulator's file lands.
|
|
|
|
A profile's ``path`` is where the emulator's code reads a file. UAE4All2
|
|
reads kickstarts/<name> below its working directory, and RetroPie starts it
|
|
in its install directory, whose kickstarts/ it links to BIOS/amiga. The
|
|
pack, the manifest and verify all put the file in amiga/ on that platform,
|
|
and a platform with no such record keeps the profile's layout.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
|
|
|
|
import hashlib
|
|
import io
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
from contextlib import redirect_stdout
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
|
|
from common import ( # noqa: E402
|
|
_emulator_profiles_cache,
|
|
load_emulator_profiles,
|
|
load_platform_config,
|
|
platform_layouts,
|
|
standalone_set,
|
|
)
|
|
from generate_pack import generate_manifest, generate_pack # noqa: E402
|
|
from verify import verify_platform # noqa: E402
|
|
|
|
WORKING_DIRECTORY = {
|
|
"emulator": "kick", "build": "standalone", "kind": "passes_path", "names": [],
|
|
"search_dir": "$md_inst", "passes_to": "working directory of $romdir/amiga/+Start.sh",
|
|
"source_ref": "kick.sh:90",
|
|
}
|
|
LINK = {
|
|
"emulator": "kick", "build": "standalone", "kind": "links", "names": [],
|
|
"search_dir": "$biosdir/amiga", "passes_to": "$md_inst/kickstarts is a symlink to it",
|
|
"source_ref": "kick.sh:79",
|
|
}
|
|
|
|
|
|
class ALinkMovesTheDestination(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
_emulator_profiles_cache.clear()
|
|
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
|
|
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
|
self.addCleanup(shutil.rmtree, self.root, True)
|
|
bios = self.root / "bios" / "Commodore" / "Amiga"
|
|
bios.mkdir(parents=True)
|
|
files, by_md5, by_name = {}, {}, {}
|
|
for name, data in (("kick13.rom", b"KICKSTART 1.3"), ("kick.rom", b"CUSTOM KICKSTART")):
|
|
(bios / name).write_bytes(data)
|
|
sha1 = hashlib.sha1(data).hexdigest()
|
|
md5 = hashlib.md5(data).hexdigest()
|
|
files[sha1] = {"path": str(bios / name), "name": name, "sha1": sha1,
|
|
"md5": md5, "crc32": "", "size": len(data)}
|
|
by_md5[md5] = sha1
|
|
by_name[name] = [sha1]
|
|
self.db = {
|
|
"files": files,
|
|
"indexes": {"by_md5": by_md5, "by_name": by_name, "by_crc32": {},
|
|
"by_path_suffix": {}},
|
|
}
|
|
kick13 = by_name["kick13.rom"][0]
|
|
self.platforms = self.root / "platforms"
|
|
self.emulators = self.root / "emulators"
|
|
self.platforms.mkdir()
|
|
self.emulators.mkdir()
|
|
for name, launchers in (("linkplat", [WORKING_DIRECTORY, LINK]), ("plainplat", [])):
|
|
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
|
|
"platform": name, "version": "1", "verification_mode": "existence",
|
|
"base_destination": "BIOS", "cores": ["kick"], "standalone_cores": ["kick"],
|
|
"launchers": launchers,
|
|
"systems": {"commodore-amiga": {"files": []}},
|
|
}))
|
|
(self.platforms / "_registry.yml").write_text(yaml.safe_dump({"platforms": {
|
|
"linkplat": {"config": "linkplat.yml"}, "plainplat": {"config": "plainplat.yml"},
|
|
}}))
|
|
(self.emulators / "kick.yml").write_text(yaml.safe_dump({
|
|
"emulator": "Kick", "type": "standalone", "cores": ["kick"],
|
|
"systems": ["commodore-amiga"],
|
|
"standalone_directories": ["getcwd"],
|
|
"files": [
|
|
{"name": "kick13.rom", "path": "kickstarts/kick13.rom",
|
|
"sha1": kick13, "required": False},
|
|
# Read in the working directory itself, which the platform
|
|
# links nowhere in the BIOS tree.
|
|
{"name": "kick.rom", "path": "kick.rom", "required": False},
|
|
],
|
|
}))
|
|
|
|
def pack_names(self, platform: str, name: str = "kick13.rom") -> list[str]:
|
|
with redirect_stdout(io.StringIO()):
|
|
zip_path = generate_pack(
|
|
platform, str(self.platforms), self.db, str(self.root / "bios"),
|
|
str(self.root / "out" / platform), emulators_dir=str(self.emulators),
|
|
offline=True,
|
|
)
|
|
with zipfile.ZipFile(zip_path) as zf:
|
|
return [n for n in zf.namelist() if n.rsplit("/", 1)[-1] == name]
|
|
|
|
def manifest(self, platform: str) -> dict:
|
|
with redirect_stdout(io.StringIO()):
|
|
return generate_manifest(
|
|
platform, str(self.platforms), self.db, str(self.root / "bios"),
|
|
str(self.platforms / "_registry.yml"),
|
|
emulators_dir=str(self.emulators), offline=True,
|
|
)
|
|
|
|
def manifest_dests(self, platform: str, name: str = "kick13.rom") -> list[str]:
|
|
return [f["dest"] for f in self.manifest(platform)["files"]
|
|
if f["dest"].rsplit("/", 1)[-1] == name]
|
|
|
|
def verify_entries(self, platform: str, name: str = "kick13.rom") -> list[dict]:
|
|
config = load_platform_config(platform, str(self.platforms))
|
|
result = verify_platform(config, self.db, str(self.emulators))
|
|
return [u for u in result["undeclared_files"] if u["name"] == name]
|
|
|
|
def verify_paths(self, platform: str) -> list[str]:
|
|
return [u["path"] for u in self.verify_entries(platform)]
|
|
|
|
def test_the_link_moves_it_in_pack_manifest_and_verify(self) -> None:
|
|
self.assertEqual(self.pack_names("linkplat"), ["amiga/kick13.rom"])
|
|
self.assertEqual(self.manifest_dests("linkplat"), ["amiga/kick13.rom"])
|
|
self.assertEqual(self.verify_paths("linkplat"), ["amiga/kick13.rom"])
|
|
|
|
def test_a_platform_without_the_record_is_unchanged(self) -> None:
|
|
self.assertEqual(self.pack_names("plainplat"), ["kickstarts/kick13.rom"])
|
|
self.assertEqual(self.manifest_dests("plainplat"), ["kickstarts/kick13.rom"])
|
|
self.assertEqual(self.verify_paths("plainplat"), ["kickstarts/kick13.rom"])
|
|
|
|
def test_a_file_outside_every_binding_is_named_never_placed(self) -> None:
|
|
self.assertEqual(self.pack_names("linkplat", "kick.rom"), [])
|
|
manifest = self.manifest("linkplat")
|
|
self.assertEqual(self.manifest_dests("linkplat", "kick.rom"), [])
|
|
omitted = [o for o in manifest["omitted_files"] if o["name"] == "kick.rom"]
|
|
self.assertEqual(len(omitted), 1)
|
|
self.assertEqual(
|
|
(omitted[0]["dest"], omitted[0]["root"], omitted[0]["reason"], omitted[0]["cores"]),
|
|
("kick.rom", "emulator", "outside_bios_folder", ["kick"]),
|
|
)
|
|
(entry,) = self.verify_entries("linkplat", "kick.rom")
|
|
self.assertFalse(entry["in_pack"])
|
|
self.assertIn("outside the BIOS tree", entry["omitted"])
|
|
|
|
def test_a_root_file_of_another_core_is_not_claimed_by_it(self) -> None:
|
|
# Another core reads kick.rom at the BIOS root: kick's own kick.rom,
|
|
# in its working directory, is a different place.
|
|
for name in ("linkplat", "plainplat"):
|
|
path = self.platforms / f"{name}.yml"
|
|
data = yaml.safe_load(path.read_text())
|
|
data["cores"] = ["kick", "other"]
|
|
path.write_text(yaml.safe_dump(data))
|
|
(self.emulators / "other.yml").write_text(yaml.safe_dump({
|
|
"emulator": "Other", "type": "libretro", "cores": ["other"],
|
|
"systems": ["commodore-amiga"],
|
|
"files": [{"name": "kick.rom", "path": "kick.rom", "required": False}],
|
|
}))
|
|
_emulator_profiles_cache.clear()
|
|
manifest = self.manifest("linkplat")
|
|
(held,) = [f for f in manifest["files"] if f["dest"] == "kick.rom"]
|
|
self.assertEqual(held["cores"], ["other"])
|
|
omitted = [o for o in manifest["omitted_files"] if o["name"] == "kick.rom"]
|
|
self.assertEqual([(o["root"], o["cores"]) for o in omitted], [("emulator", ["kick"])])
|
|
|
|
def test_two_builds_in_one_folder_are_one_omission(self) -> None:
|
|
# RetroPie builds amiberry 5.7.2 and 6.3.3, one per board, both in
|
|
# $md_inst of its amiberry package: one place, read by both.
|
|
data = yaml.safe_load((self.emulators / "kick.yml").read_text())
|
|
for key in ("kick-1", "kick-2"):
|
|
(self.emulators / f"{key}.yml").write_text(
|
|
yaml.safe_dump({**data, "built_by": ["linkplat"]}))
|
|
_emulator_profiles_cache.clear()
|
|
omitted = [o for o in self.manifest("linkplat")["omitted_files"]
|
|
if o["name"] == "kick.rom"]
|
|
self.assertEqual([(o["root"], o["cores"]) for o in omitted],
|
|
[("emulator", ["kick-1", "kick-2"])])
|
|
|
|
def test_the_installer_accepts_the_omission(self) -> None:
|
|
import install
|
|
|
|
manifest = self.manifest("linkplat")
|
|
omitted = next(o for o in manifest["omitted_files"] if o["name"] == "kick.rom")
|
|
install._check_omission(omitted, omitted["dest"])
|
|
# Two emulators reading kick.rom in their own folders read two files.
|
|
self.assertNotEqual(
|
|
install._entry_key(omitted), install._entry_key({**omitted, "cores": ["other"]})
|
|
)
|
|
self.assertNotEqual(install._entry_key(omitted), install._entry_key({"dest": "kick.rom"}))
|
|
self.assertEqual(install.OMISSION_REASONS[omitted["reason"]], "read outside the BIOS folder")
|
|
with self.assertRaises(ValueError):
|
|
install._check_root({"root": "emulator"}, "kick.rom")
|
|
install._check_root(omitted, "kick.rom", omitted=True)
|
|
|
|
def test_without_relocation_the_root_file_stays_at_the_root(self) -> None:
|
|
self.assertEqual(self.pack_names("plainplat", "kick.rom"), ["kick.rom"])
|
|
|
|
def test_the_link_alone_binds_nothing(self) -> None:
|
|
# Without the working directory the link's $md_inst is not the
|
|
# directory the profile's paths are relative to.
|
|
config = {"standalone_cores": ["kick"], "launchers": [LINK]}
|
|
profile = load_emulator_profiles(str(self.emulators))["kick"]
|
|
(view, standalone), = platform_layouts("kick", profile, standalone_set(config))
|
|
self.assertTrue(standalone)
|
|
self.assertNotIn("standalone_path", view["files"][0])
|
|
|
|
|
|
class UAE4AllOnRetroPie(unittest.TestCase):
|
|
"""The real profile against the real launcher records."""
|
|
|
|
def test_kickstarts_land_in_amiga(self) -> None:
|
|
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["uae4all"]
|
|
config = load_platform_config("retropie", str(REPO_ROOT / "platforms"))
|
|
(view, standalone), = platform_layouts("uae4all", profile, standalone_set(config))
|
|
self.assertTrue(standalone)
|
|
for entry in view["files"]:
|
|
if entry["path"].startswith("kickstarts/"):
|
|
self.assertEqual(
|
|
entry["standalone_path"], "amiga/" + entry["path"].removeprefix("kickstarts/")
|
|
)
|
|
else:
|
|
# kick.rom and rom.key in the working directory, $md_inst
|
|
self.assertEqual(entry.get("unplaced"), "outside_bios_folder", entry["path"])
|
|
|
|
|
|
|
|
class FsUaeOnRetroPie(unittest.TestCase):
|
|
"""RetroPie lists KS 1.3 as kick34005.A500, and its fs-uae launcher
|
|
passes $biosdir/kick13.rom (fs-uae.sh:23-26). The listed copy settles
|
|
fs-uae's entry by content, and the launcher's name was filled only as
|
|
a copy of UAE4All2's amiga/kick13.rom: a board without UAE4All2 lost it."""
|
|
|
|
def test_the_launcher_name_is_filled_without_another_core(self) -> None:
|
|
import json
|
|
|
|
from common import load_target_config
|
|
|
|
db_path = REPO_ROOT / "database.json"
|
|
if not db_path.exists():
|
|
self.skipTest("database.json is not built")
|
|
db = json.loads(db_path.read_text(encoding="utf-8"))
|
|
cores = load_target_config("retropie", "x86", str(REPO_ROOT / "platforms"))
|
|
self.assertNotIn("uae4all", cores)
|
|
with redirect_stdout(io.StringIO()):
|
|
manifest = generate_manifest(
|
|
"retropie", str(REPO_ROOT / "platforms"), db, str(REPO_ROOT / "bios"),
|
|
str(REPO_ROOT / "platforms" / "_registry.yml"),
|
|
emulators_dir=str(REPO_ROOT / "emulators"), offline=True,
|
|
target_cores=set(cores), target_name="x86",
|
|
)
|
|
held = [(f["dest"], f["cores"]) for f in manifest["files"]
|
|
if f["dest"].rsplit("/", 1)[-1] == "kick13.rom"]
|
|
self.assertEqual(held, [("kick13.rom", ["fs-uae"])])
|
|
|
|
|
|
class XboxOnRetroBat(unittest.TestCase):
|
|
"""RetroBat's launcher writes xemu's eeprom.bin and disk image into
|
|
saves/<system> and cxbx-reloaded's EEPROM.bin into its emulator folder,
|
|
and each Cxbx build reads its data from the folder it runs from
|
|
(Xemu.Generator.cs:82-113, Cxbx.Generator.cs:99-115, 158, 216-223)."""
|
|
|
|
def test_nothing_lands_at_the_bios_root(self) -> None:
|
|
import json
|
|
|
|
db_path = REPO_ROOT / "database.json"
|
|
if not db_path.exists():
|
|
self.skipTest("database.json is not built")
|
|
db = json.loads(db_path.read_text(encoding="utf-8"))
|
|
with redirect_stdout(io.StringIO()):
|
|
manifest = generate_manifest(
|
|
"retrobat", str(REPO_ROOT / "platforms"), db, str(REPO_ROOT / "bios"),
|
|
str(REPO_ROOT / "platforms" / "_registry.yml"),
|
|
emulators_dir=str(REPO_ROOT / "emulators"), offline=True,
|
|
)
|
|
at_root = {f["dest"].lower() for f in manifest["files"] if not f.get("root")}
|
|
# The launcher writes the two EEPROMs and the disk image, mounts an
|
|
# .iso with its own tool, and each Cxbx build reads keys.bin from
|
|
# its own folder. wpcap.dll comes with the Npcap driver.
|
|
self.assertFalse(at_root & {
|
|
"eeprom.bin", "xbox_hdd.qcow2", "dokan2.dll", "keys.bin", "wpcap.dll",
|
|
})
|
|
placed = {(f.get("root"), f["dest"]) for f in manifest["files"]}
|
|
self.assertIn(("emulators", "cxbx-reloaded/keys.bin"), placed)
|
|
self.assertIn(("emulators", "chihiro/EEPROM.bin"), placed)
|
|
self.assertNotIn(("emulators", "cxbx-reloaded/EEPROM.bin"), placed)
|
|
|
|
|
|
PROGRAM = b"PROGRAM BINARY"
|
|
PROGRAM_DATA = b"PROGRAM DATA"
|
|
|
|
|
|
class AProgramRunFromItsFolder(unittest.TestCase):
|
|
"""A program that reads its files beside its executable.
|
|
|
|
The profile gives the layout of the program's archive (prog/prog,
|
|
prog/prog.dat) and declares the executable directory. A platform that
|
|
runs prog/prog from its BIOS folder keeps that layout, one that runs it
|
|
from roms/prog places the program folder below its roms folder, and one
|
|
that runs it from an emulator folder names its files as not placeable.
|
|
"""
|
|
|
|
def setUp(self) -> None:
|
|
_emulator_profiles_cache.clear()
|
|
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
|
|
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
|
self.addCleanup(shutil.rmtree, self.root, True)
|
|
bios = self.root / "bios" / "Maker" / "Prog"
|
|
bios.mkdir(parents=True)
|
|
files, by_md5, by_name = {}, {}, {}
|
|
for name, data in (("prog", PROGRAM), ("prog.dat", PROGRAM_DATA)):
|
|
(bios / name).write_bytes(data)
|
|
sha1 = hashlib.sha1(data).hexdigest()
|
|
md5 = hashlib.md5(data).hexdigest()
|
|
files[sha1] = {"path": str(bios / name), "name": name, "sha1": sha1,
|
|
"md5": md5, "crc32": "", "size": len(data)}
|
|
by_md5[md5] = sha1
|
|
by_name[name] = [sha1]
|
|
self.db = {"files": files, "indexes": {
|
|
"by_md5": by_md5, "by_name": by_name, "by_crc32": {}, "by_path_suffix": {}}}
|
|
self.platforms = self.root / "platforms"
|
|
self.emulators = self.root / "emulators"
|
|
self.platforms.mkdir()
|
|
self.emulators.mkdir()
|
|
run = {"emulator": "prog", "build": "standalone", "kind": "fixed_name",
|
|
"passes_to": "exec", "source_ref": "start_prog.sh:9"}
|
|
launchers = {
|
|
"biosplat": [{**run, "names": ["prog/prog"], "search_dir": "/userdata/bios"}],
|
|
"romsplat": [{**run, "names": ["prog"], "search_dir": "/storage/roms/prog"}],
|
|
"exeplat": [{**run, "names": ["prog"], "search_dir": "<emulator folder>"}],
|
|
"emuplat": [{**run, "names": ["prog"], "search_dir": '<GetPath("prog")>'}],
|
|
"plainplat": [],
|
|
}
|
|
for name, records in launchers.items():
|
|
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
|
|
"platform": name, "version": "1", "verification_mode": "existence",
|
|
"base_destination": "bios", "cores": ["prog"], "standalone_cores": ["prog"],
|
|
"launchers": records,
|
|
"systems": {"prog-system": {"files": []}},
|
|
}))
|
|
(self.platforms / "_registry.yml").write_text(yaml.safe_dump({"platforms": {
|
|
**{name: {"config": f"{name}.yml"} for name in launchers},
|
|
"emuplat": {"config": "emuplat.yml", "emulators_directory": {
|
|
"launcher_spelling": '<GetPath("{key}")>', "bios_folder": "bios",
|
|
"searched": ["frontend/{key}", "emulators/{key}"],
|
|
"default": "emulators/{key}", "config": "frontend/launcher.cfg"}},
|
|
}}))
|
|
# The roms root as the OS's own source records it (romfolders).
|
|
(self.platforms / "romfolders").mkdir()
|
|
(self.platforms / "romfolders" / "romsplat.yml").write_text(yaml.safe_dump({
|
|
"setup": "romsplat", "platform": "romsplat",
|
|
"roms_root": {"path": "/storage/roms", "from_bios": "..", "source_ref": "os x.mk:1"},
|
|
"folders": [],
|
|
}))
|
|
# A list that installs into the roms root itself, its BIOS folder
|
|
# one of the folders there (AmberELEC's /storage/roms).
|
|
(self.platforms / "rootplat.yml").write_text(yaml.safe_dump({
|
|
"platform": "rootplat", "version": "1", "verification_mode": "existence",
|
|
"base_destination": "", "cores": ["prog"], "standalone_cores": ["prog"],
|
|
"launchers": [{**run, "names": ["prog"], "search_dir": "/storage/roms/prog"}],
|
|
"systems": {"prog-system": {"files": [
|
|
{"name": "own.bin", "destination": "bios/own.bin", "required": False},
|
|
]}},
|
|
}))
|
|
registry = yaml.safe_load((self.platforms / "_registry.yml").read_text())
|
|
registry["platforms"]["rootplat"] = {"config": "rootplat.yml"}
|
|
(self.platforms / "_registry.yml").write_text(yaml.safe_dump(registry))
|
|
(self.platforms / "romfolders" / "rootplat.yml").write_text(yaml.safe_dump({
|
|
"setup": "rootplat", "platform": "rootplat",
|
|
"roms_root": {"path": "/storage/roms", "from_bios": "..", "source_ref": "os x.mk:1"},
|
|
"folders": [],
|
|
}))
|
|
(self.emulators / "prog.yml").write_text(yaml.safe_dump({
|
|
"emulator": "Prog", "type": "standalone", "cores": ["prog"],
|
|
"systems": ["prog-system"],
|
|
"standalone_directories": ["executable directory"],
|
|
"files": [
|
|
{"name": "prog", "path": "prog/prog", "sha1": by_name["prog"][0],
|
|
"required": True},
|
|
{"name": "prog.dat", "path": "prog/prog.dat",
|
|
"sha1": by_name["prog.dat"][0], "required": True},
|
|
],
|
|
}))
|
|
|
|
def pack(self, platform: str) -> list[str]:
|
|
with redirect_stdout(io.StringIO()):
|
|
zip_path = generate_pack(
|
|
platform, str(self.platforms), self.db, str(self.root / "bios"),
|
|
str(self.root / "out" / platform), emulators_dir=str(self.emulators),
|
|
offline=True,
|
|
)
|
|
with zipfile.ZipFile(zip_path) as zf:
|
|
return sorted(n for n in zf.namelist() if n.split("/")[-1].startswith("prog"))
|
|
|
|
def manifest(self, platform: str) -> dict:
|
|
with redirect_stdout(io.StringIO()):
|
|
return generate_manifest(
|
|
platform, str(self.platforms), self.db, str(self.root / "bios"),
|
|
str(self.platforms / "_registry.yml"),
|
|
emulators_dir=str(self.emulators), offline=True,
|
|
)
|
|
|
|
def verify(self, platform: str) -> list[dict]:
|
|
config = load_platform_config(platform, str(self.platforms))
|
|
result = verify_platform(config, self.db, str(self.emulators))
|
|
return [u for u in result["undeclared_files"] if u["name"].startswith("prog")]
|
|
|
|
def test_run_from_the_bios_folder_keeps_the_layout(self) -> None:
|
|
for platform in ("biosplat", "plainplat"):
|
|
with self.subTest(platform=platform):
|
|
self.assertEqual(self.pack(platform), ["prog/prog", "prog/prog.dat"])
|
|
self.assertEqual(
|
|
sorted(f["dest"] for f in self.manifest(platform)["files"]),
|
|
["prog/prog", "prog/prog.dat"],
|
|
)
|
|
|
|
def test_run_from_the_roms_folder_is_installed_there(self) -> None:
|
|
self.assertEqual(self.pack("romsplat"), [])
|
|
manifest = self.manifest("romsplat")
|
|
self.assertEqual(
|
|
sorted((f["dest"], f.get("root")) for f in manifest["files"]),
|
|
[("prog/prog", "roms"), ("prog/prog.dat", "roms")],
|
|
)
|
|
self.assertEqual(manifest["roms_directory"], {"bios_folder": "bios", "from_bios": ".."})
|
|
entries = self.verify("romsplat")
|
|
self.assertEqual(sorted(u["path"] for u in entries), ["prog/prog", "prog/prog.dat"])
|
|
self.assertTrue(all(u["in_pack"] is False for u in entries))
|
|
|
|
def test_a_roms_root_inside_the_install_tree_is_a_plain_dest(self) -> None:
|
|
self.assertEqual(self.pack("rootplat"), ["prog/prog", "prog/prog.dat"])
|
|
manifest = self.manifest("rootplat")
|
|
self.assertEqual(
|
|
sorted((f["dest"], f.get("root")) for f in manifest["files"] if "prog" in f["dest"]),
|
|
[("prog/prog", None), ("prog/prog.dat", None)],
|
|
)
|
|
self.assertNotIn("roms_directory", manifest)
|
|
|
|
def test_run_from_an_emulator_folder_is_named(self) -> None:
|
|
self.assertEqual(self.pack("exeplat"), [])
|
|
manifest = self.manifest("exeplat")
|
|
self.assertEqual([f["dest"] for f in manifest["files"]], [])
|
|
self.assertEqual(
|
|
sorted((o["dest"], o["root"], o["reason"]) for o in manifest["omitted_files"]),
|
|
[("prog/prog", "emulator", "outside_bios_folder"),
|
|
("prog/prog.dat", "emulator", "outside_bios_folder")],
|
|
)
|
|
|
|
def test_the_installer_finds_the_roms_folder_above_the_bios_folder(self) -> None:
|
|
import install
|
|
|
|
manifest = self.manifest("romsplat")
|
|
install._check_relative_directories(manifest)
|
|
files = list(manifest["files"])
|
|
card = self.root / "card" / "roms" / "bios"
|
|
kept, left = install.place_relative_entries(files, card, manifest, "romsplat", say=False)
|
|
self.assertEqual((len(kept), left), (2, []))
|
|
self.assertEqual(
|
|
install._entry_path(card, kept[0]),
|
|
(card.parent / kept[0]["dest"]).resolve()
|
|
if install._entry_path(card, kept[0]).is_absolute() else card.parent / kept[0]["dest"],
|
|
)
|
|
other = self.root / "elsewhere"
|
|
kept, left = install.place_relative_entries(files, other, manifest, "romsplat", say=False)
|
|
self.assertEqual(kept, [])
|
|
self.assertEqual({o["reason"] for o in left}, {"no_roms_directory"})
|
|
with self.assertRaises(ValueError):
|
|
install._check_relative_directories({"files": files})
|
|
# A roms root beside the BIOS folder (Batocera's ../roms).
|
|
beside = {**manifest, "roms_directory": {"bios_folder": "bios", "from_bios": "../roms"}}
|
|
install._check_relative_directories(beside)
|
|
disk = self.root / "userdata" / "bios"
|
|
install.place_relative_entries(files, disk, beside, "romsplat", say=False)
|
|
self.assertEqual(install._entry_place(disk, kept_entry := files[0])[0],
|
|
self.root / "userdata" / "roms")
|
|
self.assertEqual(kept_entry["root"], "roms")
|
|
for climbing in ("roms", "../roms/..", "/storage/roms"):
|
|
with self.subTest(from_bios=climbing), self.assertRaises(ValueError):
|
|
install._check_relative_directories(
|
|
{**manifest, "roms_directory": {"bios_folder": "bios", "from_bios": climbing}}
|
|
)
|
|
|
|
def test_run_from_an_emulator_folder_the_registry_describes(self) -> None:
|
|
self.assertEqual(self.pack("emuplat"), [])
|
|
manifest = self.manifest("emuplat")
|
|
self.assertEqual(
|
|
sorted((f["dest"], f.get("root")) for f in manifest["files"]),
|
|
[("prog/prog", "emulators"), ("prog/prog.dat", "emulators")],
|
|
)
|
|
self.assertEqual(manifest["emulators_directory"]["default"], "emulators/{key}")
|
|
self.assertTrue(all(u["in_pack"] is False for u in self.verify("emuplat")))
|
|
|
|
def test_the_installer_follows_the_launcher_search(self) -> None:
|
|
import install
|
|
|
|
manifest = self.manifest("emuplat")
|
|
install._check_emulators_directory(manifest)
|
|
files = list(manifest["files"])
|
|
home = self.root / "Frontend"
|
|
bios = home / "bios"
|
|
bios.mkdir(parents=True)
|
|
# Not the platform's folder: no launcher config beside it.
|
|
kept, left = install.place_emulator_entries(files, bios, manifest, "emuplat", say=False)
|
|
self.assertEqual((kept, {o["reason"] for o in left}), ([], {"no_emulators_directory"}))
|
|
(home / "frontend").mkdir()
|
|
(home / "frontend" / "launcher.cfg").write_text("other=x\n")
|
|
kept, left = install.place_emulator_entries(files, bios, manifest, "emuplat", say=False)
|
|
self.assertEqual((len(kept), left), (2, []))
|
|
entry = next(f for f in kept if f["dest"] == "prog/prog")
|
|
self.assertEqual(install._entry_place(bios, entry), (home / "emulators" / "prog", "prog"))
|
|
# The first searched place that exists wins.
|
|
(home / "frontend" / "prog").mkdir()
|
|
install.place_emulator_entries(files, bios, manifest, "emuplat", say=False)
|
|
self.assertEqual(install._entry_place(bios, entry)[0], home / "frontend" / "prog")
|
|
# A key the launcher config sets moves the folder: left out.
|
|
(home / "frontend" / "launcher.cfg").write_text("prog=D:\\Prog\n")
|
|
kept, left = install.place_emulator_entries(files, bios, manifest, "emuplat", say=False)
|
|
self.assertEqual((kept, len(left)), ([], 2))
|
|
|
|
|
|
class EachProgramItsOwnProfile(unittest.TestCase):
|
|
"""PICO-8 and Voxatron are two programs: a platform running one gets its files only."""
|
|
|
|
def test_only_the_platforms_that_run_voxatron_get_it(self) -> None:
|
|
from common import list_registered_platforms, resolve_platform_cores
|
|
|
|
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
|
runs = {
|
|
name: {k for k in ("lexaloffle", "voxatron")
|
|
if k in resolve_platform_cores(
|
|
load_platform_config(name, str(REPO_ROOT / "platforms")), profiles)}
|
|
for name in list_registered_platforms(str(REPO_ROOT / "platforms"))
|
|
}
|
|
self.assertEqual(
|
|
{name for name, found in runs.items() if "voxatron" in found}, {"batocera", "knulli"}
|
|
)
|
|
self.assertTrue(all("lexaloffle" in runs[name] for name in (
|
|
"batocera", "knulli", "rocknix", "retrodeck", "retrobat", "recalbox")))
|
|
|
|
def test_a_standalone_emulator_is_run(self) -> None:
|
|
from common import resolve_platform_cores
|
|
|
|
profiles = {"emu": {"emulator": "Emu", "type": "standalone", "cores": ["emu"],
|
|
"systems": ["sys"], "files": []}}
|
|
config = {"cores": ["other"], "standalone_cores": ["emu"], "systems": {}}
|
|
self.assertEqual(resolve_platform_cores(config, profiles), {"emu"})
|
|
|
|
|
|
BIOS_SETTING = {
|
|
"emulator": "ps2emu", "build": "standalone", "kind": "passes_path", "names": [],
|
|
"search_dir": "/userdata/bios/ps2",
|
|
"passes_to": "inis/PCSX2.ini [Folders] Bios = ../../../bios/ps2, relative to the data root",
|
|
"source_ref": "ps2emuGenerator.py:219",
|
|
}
|
|
# The same folder through a scans_dir record, and a record setting the image
|
|
# name alone, which hands no folder.
|
|
BIOS_SCAN = {**BIOS_SETTING, "kind": "scans_dir", "passes_to": "PCSX2.ini [Folders] Bios"}
|
|
BIOS_NAME = {**BIOS_SETTING, "passes_to": "PCSX2.ini [Filenames] BIOS"}
|
|
|
|
|
|
class ASettingSetsTheBiosFolder(unittest.TestCase):
|
|
"""KNULLI sets AetherSX2's [Folders] Bios to bios/ps2.
|
|
|
|
The profile reads its BIOS images by content from bios/ under its data
|
|
root, and names that setting in standalone_directories. Every image the
|
|
scan finds goes to ps2/ in the pack, the manifest and verify, and a file
|
|
the layout keeps outside its bios/ folder is named, never packed.
|
|
"""
|
|
|
|
def setUp(self) -> None:
|
|
_emulator_profiles_cache.clear()
|
|
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
|
|
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
|
self.addCleanup(shutil.rmtree, self.root, True)
|
|
files, by_md5, by_name = {}, {}, {}
|
|
for folder, name, data in (
|
|
("Sony/PlayStation 2", "img-a.bin", b"PS2 IMAGE A....."),
|
|
("Sony/PlayStation 2", "img-b.bin", b"PS2 IMAGE B....."),
|
|
("Other/ps2emu/arcade", "boot.zip", b"BOOT PROGRAMS"),
|
|
):
|
|
where = self.root / "bios" / folder
|
|
where.mkdir(parents=True, exist_ok=True)
|
|
(where / name).write_bytes(data)
|
|
sha1 = hashlib.sha1(data).hexdigest()
|
|
md5 = hashlib.md5(data).hexdigest()
|
|
files[sha1] = {"path": str(where / name), "name": name, "sha1": sha1,
|
|
"md5": md5, "crc32": "", "size": len(data)}
|
|
by_md5[md5] = sha1
|
|
by_name[name] = [sha1]
|
|
self.db = {
|
|
"files": files,
|
|
"indexes": {"by_md5": by_md5, "by_name": by_name, "by_crc32": {},
|
|
"by_path_suffix": {}},
|
|
}
|
|
self.platforms = self.root / "platforms"
|
|
self.emulators = self.root / "emulators"
|
|
self.platforms.mkdir()
|
|
self.emulators.mkdir()
|
|
platforms = {"setplat": [BIOS_SETTING], "scanplat": [BIOS_SCAN],
|
|
"nameplat": [BIOS_NAME], "plainplat": []}
|
|
for name, launchers in platforms.items():
|
|
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
|
|
"platform": name, "version": "1", "verification_mode": "existence",
|
|
"base_destination": "bios", "cores": ["ps2emu"],
|
|
"standalone_cores": ["ps2emu"], "launchers": launchers,
|
|
"systems": {"sony-playstation-2": {"files": []}},
|
|
}))
|
|
(self.platforms / "_registry.yml").write_text(yaml.safe_dump({"platforms": {
|
|
name: {"config": f"{name}.yml"} for name in platforms
|
|
}}))
|
|
(self.emulators / "ps2emu.yml").write_text(yaml.safe_dump({
|
|
"emulator": "PS2Emu", "type": "standalone", "bios_mode": "agnostic",
|
|
"cores": ["ps2emu"], "systems": ["sony-playstation-2"],
|
|
"bios_directory": "bios/",
|
|
"standalone_directories": ["[Folders] Bios = bios"],
|
|
"files": [
|
|
{"name": "img-a.bin", "path": "bios/img-a.bin", "required": True,
|
|
"min_size": 16, "max_size": 16, "validation": ["size"]},
|
|
# Read below the data root, never below its bios/ folder.
|
|
{"name": "boot.zip", "standalone_path": "arcade/boot.zip",
|
|
"mode": "standalone", "required": False},
|
|
],
|
|
}))
|
|
|
|
def pack_names(self, platform: str) -> list[str]:
|
|
with redirect_stdout(io.StringIO()):
|
|
zip_path = generate_pack(
|
|
platform, str(self.platforms), self.db, str(self.root / "bios"),
|
|
str(self.root / "out" / platform), emulators_dir=str(self.emulators),
|
|
offline=True,
|
|
)
|
|
with zipfile.ZipFile(zip_path) as zf:
|
|
return sorted(n for n in zf.namelist() if n.endswith((".bin", ".zip")))
|
|
|
|
def manifest(self, platform: str) -> dict:
|
|
with redirect_stdout(io.StringIO()):
|
|
return generate_manifest(
|
|
platform, str(self.platforms), self.db, str(self.root / "bios"),
|
|
str(self.platforms / "_registry.yml"),
|
|
emulators_dir=str(self.emulators), offline=True,
|
|
)
|
|
|
|
def verify_dests(self, platform: str) -> list[str]:
|
|
config = load_platform_config(platform, str(self.platforms))
|
|
result = verify_platform(config, self.db, str(self.emulators))
|
|
return sorted(d for d in result["pack_destinations"] if d.endswith((".bin", ".zip")))
|
|
|
|
def assert_placed(self, platform: str, expected: list[str]) -> None:
|
|
self.assertEqual(self.pack_names(platform), expected)
|
|
self.assertEqual(
|
|
sorted(f["dest"] for f in self.manifest(platform)["files"]), expected
|
|
)
|
|
self.assertEqual(self.verify_dests(platform), expected)
|
|
|
|
def test_every_scanned_image_lands_in_the_folder_set(self) -> None:
|
|
self.assert_placed("setplat", ["ps2/img-a.bin", "ps2/img-b.bin"])
|
|
|
|
def test_a_scans_dir_record_sets_it_too(self) -> None:
|
|
self.assert_placed("scanplat", ["ps2/img-a.bin", "ps2/img-b.bin"])
|
|
|
|
def test_the_image_name_setting_hands_no_folder(self) -> None:
|
|
self.assert_placed("nameplat", ["img-a.bin", "img-b.bin"])
|
|
|
|
def test_a_platform_without_the_record_keeps_the_layout(self) -> None:
|
|
self.assert_placed("plainplat", ["img-a.bin", "img-b.bin"])
|
|
|
|
def test_a_file_outside_the_bios_folder_is_named(self) -> None:
|
|
omitted = [o for o in self.manifest("setplat")["omitted_files"]
|
|
if o["name"] == "boot.zip"]
|
|
self.assertEqual(
|
|
[(o["dest"], o["root"], o["reason"], o["cores"]) for o in omitted],
|
|
[("arcade/boot.zip", "emulator", "outside_bios_folder", ["ps2emu"])],
|
|
)
|
|
|
|
def test_the_ini_key_carries_its_section(self) -> None:
|
|
from launchers import ini_keys
|
|
|
|
self.assertEqual(
|
|
ini_keys("PCSX2.ini [Folders] Bios = /storage/roms/bios/armsx2, [Filenames] BIOS"),
|
|
{"[Folders] Bios", "[Filenames] BIOS"},
|
|
)
|
|
self.assertEqual(ini_keys("command line -rompath"), frozenset())
|
|
|
|
|
|
class AetherSX2OnKnulli(unittest.TestCase):
|
|
"""The real profile against KNULLI's aethersx2Generator.py:219 record."""
|
|
|
|
def test_the_bios_images_land_in_ps2(self) -> None:
|
|
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["aethersx2"]
|
|
config = load_platform_config("knulli", str(REPO_ROOT / "platforms"))
|
|
(view, standalone), = platform_layouts("aethersx2", profile, standalone_set(config))
|
|
self.assertTrue(standalone)
|
|
for entry in view["files"]:
|
|
path = entry.get("path") or ""
|
|
if path.startswith("bios/"):
|
|
self.assertEqual(entry["standalone_path"], "ps2/" + path.removeprefix("bios/"))
|
|
elif path:
|
|
self.assertEqual(entry.get("unplaced"), "outside_bios_folder", path)
|
|
|
|
|
|
class Quasi88OnRetroPie(unittest.TestCase):
|
|
"""RetroPie writes -romdir $biosdir/pc88 into quasi88.ini (quasi88.sh:67)."""
|
|
|
|
def test_the_roms_land_in_pc88(self) -> None:
|
|
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["quasi88kai"]
|
|
config = load_platform_config("retropie", str(REPO_ROOT / "platforms"))
|
|
(view, standalone), = platform_layouts("quasi88kai", profile, standalone_set(config))
|
|
self.assertTrue(standalone)
|
|
paths = [e["path"] for e in view["files"]]
|
|
self.assertTrue(paths and all(p.startswith("rom/") for p in paths))
|
|
self.assertEqual(
|
|
[e["standalone_path"] for e in view["files"]],
|
|
["pc88/" + p.removeprefix("rom/") for p in paths],
|
|
)
|
|
|
|
|
|
class FlycastOnEmuELEC(unittest.TestCase):
|
|
"""EmuELEC links /storage/.local/share/flycast to bios/dc (flycast.sh:11-14),
|
|
and root's home is /storage (busybox profile.d/98-busybox.conf:4)."""
|
|
|
|
def test_the_home_directory_is_spelled_as_home(self) -> None:
|
|
from launchers import normalize_handle
|
|
|
|
self.assertEqual(
|
|
normalize_handle("/storage/.local/share/flycast", "/storage"), "~/.local/share/flycast"
|
|
)
|
|
self.assertEqual(normalize_handle("/storage", "/storage/"), "~")
|
|
self.assertEqual(normalize_handle("/storagex/a", "/storage"), "/storagex/a")
|
|
self.assertEqual(
|
|
normalize_handle("/storage/.local/share/flycast"), "/storage/.local/share/flycast"
|
|
)
|
|
|
|
def test_the_bios_lands_in_dc(self) -> None:
|
|
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["flycast"]
|
|
config = load_platform_config("emuelec", str(REPO_ROOT / "platforms"))
|
|
layouts = platform_layouts("flycast", profile, standalone_set(config))
|
|
(view,) = [v for v, standalone in layouts if standalone]
|
|
self.assertTrue(view["files"])
|
|
for entry in view["files"]:
|
|
self.assertEqual(entry["standalone_path"], "dc/" + entry["layout_path"])
|
|
|
|
|
|
def standalone_view(emulator: str, platform: str) -> dict:
|
|
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))[emulator]
|
|
config = load_platform_config(platform, str(REPO_ROOT / "platforms"))
|
|
(view,) = [v for v, standalone in platform_layouts(emulator, profile, standalone_set(config))
|
|
if standalone]
|
|
return view
|
|
|
|
|
|
class Eka2l1OnRetroBat(unittest.TestCase):
|
|
"""RetroBat writes data-storage = bios/eka2l1/data (Eka2l1.Generator.cs:153, 156)."""
|
|
|
|
def test_the_device_rom_lands_in_eka2l1_data(self) -> None:
|
|
view = standalone_view("eka2l1", "retrobat")
|
|
(rom,) = [e for e in view["files"] if e["name"] == "SYM.ROM"]
|
|
self.assertEqual(rom["standalone_path"], "eka2l1/data/roms/nem-4/SYM.ROM")
|
|
# Shipped beside the executable, in RetroBat's emulator folder.
|
|
for entry in view["files"]:
|
|
if entry.get("mode") == "standalone":
|
|
self.assertEqual(entry.get("unplaced"), "outside_bios_folder", entry["name"])
|
|
|
|
|
|
class CemuOnRetroDECK(unittest.TestCase):
|
|
"""RetroDECK sets mlc_path to $bios_path/cemu (cemu/component_prepare.sh:14, 29)
|
|
and keeps Cemu's user data in its Flatpak's $XDG_DATA_HOME/Cemu."""
|
|
|
|
def test_the_mlc_lands_in_cemu(self) -> None:
|
|
view = standalone_view("cemu", "retrodeck")
|
|
mlc = [e for e in view["files"] if (e.get("layout_path") or "").startswith("mlc01/")]
|
|
self.assertTrue(mlc)
|
|
for entry in mlc:
|
|
self.assertEqual(
|
|
entry["standalone_path"], "cemu/" + entry["layout_path"].removeprefix("mlc01/")
|
|
)
|
|
(otp,) = [e for e in view["files"] if e["name"] == "otp.bin"]
|
|
self.assertEqual(otp.get("unplaced"), "outside_bios_folder")
|
|
|
|
|
|
class DolphinUserDirectory(unittest.TestCase):
|
|
"""EmuELEC runs Dolphin with XDG_DATA_HOME=/storage/roms/dolphin (dolphin.sh:28),
|
|
RetroDECK links $XDG_DATA_HOME/dolphin-emu/Triforce to bios/Triforce
|
|
(dolphin/component_prepare.sh:30, 49)."""
|
|
|
|
def test_emuelec_installs_the_user_directory_below_its_roms(self) -> None:
|
|
view = standalone_view("dolphin", "emuelec")
|
|
placed = {e["name"]: e["standalone_path"] for e in view["files"] if e.get("layout_path")}
|
|
self.assertEqual(placed["segaboot.gcm"], "dolphin/dolphin-emu/Triforce/segaboot.gcm")
|
|
# A standalone-only entry's path is its place in the User directory.
|
|
self.assertEqual(
|
|
placed["rtl8723a_fw.bin"], "dolphin/dolphin-emu/Load/Firmware/rtl_bt/rtl8723a_fw.bin"
|
|
)
|
|
self.assertTrue(all(e.get("unprefixed") for e in view["files"] if e.get("layout_path")))
|
|
|
|
def test_retrodeck_places_triforce_alone(self) -> None:
|
|
view = standalone_view("dolphin", "retrodeck")
|
|
placed = {e["name"]: e["standalone_path"] for e in view["files"] if e.get("layout_path")}
|
|
self.assertEqual(placed, {"segaboot.gcm": "Triforce/segaboot.gcm"})
|
|
(ipl, *_) = [e for e in view["files"] if e["name"] == "IPL.bin"]
|
|
self.assertEqual(ipl.get("unplaced"), "outside_bios_folder")
|
|
|
|
|
|
class MameSearchPaths(unittest.TestCase):
|
|
"""MAME reads each semicolon-separated search path in turn
|
|
(src/emu/fileio.cpp:53-58, 330-392)."""
|
|
|
|
def sets(self, view: dict) -> list[dict]:
|
|
return [e for e in view["files"] if e.get("category") == "bios_zip"]
|
|
|
|
def test_retrobat_keeps_the_sets_at_the_bios_root(self) -> None:
|
|
# -rp bios;<rom directory> (Mame64.Generator.cs:93-96).
|
|
view = standalone_view("mame", "retrobat")
|
|
sets = self.sets(view)
|
|
self.assertGreater(len(sets), 1000)
|
|
for entry in sets:
|
|
self.assertEqual(entry["standalone_path"], entry["name"])
|
|
self.assertNotIn("unplaced", entry)
|
|
(bbc,) = [e for e in view["files"] if e["name"] == "bbc.zip"]
|
|
# -sp bios/mame/samples, hbmame's folder being another record
|
|
# (Mame64.Generator.cs:30, 101, 106-107).
|
|
self.assertEqual(bbc["standalone_path"], "mame/samples/bbc.zip")
|
|
|
|
def test_knulli_keeps_the_root_listed_after_bios_mame(self) -> None:
|
|
# -rompath <romDirname>;/userdata/bios/mame;/userdata/bios (mameGenerator.py:116-122).
|
|
view = standalone_view("mame", "knulli")
|
|
self.assertTrue(all(e["standalone_path"] == e["name"] for e in self.sets(view)))
|
|
(bbc,) = [e for e in view["files"] if e["name"] == "bbc.zip"]
|
|
self.assertEqual(bbc["standalone_path"], "mame/samples/bbc.zip")
|
|
|
|
def test_retropie_moves_the_sets_to_bios_mame(self) -> None:
|
|
# rompath $romdir/mame;$romdir/arcade;$biosdir/mame (mame.sh:133).
|
|
view = standalone_view("mame", "retropie")
|
|
self.assertTrue(all(
|
|
e["standalone_path"] == "mame/" + e["name"] for e in self.sets(view)
|
|
))
|
|
|
|
def test_retrodeck_reads_samples_in_mame_sa(self) -> None:
|
|
# samplepath $bios_path/mame-sa/samples (mame/component_prepare.sh:94, 117).
|
|
view = standalone_view("mame", "retrodeck")
|
|
(bbc,) = [e for e in view["files"] if e["name"] == "bbc.zip"]
|
|
self.assertEqual(bbc["standalone_path"], "mame-sa/samples/bbc.zip")
|
|
# ES-DE hands -rompath %GAMEDIR%;%ROMPATH%/<system>: no BIOS folder.
|
|
self.assertTrue(all(
|
|
e.get("unplaced") == "outside_bios_folder" for e in self.sets(view)
|
|
))
|
|
|
|
def test_a_passed_search_setting_leaves_the_bundled_tree_unread(self) -> None:
|
|
# RetroBat passes -hash bios/mame/hash and -sp bios/mame/samples
|
|
# (Mame64.Generator.cs:30, 101, 106-107, 172, 177-178), RetroDECK
|
|
# writes samplepath $bios_path/mame-sa/samples (component_prepare.sh:94):
|
|
# the trees beside the executable are not read, the pack brings them.
|
|
for platform, tree, place in (
|
|
("retrobat", "hash/", "mame/hash/"),
|
|
("retrobat", "floppy/", "mame/samples/floppy/"),
|
|
("knulli", "floppy/", "mame/samples/floppy/"),
|
|
("retrodeck", "floppy/", "mame-sa/samples/floppy/"),
|
|
):
|
|
view = standalone_view("mame", platform)
|
|
(entry,) = [e for e in view["files"]
|
|
if e["name"] == tree and e.get("mode") == "standalone"]
|
|
self.assertEqual(entry["standalone_path"], place, platform)
|
|
self.assertNotIn("bundled", entry, platform)
|
|
# Where no launcher passes the setting, the bundled copy is read.
|
|
view = standalone_view("mame", "emudeck")
|
|
(entry,) = [e for e in view["files"]
|
|
if e["name"] == "hash/" and e.get("mode") == "standalone"]
|
|
self.assertTrue(entry.get("bundled"))
|
|
|
|
def test_retropie_reads_samples_below_its_roms(self) -> None:
|
|
# samplepath $romdir/mame/samples;$romdir/arcade/samples (mame.sh:135).
|
|
view = standalone_view("mame", "retropie")
|
|
(bbc,) = [e for e in view["files"] if e["name"] == "bbc.zip"]
|
|
self.assertEqual((bbc["standalone_path"], bbc.get("root")),
|
|
("mame/samples/bbc.zip", "roms"))
|
|
(tree,) = [e for e in view["files"]
|
|
if e["name"] == "floppy/" and e.get("mode") == "standalone"]
|
|
self.assertEqual((tree["standalone_path"], tree.get("root")),
|
|
("mame/samples/floppy/", "roms"))
|
|
self.assertNotIn("bundled", tree)
|
|
|
|
def test_batocera_keeps_the_sets_at_the_root_and_samples_in_bios_mame(self) -> None:
|
|
# -rompath <romDirname>;/userdata/bios/mame;/userdata/bios and
|
|
# -samplepath /userdata/bios/mame/samples (mameGenerator.py:129-142).
|
|
view = standalone_view("mame", "batocera")
|
|
self.assertTrue(all(e["standalone_path"] == e["name"] for e in self.sets(view)))
|
|
(bbc,) = [e for e in view["files"] if e["name"] == "bbc.zip"]
|
|
self.assertEqual(bbc["standalone_path"], "mame/samples/bbc.zip")
|
|
|
|
def test_a_moved_tree_is_found_by_its_layout_path(self) -> None:
|
|
from common import expand_directory_entries, load_database
|
|
|
|
db = load_database(str(REPO_ROOT / "database.json"))
|
|
view = standalone_view("mame", "retrodeck")
|
|
(tree,) = [e for e in view["files"]
|
|
if e["name"] == "floppy/" and e.get("mode") == "standalone"]
|
|
members = expand_directory_entries([tree], db, standalone=True)
|
|
self.assertEqual(len(members), 19)
|
|
for member in members:
|
|
self.assertTrue(member["path"].startswith("mame-sa/samples/floppy/"))
|
|
|
|
def test_psxmame_reads_bios_psxmame_bios(self) -> None:
|
|
# -rompath bios/psxmame/bios;<rom directory> (PSXMame.Generator.cs:38-48).
|
|
view = standalone_view("psxmame", "retrobat")
|
|
self.assertTrue(view["files"])
|
|
for entry in view["files"]:
|
|
self.assertEqual(entry["standalone_path"], "psxmame/bios/" + entry["name"])
|
|
|
|
|
|
class LibretroMameSetsInMameBios(unittest.TestCase):
|
|
"""libretro/mame searches the content folder, then <system>/mame/bios and
|
|
<system>/mame/roms, never the system folder itself
|
|
(src/osd/libretro/libretro-internal/retro_init.cpp:580-597)."""
|
|
|
|
def test_every_libretro_mame_set_is_read_in_mame_bios(self) -> None:
|
|
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
|
for key in ("mame", "mame0258", "mame0278", "mamearcade", "mamemess"):
|
|
sets = [f for f in profiles[key]["files"] if f.get("category") == "bios_zip"
|
|
and f.get("mode") != "standalone" and f["name"] != "bbc.zip"]
|
|
self.assertTrue(sets, key)
|
|
for entry in sets:
|
|
self.assertEqual(entry.get("path"), "mame/bios/" + entry["name"], key)
|
|
|
|
def test_the_standalone_build_keeps_its_rompath_layout(self) -> None:
|
|
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["mame"]
|
|
for entry in profile["files"]:
|
|
if str(entry.get("path", "")).startswith("mame/bios/"):
|
|
self.assertEqual(entry.get("standalone_path"), entry["name"])
|
|
|
|
def test_retrobat_lays_out_both_builds(self) -> None:
|
|
# es_systems.cfg offers the libretro core mame and the standalone
|
|
# mame64, two names of one profile.
|
|
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["mame"]
|
|
config = load_platform_config("retrobat", str(REPO_ROOT / "platforms"))
|
|
builds = sorted(s for _v, s in platform_layouts("mame", profile, standalone_set(config)))
|
|
self.assertEqual(builds, [False, True])
|
|
|
|
|
|
class GzdoomOnRetroDECK(unittest.TestCase):
|
|
"""RetroDECK writes RETRODECKROMSDIR/doom into [IWADSearch.Directories]
|
|
(gzdoom.ini:9, component_prepare.sh:27)."""
|
|
|
|
def test_the_iwads_land_in_roms_doom(self) -> None:
|
|
view = standalone_view("gzdoom", "retrodeck")
|
|
iwads = [e for e in view["files"] if e.get("system")]
|
|
self.assertEqual(len(iwads), 43)
|
|
for entry in iwads:
|
|
self.assertEqual(entry["standalone_path"], "roms/doom/" + entry["name"])
|
|
self.assertTrue(entry.get("unprefixed"))
|
|
# The engine pk3s are read beside the executable first.
|
|
(pk3,) = [e for e in view["files"] if e["name"] == "gzdoom.pk3"]
|
|
self.assertNotIn("standalone_path", pk3)
|
|
|
|
def test_another_platform_keeps_them(self) -> None:
|
|
view = standalone_view("gzdoom", "batocera")
|
|
self.assertFalse(any(e.get("standalone_path") for e in view["files"]))
|
|
|
|
|
|
class AdvanceMameOnEmuELEC(unittest.TestCase):
|
|
"""advmame.rc sets dir_rom /storage/roms/mame, /storage/roms/arcade for
|
|
the arcade system (advmame.rc:11, advmame.sh:26-30)."""
|
|
|
|
def test_the_sets_land_in_roms_mame_and_roms_arcade(self) -> None:
|
|
# Started for arcade it reads roms/arcade, for mame and cps1-3
|
|
# roms/mame (es_systems.json, advmame.sh:26-30): an arcade set is
|
|
# read in both.
|
|
view = standalone_view("advancemame", "emuelec")
|
|
sets = [e for e in view["files"] if e.get("category") == "bios_zip"]
|
|
self.assertEqual(len(sets), 58)
|
|
places: dict[str, set[str]] = {}
|
|
for entry in sets:
|
|
self.assertTrue(entry.get("unprefixed"))
|
|
places.setdefault(entry["name"], set()).add(entry["standalone_path"])
|
|
self.assertEqual(len(places), 29)
|
|
for name, paths in places.items():
|
|
self.assertEqual(paths, {"mame/" + name, "arcade/" + name})
|
|
|
|
def test_both_copies_are_one_file(self) -> None:
|
|
# Outside the BIOS tree the folder is no proof of content: each copy
|
|
# resolves by its layout path, so both folders hold the same file.
|
|
from common import load_database
|
|
|
|
db = load_database(str(REPO_ROOT / "database.json"))
|
|
with redirect_stdout(io.StringIO()):
|
|
manifest = generate_manifest(
|
|
"emuelec", str(REPO_ROOT / "platforms"), db, str(REPO_ROOT / "bios"),
|
|
str(REPO_ROOT / "platforms" / "_registry.yml"),
|
|
emulators_dir=str(REPO_ROOT / "emulators"), offline=True,
|
|
)
|
|
by_dest = {f["dest"]: f["sha1"] for f in manifest["files"]}
|
|
sets = [d.removeprefix("arcade/") for d in by_dest if d.startswith("arcade/")]
|
|
self.assertEqual(len(sets), 29)
|
|
for name in sets:
|
|
self.assertEqual(by_dest["arcade/" + name], by_dest["mame/" + name], name)
|
|
|
|
def test_the_roms_folder_is_no_proof_of_content(self) -> None:
|
|
# bios/Arcade/MAME/konamigv.zip is MAME 0.289's set, not AdvanceMAME's.
|
|
from common import load_database, resolve_local_file
|
|
|
|
db = load_database(str(REPO_ROOT / "database.json"))
|
|
view = standalone_view("advancemame", "emuelec")
|
|
entry = next(e for e in view["files"]
|
|
if e["name"] == "konamigv.zip" and e["standalone_path"].startswith("mame/"))
|
|
moved, _status = resolve_local_file(entry, db, dest_hint=entry["standalone_path"])
|
|
plain = {k: v for k, v in entry.items()
|
|
if k not in ("layout_path", "standalone_path", "unprefixed")}
|
|
root, _status = resolve_local_file(plain, db, dest_hint=entry["name"])
|
|
self.assertEqual(moved, root)
|
|
self.assertNotIn("/MAME/", moved)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|