Files
libretro/tests/test_launcher_placement.py
T
Abdessamad Derraz ffb3cbfed3 fix: place Xbox files where RetroBat reads them
Each Cxbx build reads its data from the folder its settings.ini sits
in, which RetroBat writes beside the binary: the three profiles name
Settings::GetDataLocation, so keys.bin, EmuDisk and EmuMediaBoard go to
emulators/<folder> on RetroBat instead of the BIOS root. RetroBat writes
cxbx-reloaded's EEPROM.bin and xemu's eeprom.bin and xbox_hdd.qcow2
itself (installed_on), and mounts an .iso with its own tool, so Cxbx's
dokan2.dll is never loaded there (unread_on).
2026-11-10 02:36:02 +01:00

556 lines
27 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.
self.assertFalse(at_root & {"eeprom.bin", "xbox_hdd.qcow2", "dokan2.dll", "keys.bin"})
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"})
if __name__ == "__main__":
unittest.main()