Files
libretro/tests/test_standalone_links.py
T
Abdessamad Derraz bc386e0561 feat: profile amiberry 8.0.0 built by EmuELEC
EmuELEC 4.8 builds amiberry 452783a (CMakeLists.txt declares 8.0.0).
Same paths as 7.1.1, a ROM database with the PPC flash images, which
the scan hashes with their config bytes cleared, so no file hash is
declared for them. Tests check that ROCKNIX, AmberELEC and EmuELEC
resolve their own build, the other platforms the 8.5.0 profile, and
that ROCKNIX places its kickstarts at the BIOS root.
2026-11-09 20:58:25 +01:00

700 lines
31 KiB
Python

"""A standalone build's files land in the folder its platform hands it.
``standalone_path`` is relative to the directories the standalone build
resolves it against (``standalone_directories``). RetroPie links Flycast's
~/.local/share/flycast to bios/dc and writes Dreamcast.BiosPath, ROCKNIX
patches Flycast's data directory to /storage/roms/dreamcast/data and links
that to bios/dc, EmuDeck links the Flatpak's data directory to
bios/flycast/bios, Batocera sets FLYCAST_BIOS_PATH and RetroBat
Dreamcast.BiosPath to bios/dc. Laid out at the BIOS root instead, Flycast's
naomi.zip took the place of MAME's.
"""
from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import sys
import textwrap
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from common import ( # noqa: E402
entry_launch_systems,
load_emulator_profiles,
load_platform_config,
platform_layouts,
standalone_bindings_for,
standalone_destination,
standalone_set,
system_file_index,
)
from launchers import ( # noqa: E402
bound_path,
handed_directories,
normalize_handle,
standalone_bindings,
)
def link(emulator: str, search_dir: str, passes_to: str, **fields: object) -> dict:
return {
"emulator": emulator, "build": "standalone", "kind": "links", "names": [],
"search_dir": search_dir, "passes_to": passes_to, "source_ref": "x.sh:1",
**fields,
}
def passes(emulator: str, search_dir: str, passes_to: str) -> dict:
return {**link(emulator, search_dir, passes_to), "kind": "passes_path"}
PROFILE = {
"emulator": "Emu",
"type": "standalone + libretro",
"cores": ["emu_core"],
"standalone_directories": ["$XDG_DATA_HOME/emu", "$HOME/.local/share/emu", "find_user_data_dir"],
"files": [
{"name": "boot.bin", "path": "dc/boot.bin", "standalone_path": "boot.bin"},
{"name": "set.zip", "path": "dc/set.zip", "standalone_path": "set.zip"},
{"name": "shared.bin", "path": "dc/shared.bin"},
],
}
def config(*launchers: dict, base: str = "bios") -> dict:
return {"base_destination": base, "standalone_cores": ["emu"], "launchers": list(launchers)}
def standalone_view(cfg: dict) -> dict:
views = platform_layouts("emu", PROFILE, standalone_set(cfg))
return next(view for view, standalone in views if standalone)
class HandleSpelling(unittest.TestCase):
def test_home_and_flatpak_spellings_are_one_directory(self) -> None:
self.assertEqual(normalize_handle("$home/.local/share/emu"), "~/.local/share/emu")
self.assertEqual(normalize_handle("${HOME}/.local/share/emu/"), "~/.local/share/emu")
self.assertEqual(
normalize_handle("${HOME}/.var/app/org.emu.Emu/data/emu/"), "$XDG_DATA_HOME/emu"
)
self.assertEqual(normalize_handle("find_user_data_dir()"), "find_user_data_dir")
class HandedDirectories(unittest.TestCase):
def test_retropie_variable_resolves_to_a_bios_folder(self) -> None:
found = handed_directories(config(
link("emu", "$biosdir/dc", "$home/.local/share/emu is a symlink to it"), base="BIOS",
))
self.assertEqual([h.folder for h in found], ["dc"])
self.assertIn("~/.local/share/emu", found[0].handles)
def test_named_and_slotted_links_hand_no_directory(self) -> None:
found = handed_directories(config(
{**link("emu", "$biosdir", "$md_inst/<name>"), "names": ["boot.bin"]},
{**link("emu", "$bios_path/gc/EU", "$XDG_DATA_HOME/emu/GC/EUR"), "slot": "EUR"},
))
self.assertEqual(found, [])
def test_directory_outside_the_tree_has_no_folder(self) -> None:
found = handed_directories(config(
passes("emu", "/storage/roms/dreamcast/data", "find_user_data_dir() returns it"),
))
self.assertIsNone(found[0].folder)
class Root(unittest.TestCase):
def root(self, *launchers: dict, base: str = "bios") -> str | None:
found = self.bindings(*launchers, base=base).get("")
return found[1] if found else None
def bindings(self, *launchers: dict, base: str = "bios") -> dict[str, tuple[str, str]]:
return standalone_bindings(
{"emu"}, PROFILE["standalone_directories"], handed_directories(config(*launchers, base=base))
)
def test_link_of_the_data_directory_gives_its_folder(self) -> None:
self.assertEqual(
self.root(link("emu", "$biosdir/dc", "$home/.local/share/emu is a symlink to it")),
"dc",
)
def test_flatpak_data_directory(self) -> None:
self.assertEqual(
self.root(link(
"emu", "${biosPath}/emu/bios",
"symlink ${biosPath}/emu/bios -> ${HOME}/.var/app/org.emu.Emu/data/emu/",
)),
"emu/bios",
)
def test_patched_data_directory_followed_through_its_link(self) -> None:
self.assertEqual(
self.root(
passes("emu", "/storage/roms/dreamcast/data", "find_user_data_dir() returns it"),
link("emu", "/storage/roms/bios/dc",
"symlink /storage/roms/dreamcast/data -> /storage/roms/bios/dc"),
),
"dc",
)
def test_link_of_a_subfolder_binds_that_subfolder_only(self) -> None:
# A subfolder of the emulator's tree (Dolphin's Triforce) is not the
# directory standalone_path is relative to, only the files below it move.
found = self.bindings(
link("emu", "$bios_path/Triforce", "$XDG_DATA_HOME/emu/Triforce is a symlink to it"),
)
self.assertEqual(found, {"Triforce": ("", "Triforce")})
self.assertEqual(bound_path("Triforce/segaboot.gcm", found), ("", "Triforce/segaboot.gcm"))
self.assertIsNone(bound_path("boot.bin", found))
def test_two_folders_give_nothing(self) -> None:
self.assertIsNone(self.root(
link("emu", "$biosdir/dc", "$home/.local/share/emu is a symlink to it"),
link("emu", "$biosdir/other", "$XDG_DATA_HOME/emu is a symlink to it"),
))
def bind(cfg: dict, declared: list[str], pools: dict[str, str] | None = None,
systems: tuple[str, ...] = ()) -> dict[str, tuple[str, str]]:
return standalone_bindings(
{"emu"}, declared, handed_directories(cfg), pools=pools, systems=systems
)
class SearchLists(unittest.TestCase):
"""A setting read as a list of directories, every one searched."""
POOLS = {"filepool": "share/systemroms"}
def filepool(self, folder: str, position: int) -> dict:
return passes(
"emu", f"/userdata/bios/{folder}",
f"written: filepool add -path /userdata/bios/{folder} -types system_rom -position {position}",
)
def test_two_pool_entries_give_the_first_one(self) -> None:
# Batocera adds bios/Machines and bios/openmsx to openMSX's filepool.
cfg = config(self.filepool("Machines", 1), self.filepool("openmsx", 2))
self.assertEqual(
bind(cfg, ["~/.openMSX"], self.POOLS), {"share/systemroms": ("", "Machines")}
)
def test_the_entry_the_platform_list_fills_comes_first(self) -> None:
cfg = config(self.filepool("Machines", 1), self.filepool("openmsx", 2))
cfg["systems"] = {"msx": {"files": [{"name": "a.rom", "destination": "openmsx/a.rom"}]}}
self.assertEqual(
bind(cfg, ["~/.openMSX"], self.POOLS), {"share/systemroms": ("", "openmsx")}
)
def test_without_the_pool_two_folders_give_nothing(self) -> None:
cfg = config(self.filepool("Machines", 1), self.filepool("openmsx", 2))
self.assertEqual(bind(cfg, ["~/.openMSX", "filepool"]), {})
def test_a_colon_list_gives_each_directory_in_order(self) -> None:
# RetroDECK writes XRoar's rompath as four directories.
found = handed_directories(config(passes(
"emu", "$roms_path/coco:$roms_path/dragon32:$bios_path", "xroar.conf rompath",
)))
self.assertEqual(
[h.directory for h in found], ["$roms_path/coco", "$roms_path/dragon32", "$bios_path"]
)
self.assertEqual(
bind(config(passes("emu", "$roms_path/coco:$bios_path/xroar", "xroar.conf rompath")),
[], {"rompath": ""}),
{"": ("", "xroar")},
)
def test_a_drive_letter_is_one_directory(self) -> None:
self.assertEqual(handed_directories(config(passes("emu", "C:/bios", "rompath"))), [])
class AboveTheDeclaredDirectory(unittest.TestCase):
def test_a_prefix_lands_the_declared_directory_where_its_link_is(self) -> None:
# RetroPie builds XRoar with --prefix=$md_inst and links
# $md_inst/share/xroar/roms to $biosdir.
cfg = config(
passes("emu", "$md_inst", "${prefix} of configure --prefix"),
link("emu", "$biosdir", "$md_inst/share/xroar/roms is a symlink to it"),
base="BIOS",
)
self.assertEqual(bind(cfg, ["${prefix}/share/xroar/roms"]), {"": ("", "")})
def test_a_folder_of_the_tree_takes_the_rest_of_the_path(self) -> None:
cfg = config(passes("emu", "$biosdir/emu", "EMU_HOME"), base="BIOS")
self.assertEqual(bind(cfg, ["EMU_HOME/share/roms"]), {"": ("", "emu/share/roms")})
class PerSystem(unittest.TestCase):
"""A record written for some systems replaces, there, the others."""
CFG = {
**config(
passes("emu", "bios/dragon", "command line -rompath"),
{**passes("emu", "bios/coco", "command line -rompath"), "systems": ["coco"]},
),
"systems": {"coco": {"files": [{"name": "x.zip", "native_system": "coco"}]}},
}
POOLS = {"-rompath": ""}
def test_the_system_record_wins_for_its_system(self) -> None:
self.assertEqual(bind(self.CFG, [], self.POOLS, ("coco",)), {"": ("", "coco")})
def test_another_system_keeps_the_generic_record(self) -> None:
self.assertEqual(bind(self.CFG, [], self.POOLS, ("dragon32",)), {"": ("", "dragon")})
self.assertEqual(bind(self.CFG, [], self.POOLS), {"": ("", "dragon")})
def test_native_names_are_read_as_our_ids(self) -> None:
cfg = {
**config({**passes("emu", "bios/trs80coco", "-rompath"), "systems": ["trs80coco"]}),
"systems": {"coco": {"native_id": "trs80coco", "files": []}},
}
self.assertEqual(
[sorted(h.systems) for h in handed_directories(cfg)], [["coco"]]
)
def test_a_system_placeholder_gives_one_directory_per_system(self) -> None:
# Recalbox passes -rompath os.path.join(BIOS, args.system).
cfg = {
**config(passes("emu", "/recalbox/share/bios/<system>", "command line -rompath")),
"systems": {
"coco": {"native_id": "trs80coco", "files": []},
"dragon64": {"native_id": "dragon", "files": []},
},
}
self.assertEqual(
[(h.folder, sorted(h.systems)) for h in handed_directories(cfg)],
[("dragon", ["dragon64"]), ("trs80coco", ["coco"])],
)
self.assertEqual(bind(cfg, [], self.POOLS | {"-rompath": ""}, ("coco",)),
{"": ("", "trs80coco")})
self.assertEqual(bind(cfg, [], {"-rompath": ""}, ("dragon32",)), {})
def test_an_entry_is_launched_for_the_system_that_declares_its_name(self) -> None:
cfg = {
**config({**passes("emu", "bios/coco", "-rompath"), "systems": ["coco"]}),
"systems": {"coco": {"files": [{"name": "coco3.rom", "native_system": "coco"}]}},
}
index = system_file_index(standalone_set(cfg))
self.assertEqual(
entry_launch_systems({"name": "COCO3.ROM", "system": "coco3"}, index),
frozenset({"coco3", "coco"}),
)
class SystemFamily(unittest.TestCase):
def test_an_entry_follows_the_launch_its_system_is_listed_under(self) -> None:
# Recalbox lists mc10.rom under trs80coco: alice.rom, of the same
# profile system, is read there too.
profile = {
"emulator": "Emu", "type": "standalone", "standalone_pools": {"-rompath": ""},
"files": [
{"name": "mc10.rom", "path": "mc10.rom", "system": "mc10"},
{"name": "alice.rom", "path": "alice.rom", "system": "mc10"},
],
}
cfg = {
**config(passes("emu", "/recalbox/share/bios/<system>", "-rompath")),
"systems": {"coco": {"native_id": "trs80coco",
"files": [{"name": "mc10.rom"}]}},
}
((view, _standalone),) = platform_layouts("emu", profile, standalone_set(cfg))
self.assertEqual(
[f.get("standalone_path") for f in view["files"]],
["trs80coco/mc10.rom", "trs80coco/alice.rom"],
)
class OfferedMachine(unittest.TestCase):
def test_an_entry_system_offered_as_a_machine_selects_that_launch(self) -> None:
profile = {
"emulator": "Emu", "type": "standalone", "standalone_pools": {"-rompath": ""},
"files": [
{"name": "coco3.rom", "path": "coco3.rom", "system": "coco3"},
{"name": "mc10.rom", "path": "mc10.rom", "system": "mc10"},
],
}
cfg = {
**config(
passes("emu", "bios/dragon", "-rompath"),
{**passes("emu", "bios/coco", "-rompath"), "systems": ["coco"],
"machines": ["coco", "coco3"]},
),
"systems": {"coco": {"files": [{"name": "x.zip", "native_system": "coco"}]}},
}
((view, _standalone),) = platform_layouts("emu", profile, standalone_set(cfg))
self.assertEqual(
[f.get("standalone_path") for f in view["files"]], ["coco/coco3.rom", "dragon/mc10.rom"]
)
class Layout(unittest.TestCase):
LINKED = link("emu", "$biosdir/dc", "$home/.local/share/emu is a symlink to it")
def test_standalone_view_takes_the_linked_folder(self) -> None:
view = standalone_view(config(self.LINKED, base="BIOS"))
self.assertEqual(
[f.get("standalone_path") for f in view["files"]], ["dc/boot.bin", "dc/set.zip", None]
)
self.assertEqual(view["files"][2]["path"], "dc/shared.bin")
def test_without_a_link_the_profile_layout_stands(self) -> None:
view = standalone_view(config())
self.assertEqual([f.get("standalone_path") for f in view["files"]][:2], ["boot.bin", "set.zip"])
def test_profile_is_not_changed(self) -> None:
standalone_view(config(self.LINKED, base="BIOS"))
self.assertEqual(PROFILE["files"][0]["standalone_path"], "boot.bin")
def test_libretro_view_keeps_path(self) -> None:
cfg = {**config(self.LINKED, base="BIOS"), "dual_mode_cores": ["emu"]}
views = platform_layouts("emu", PROFILE, standalone_set(cfg))
libretro = next(view for view, standalone in views if not standalone)
self.assertIs(libretro, PROFILE)
def test_dual_mode_gets_both_layouts(self) -> None:
# The libretro build still reads dc/, the standalone build the
# linked folder: neither loses a file to the other.
cfg = {**config(link("emu", "$biosdir/emu", "$home/.local/share/emu is a symlink to it"),
base="BIOS"), "dual_mode_cores": ["emu"]}
views = dict((standalone, view) for view, standalone in
platform_layouts("emu", PROFILE, standalone_set(cfg)))
self.assertEqual(
[f["path"] for f in views[False]["files"]], ["dc/boot.bin", "dc/set.zip", "dc/shared.bin"]
)
self.assertEqual(
[f.get("standalone_path") for f in views[True]["files"]], ["emu/boot.bin", "emu/set.zip", None]
)
def test_union_of_platforms_with_two_folders_keeps_the_profile_layout(self) -> None:
union = standalone_set(config(self.LINKED, base="BIOS")) | standalone_set(config(
link("emu", "${biosPath}/emu/bios", "symlink ${biosPath}/emu/bios -> ${HOME}/.local/share/emu"),
))
self.assertEqual(standalone_bindings_for("emu", PROFILE, union), {})
def test_destination_function(self) -> None:
entry = PROFILE["files"][0]
self.assertEqual(standalone_destination(entry, PROFILE, {"": ("", "dc")}), ("", "dc/boot.bin"))
self.assertIsNone(standalone_destination(entry, PROFILE, {}))
# The entry both builds share keeps path in a dual-build profile.
self.assertIsNone(standalone_destination(PROFILE["files"][2], PROFILE, {"": ("", "dc")}))
class FlycastOnEachPlatform(unittest.TestCase):
"""The real profile against the real launcher records."""
EXPECTED = {
"batocera": "dc",
"emudeck": "flycast/bios",
"retrobat": "dc",
"retropie": "dc",
"rocknix": "dc",
}
@classmethod
def setUpClass(cls) -> None:
cls.profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["flycast"]
def test_each_platform_hands_flycast_its_folder(self) -> None:
for platform, folder in self.EXPECTED.items():
with self.subTest(platform=platform):
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
self.assertEqual(
standalone_bindings_for("flycast", self.profile, standalone_set(cfg)),
{"": ("", folder)},
)
def test_no_standalone_file_lands_at_the_bios_root(self) -> None:
# Flycast's naomi.zip and awbios.zip share their names with MAME's
# root archives: at the root they replace them.
for platform, folder in self.EXPECTED.items():
with self.subTest(platform=platform):
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
for view, standalone in platform_layouts(
"flycast", self.profile, standalone_set(cfg)
):
if not standalone:
continue
for entry in view["files"]:
self.assertTrue(
entry["standalone_path"].startswith(folder + "/"),
entry["standalone_path"],
)
def test_emudeck_keeps_the_libretro_layout_beside_the_linked_one(self) -> None:
# EmuDeck's RetroArch_installCores downloads flycast_libretro, which
# reads system/dc/, next to the standalone Flycast in flycast/bios/.
cfg = load_platform_config("emudeck", str(REPO_ROOT / "platforms"))
self.assertIn("flycast", cfg.get("dual_mode_cores") or [])
views = {
standalone: view
for view, standalone in platform_layouts("flycast", self.profile, standalone_set(cfg))
}
self.assertEqual(set(views), {False, True})
for entry in views[False]["files"]:
self.assertTrue(entry["path"].startswith("dc/"), entry["path"])
for entry in views[True]["files"]:
self.assertTrue(entry["standalone_path"].startswith("flycast/bios/"))
class Scrapers(unittest.TestCase):
def test_emudeck_installed_libretro_cores(self) -> None:
from scraper.emudeck_scraper import installed_libretro_cores
script = textwrap.dedent("""\
RetroArch_installCores(){
\tRAcores=(
\t\t\t81_libretro \\
\t\t\tflycast_libretro \\
\t\t\tmesen-s_libretro \\
\t\t\t)
}
""")
self.assertEqual(installed_libretro_cores(script), ["81", "flycast", "mesen-s"])
with self.assertRaises(RuntimeError):
installed_libretro_cores("RetroArch_installCores(){\n}\n")
"""The records each platform's scraper writes for these directories."""
def test_rocknix_patched_data_directory(self) -> None:
from scraper import rocknix_launchers as rl
from scraper.shell_launchers import LauncherPatternMissing, RepoTree
path = f"{rl.STANDALONE}/flycast-sa/patches/000-conf-dir.patch"
patch = textwrap.dedent("""\
--- a/core/linux-dist/main.cpp
+++ b/core/linux-dist/main.cpp
@@ -69,12 +69,17 @@
static std::string find_user_data_dir()
{
+#ifdef __unix__
+\tflycast::mkdir("/storage/roms/dreamcast/data", 0755);
+\treturn "/storage/roms/dreamcast/data/";
+#else
""")
records = rl.patched_directories(RepoTree.from_files({path: patch}))
self.assertEqual(len(records), 1)
entry = records[0].entry()
self.assertEqual(entry["search_dir"], "/storage/roms/dreamcast/data")
self.assertEqual(entry["passes_to"], "find_user_data_dir() in core/linux-dist/main.cpp returns it")
self.assertEqual(entry["source_ref"], f"{path}:4, 8")
with self.assertRaises(LauncherPatternMissing):
rl.patched_directories(RepoTree.from_files({path: patch.replace("return", "x =")}))
def test_batocera_environment_directory(self) -> None:
from scraper.batocera_launchers import (
GENERATORS, IMPORTER, PATHS_MODULE, Configgen, environment_directories,
)
from scraper.configgen_launchers import LauncherShapeError
paths = "from pathlib import Path\nBIOS = Path('/userdata/bios')\nCONFIGS = Path('/userdata/system/configs')\n"
importer = "_GENERATOR_MAP = {}\n_LEGACY_GENERATOR_MAP = {}\n"
flycast_paths = "from ...batoceraPaths import BIOS\nFLYCAST_BIOS = BIOS / 'dc'\n"
generator = textwrap.dedent("""\
def generate(self):
return Command.Command(
array=[],
env={
"XDG_CONFIG_HOME": CONFIGS,
"FLYCAST_BIOS_PATH": FLYCAST_BIOS,
}
)
""")
tree = {
PATHS_MODULE: paths,
IMPORTER: importer,
f"{GENERATORS}/flycast/flycastPaths.py": flycast_paths,
f"{GENERATORS}/flycast/flycastGenerator.py": generator,
}
records = [r.entry() for r in environment_directories(Configgen(tree.__getitem__))]
self.assertEqual(len(records), 1)
self.assertEqual(records[0]["search_dir"], "/userdata/bios/dc")
self.assertEqual(records[0]["passes_to"], "environment FLYCAST_BIOS_PATH")
self.assertTrue(records[0]["source_ref"].endswith("flycastGenerator.py:6"))
tree[f"{GENERATORS}/flycast/flycastGenerator.py"] = generator.replace("FLYCAST_BIOS,", "CONFIGS,")
with self.assertRaises(LauncherShapeError):
environment_directories(Configgen(tree.__getitem__))
def test_retrobat_bios_path_setting(self) -> None:
from scraper import retrobat_launchers as rl
path = f"{rl.GENERATORS}/Flycast.Generator.cs"
text = textwrap.dedent("""\
private void SetupConfiguration(string path)
{
string configfile = Path.Combine(path, "emu.cfg");
using (var ini = IniFile.FromFile(configfile, IniOptions.UseSpaces))
{
string biosPath = Path.Combine(AppConfig.GetFullPath("bios"), "dc");
ini.Remove("config", "Dreamcast.BiosPath");
ini.WriteValue("config", "Dreamcast.BiosPath", biosPath);
}
}
""")
records = rl._value_writes(rl.Source(path, text), "flycast", "standalone")
self.assertEqual([r["search_dir"] for r in records], ["bios/dc"])
self.assertIn("Dreamcast.BiosPath", records[0]["passes_to"])
if __name__ == "__main__":
unittest.main()
class OpenmsxOnEachPlatform(unittest.TestCase):
"""openMSX finds system ROMs by SHA1 in share/systemroms below its user
directory and in every filepool entry. RetroBat sets OPENMSX_HOME to
bios/openmsx, RetroPie links share/systemroms of its user directory to
BIOS/openmsx, Batocera and KNULLI add bios/Machines and bios/openmsx to
the filepool and list their own files in bios/openmsx."""
EXPECTED = {
"batocera": {"share/systemroms": ("", "openmsx")},
"knulli": {"share/systemroms": ("", "openmsx")},
"retrobat": {"": ("", "openmsx")},
"retropie": {"share/systemroms": ("", "openmsx")},
}
@classmethod
def setUpClass(cls) -> None:
cls.profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["openmsx"]
def test_each_platform_hands_openmsx_its_folder(self) -> None:
for platform, expected in self.EXPECTED.items():
with self.subTest(platform=platform):
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
self.assertEqual(
standalone_bindings_for("openmsx", self.profile, standalone_set(cfg)),
expected,
)
def test_every_system_rom_lands_in_bios_openmsx(self) -> None:
for platform in self.EXPECTED:
with self.subTest(platform=platform):
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
(view, _standalone), = platform_layouts(
"openmsx", self.profile, standalone_set(cfg)
)
for entry in view["files"]:
self.assertNotIn("unplaced", entry)
self.assertTrue(
entry["standalone_path"].startswith("openmsx/"), entry["standalone_path"]
)
class XroarOnEachPlatform(unittest.TestCase):
"""XRoar reads each ROM from a directory of rompath. Batocera writes
rompath bios/xroar, RetroDECK lists $bios_path in it, RetroPie links
the build's ${prefix}/share/xroar/roms to $biosdir, RetroBat passes
-rompath bios/coco for coco and bios/dragon otherwise, Recalbox
-rompath bios/<system>."""
# Platform -> profile system -> folder, "" for the BIOS root.
EXPECTED = {
"batocera": dict.fromkeys(("dragon32", "dragon64", "coco", "coco3", "mc10"), "xroar"),
"retrodeck": dict.fromkeys(("dragon32", "dragon64", "coco", "coco3", "mc10"), ""),
"retropie": dict.fromkeys(("dragon32", "dragon64", "coco", "coco3", "mc10"), ""),
# RetroBat offers XRoar's CoCo 3 machines under coco and no MC-10.
"retrobat": {**dict.fromkeys(("dragon32", "dragon64", "mc10"), "dragon"),
**dict.fromkeys(("coco", "coco3"), "coco")},
"recalbox": {**dict.fromkeys(("dragon32", "dragon64"), "dragon"),
**dict.fromkeys(("coco", "coco3", "mc10"), "trs80coco")},
}
@classmethod
def setUpClass(cls) -> None:
cls.profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["xroar"]
def test_each_rom_lands_where_its_launch_reads_it(self) -> None:
for platform, folders in self.EXPECTED.items():
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
(view, _standalone), = platform_layouts("xroar", self.profile, standalone_set(cfg))
for entry in view["files"]:
with self.subTest(platform=platform, name=entry["name"]):
self.assertNotIn("unplaced", entry)
folder = folders[entry["system"]]
expected = f"{folder}/{entry['name']}" if folder else entry["name"]
self.assertEqual(entry["standalone_path"], expected)
class AmiberryOnEachPlatform(unittest.TestCase):
"""The standalone Amiberry scans rom_path for its ROMs. Batocera, KNULLI
and RetroPie set rom_path to bios/amiga."""
BOUND = ("batocera", "knulli", "retropie")
@classmethod
def setUpClass(cls) -> None:
cls.profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["amiberry"]
def test_rom_path_is_bios_amiga(self) -> None:
for platform in self.BOUND:
with self.subTest(platform=platform):
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
self.assertEqual(
standalone_bindings_for("amiberry", self.profile, standalone_set(cfg)),
{"": ("", "amiga")},
)
def test_every_rom_path_entry_lands_in_bios_amiga(self) -> None:
for platform in self.BOUND:
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
(view, _standalone), = platform_layouts(
"amiberry", self.profile, standalone_set(cfg)
)
for own, entry in zip(self.profile["files"], view["files"]):
layout = own.get("standalone_path")
if not layout:
continue
with self.subTest(platform=platform, name=entry["name"]):
self.assertEqual(entry["standalone_path"], f"amiga/{layout}")
self.assertNotIn("/", layout.removeprefix("mt32-roms/"))
class AmiberryBuildEachPlatformShips(unittest.TestCase):
"""ROCKNIX, AmberELEC and EmuELEC compile older Amiberry revisions: on each
the name amiberry is that build, never the 8.5.0 profile."""
OWN = {"rocknix": "amiberry-5.6.5", "amberelec": "amiberry-7.1.1", "emuelec": "amiberry-8.0.0"}
@classmethod
def setUpClass(cls) -> None:
cls.profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
def resolved(self, platform: str) -> set[str]:
from common import resolve_platform_cores
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
return {p for p in resolve_platform_cores(cfg, self.profiles) if p.startswith("amiberry")}
def test_each_platform_resolves_its_own_build(self) -> None:
for platform, own in self.OWN.items():
with self.subTest(platform=platform):
self.assertEqual(self.resolved(platform), {own})
def test_the_others_keep_the_main_profile(self) -> None:
for platform in ("batocera", "knulli", "retrobat", "retropie", "recalbox"):
with self.subTest(platform=platform):
self.assertEqual(self.resolved(platform), {"amiberry"})
def test_rocknix_reads_kickstarts_through_the_copied_link(self) -> None:
# amiberry 5.6.5 reads <cwd>/kickstarts. ROCKNIX runs it in
# /storage/.config/amiberry, the copy of /usr/config/amiberry whose
# kickstarts link points at the BIOS root.
profile = self.profiles["amiberry-5.6.5"]
cfg = load_platform_config("rocknix", str(REPO_ROOT / "platforms"))
self.assertEqual(
standalone_bindings_for("amiberry-5.6.5", profile, standalone_set(cfg)),
{"kickstarts": ("", "")},
)
((view, _standalone),) = platform_layouts("amiberry-5.6.5", profile, standalone_set(cfg))
for entry in view["files"]:
with self.subTest(name=entry["name"]):
if entry["path"].startswith("kickstarts/"):
self.assertEqual(entry["standalone_path"], entry["path"][len("kickstarts/"):])
else:
self.assertEqual(entry.get("unplaced"), "outside_bios_folder")