mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
MAME splits rompath, samplepath, hashpath and pluginspath on semicolons and tries each directory in turn (src/emu/fileio.cpp:53-58, 330-392, src/emu/emuopts.cpp:44-46, 53). The launcher records spell those lists with the game's own folder in them, which handed_directories skipped: - RetroBat passes -rp bios;<rom directory> (Mame64.Generator.cs:93-96) and -rompath bios/psxmame/bios;<rom directory> to pSxMAME (PSXMame.Generator.cs:38-48). - KNULLI passes -rompath <romDirname>;/userdata/bios/mame;/userdata/bios (mameGenerator.py:116-122). - RetroPie writes rompath $romdir/mame;$romdir/arcade;$biosdir/mame (mame.sh:133). A semicolon list now gives each directory but the game's folder. A pool that already lists the folder the files take keeps them there, and a pool standing for the whole layout holds its files, never its folders. The MAME profile names its sets at the top of the rompath and its search settings as pools, pSxMAME its sets and rompath. RetroBat's -sp and -hash records split on `bool hbmame = emulator == "hbmame"` (Mame64.Generator.cs:30): bios/mame/samples for mame64, bios/hbmame/samples for hbmame. Manifests: RetroBat gains the 12 pSxMAME sets in psxmame/bios and drops samples/bbc.zip, read by nobody, which mame/samples/bbc.zip already serves. KNULLI drops the same copy. RetroPie gains the 1427 MAME sets in BIOS/mame, the root copies staying for the libretro cores. EmuDeck names samples/bbc.zip as read outside the BIOS folder. RetroDECK's samplepath is $bios_path/mame-sa/samples (mame/component_prepare.sh:94, 117): bbc.zip moves there from bios/samples/. Its MAME runs with the -rompath ES-DE passes, %GAMEDIR%;%ROMPATH%/<system>, so the standalone sets are named as read outside the BIOS folder and the root copies stay for the libretro core. The 1427 sets of RetroBat, KNULLI and EmuDeck stay at the BIOS root, listed in their rompath. No file of any manifest is lost.
810 lines
37 KiB
Python
810 lines
37 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
|
|
SEARCHED_FILES,
|
|
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": ""}),
|
|
{SEARCHED_FILES: ("", "xroar")},
|
|
)
|
|
|
|
def test_a_semicolon_list_leaves_out_the_game_folder(self) -> None:
|
|
# MAME splits its search paths on semicolons (src/emu/fileio.cpp:53-58).
|
|
found = handed_directories(config(passes(
|
|
"emu", "<romDirname>;/userdata/bios/mame;/userdata/bios", "command line -rompath",
|
|
)))
|
|
self.assertEqual([h.folder for h in found], ["mame", ""])
|
|
found = handed_directories(config(passes(
|
|
"emu", "bios;<rom directory>", "command line -rp bios;<rom directory>",
|
|
)))
|
|
self.assertEqual([(h.folder, "-rp" in h.handles) for h in found], [("", True)])
|
|
|
|
def test_a_list_holding_the_files_folder_keeps_them_there(self) -> None:
|
|
# KNULLI lists bios/mame before bios: the sets at the root stay.
|
|
cfg = config(passes(
|
|
"emu", "<romDirname>;/userdata/bios/mame;/userdata/bios", "command line -rompath",
|
|
))
|
|
cfg["systems"] = {"x": {"files": [{"name": "a.rom", "destination": "mame/a.rom"}]}}
|
|
self.assertEqual(bind(cfg, [], {"-rompath": ""}), {SEARCHED_FILES: ("", "")})
|
|
# Without the root, the first folder of the BIOS tree.
|
|
cfg = config(passes("emu", "$romdir/mame;$biosdir/mame", "rompath in mame.ini"))
|
|
self.assertEqual(bind(cfg, [], {"rompath": ""}), {SEARCHED_FILES: ("", "mame")})
|
|
|
|
def test_a_whole_layout_list_holds_files_not_folders(self) -> None:
|
|
found = {SEARCHED_FILES: ("", "mame")}
|
|
self.assertEqual(bound_path("neogeo.zip", found), ("", "mame/neogeo.zip"))
|
|
self.assertIsNone(bound_path("samples/bbc.zip", found))
|
|
self.assertEqual(
|
|
bound_path("samples/bbc.zip", {**found, "samples": ("", "mame-sa/samples")}),
|
|
("", "mame-sa/samples/bbc.zip"),
|
|
)
|
|
|
|
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",)), {SEARCHED_FILES: ("", "coco")})
|
|
|
|
def test_another_system_keeps_the_generic_record(self) -> None:
|
|
self.assertEqual(bind(self.CFG, [], self.POOLS, ("dragon32",)), {SEARCHED_FILES: ("", "dragon")})
|
|
self.assertEqual(bind(self.CFG, [], self.POOLS), {SEARCHED_FILES: ("", "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",)),
|
|
{SEARCHED_FILES: ("", "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"])
|
|
|
|
|
|
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 adds bios/Machines and bios/openmsx to the
|
|
filepool and lists its own files in bios/openmsx. KNULLI's generator
|
|
does the same, but no KNULLI board builds openmsx."""
|
|
|
|
EXPECTED = {
|
|
"batocera": {"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 and KNULLI
|
|
set it to bios/amiga, Recalbox to bios/amiga/bios."""
|
|
|
|
BOUND = {
|
|
"batocera": ("amiberry-7.1.1-332", "amiga"),
|
|
"knulli": ("amiberry-7.1.1", "amiga"),
|
|
"recalbox": ("amiberry-8.2.2", "amiga/bios"),
|
|
}
|
|
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
|
|
|
def test_rom_path_is_the_platform_folder(self) -> None:
|
|
for platform, (name, folder) in self.BOUND.items():
|
|
with self.subTest(platform=platform):
|
|
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
|
|
self.assertEqual(
|
|
standalone_bindings_for(name, self.profiles[name], standalone_set(cfg)),
|
|
{"": ("", folder)},
|
|
)
|
|
|
|
def test_every_rom_path_entry_lands_in_that_folder(self) -> None:
|
|
for platform, (name, folder) in self.BOUND.items():
|
|
profile = self.profiles[name]
|
|
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
|
|
views = platform_layouts(name, profile, standalone_set(cfg))
|
|
((view, _standalone),) = [(v, s) for v, s in views if s]
|
|
for own, entry in zip(profile["files"], view["files"]):
|
|
if own.get("mode") == "libretro":
|
|
continue
|
|
layout = own.get("standalone_path") or own.get("path") or own["name"]
|
|
with self.subTest(platform=platform, name=entry["name"]):
|
|
self.assertNotIn("unplaced", entry)
|
|
self.assertEqual(entry["standalone_path"], f"{folder}/{layout}")
|
|
|
|
def test_retrobat_reads_its_folders_below_the_amiberry_emulator_folder(self) -> None:
|
|
# RetroBat writes rom_path and whdboot_path below the amiberry
|
|
# folder of its install and copies the kickstarts it finds in bios
|
|
# into that ROMs folder.
|
|
name = "amiberry-8.3.0"
|
|
profile = self.profiles[name]
|
|
cfg = load_platform_config("retrobat", str(REPO_ROOT / "platforms"))
|
|
self.assertEqual(
|
|
standalone_bindings_for(name, profile, standalone_set(cfg)),
|
|
{"": ("emulators", "amiberry/ROMs"), "WHDBoot": ("emulators", "amiberry/WHDBoot")},
|
|
)
|
|
((view, _standalone),) = platform_layouts(name, profile, standalone_set(cfg))
|
|
for own, entry in zip(profile["files"], view["files"]):
|
|
layout = own.get("standalone_path") or own.get("path") or own["name"]
|
|
folder = "amiberry" if layout.startswith("WHDBoot/") else "amiberry/ROMs"
|
|
with self.subTest(name=entry["name"]):
|
|
self.assertNotIn("unplaced", entry)
|
|
self.assertEqual(entry.get("root"), "emulators")
|
|
self.assertEqual(entry["standalone_path"], f"{folder}/{layout}")
|
|
|
|
|
|
class AmiberryBuildEachPlatformShips(unittest.TestCase):
|
|
"""Each platform resolves the name amiberry to the build its own package
|
|
compiles. RetroBat keeps the 8.5.0 libretro core beside its standalone,
|
|
and Batocera's x86_64 boards fetch it with RetroArch's core downloader
|
|
(installable_cores)."""
|
|
|
|
OWN = {
|
|
"rocknix": {"amiberry-5.6.5"},
|
|
"amberelec": {"amiberry-7.1.1"},
|
|
"emuelec": {"amiberry-8.0.0"},
|
|
"batocera": {"amiberry", "amiberry-7.1.1-332"},
|
|
"knulli": {"amiberry-7.1.1"},
|
|
"recalbox": {"amiberry-8.2.2"},
|
|
"retrobat": {"amiberry", "amiberry-8.3.0"},
|
|
"retropie": {"amiberry-5.7.2", "amiberry-6.3.3"},
|
|
"emudeck": {"amiberry"},
|
|
"retrodeck": {"amiberry"},
|
|
}
|
|
|
|
@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_no_standalone_amiberry_is_the_buildbot_revision(self) -> None:
|
|
from common import list_registered_platforms
|
|
|
|
for platform in list_registered_platforms(str(REPO_ROOT / "platforms")):
|
|
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
|
|
if "amiberry" not in self.resolved(platform):
|
|
continue
|
|
layouts = platform_layouts("amiberry", self.profiles["amiberry"], standalone_set(cfg))
|
|
with self.subTest(platform=platform):
|
|
self.assertEqual([standalone for _view, standalone in layouts], [False])
|
|
|
|
def test_the_buildbot_core_runs_where_retroarch_installs_it(self) -> None:
|
|
from common import load_target_config, resolve_platform_cores
|
|
|
|
for platform in ("retroarch", "lakka", "batocera"):
|
|
with self.subTest(platform=platform):
|
|
self.assertIn("amiberry", self.resolved(platform))
|
|
cfg = load_platform_config("batocera", str(REPO_ROOT / "platforms"))
|
|
# x86_64 boards download from linux-x86_64, which builds the core.
|
|
# linux-armhf does not, and aarch64 boards have no downloader.
|
|
for target, runs in (("x86_64", True), ("zen3", True), ("h3", False), ("bcm2711", False)):
|
|
cores = load_target_config("batocera", target, str(REPO_ROOT / "platforms"))
|
|
with self.subTest(target=target):
|
|
resolved = resolve_platform_cores(cfg, self.profiles, cores)
|
|
self.assertEqual("amiberry" in resolved, runs)
|
|
self.assertIn("amiberry-7.1.1-332", resolved)
|
|
|
|
def test_retropie_reads_kickstarts_below_its_start_folder(self) -> None:
|
|
# amiberry 5.7 and 6.3 read <start path>/kickstarts, which RetroPie
|
|
# links to bios/amiga. Its whdboot and roms folders stay below it.
|
|
cfg = load_platform_config("retropie", str(REPO_ROOT / "platforms"))
|
|
for name in ("amiberry-5.7.2", "amiberry-6.3.3"):
|
|
profile = self.profiles[name]
|
|
self.assertEqual(
|
|
standalone_bindings_for(name, profile, standalone_set(cfg)),
|
|
{"kickstarts": ("", "amiga")},
|
|
)
|
|
((view, _standalone),) = platform_layouts(name, profile, standalone_set(cfg))
|
|
for entry in view["files"]:
|
|
with self.subTest(name=name, file=entry["name"]):
|
|
if entry["path"].startswith("kickstarts/"):
|
|
self.assertEqual(entry["standalone_path"],
|
|
"amiga/" + entry["path"][len("kickstarts/"):])
|
|
else:
|
|
self.assertEqual(entry.get("unplaced"), "outside_bios_folder")
|
|
|
|
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")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|