mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-05 09:33:52 -06:00
chore: merge the amberelec, emuelec, knulli lists
AmberELEC, PAN4ELEC and EmuELEC detection now installs their own lists at /storage/roms, the root those lists are written against; KNULLI its own list at /userdata/bios. JELOS, KOLOS, UnofficialOS and Arch R keep RetroArch's list with the folder their retroarch.cfg names.
This commit is contained in:
commit
259fb14954
38 files changed
+299417
-284
No files matched your search
@@ -152,6 +152,22 @@ class DownloadLinksNameTheFile(unittest.TestCase):
|
||||
for asset in pack["assets"]:
|
||||
self.assertIn(f"/releases/download/{RECORD['tag']}/{asset})", page)
|
||||
|
||||
def test_a_platform_the_release_lacks_has_no_row(self):
|
||||
"""KNULLI, AmberELEC and EmuELEC were registered after the recorded
|
||||
release: a row for them linked a release page holding no such pack."""
|
||||
if not RECORD.get("packs"):
|
||||
self.skipTest("release.json is not written")
|
||||
from common import list_registered_platforms, load_platform_config
|
||||
from release_record import pack_for
|
||||
|
||||
page = _which_pack()
|
||||
for key in list_registered_platforms("platforms", include_archived=True):
|
||||
if pack_for(key, RECORD) is not None:
|
||||
continue
|
||||
label = load_platform_config(key, "platforms").get("platform", key)
|
||||
with self.subTest(platform=key):
|
||||
self.assertNotIn(f"| [{label}](", page)
|
||||
|
||||
|
||||
class SlicesLeaveWithTheirRelease(unittest.TestCase):
|
||||
"""Parts named `.zip.001` were cut by v2026.09.04, deleted since. A page
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
"""AmberELEC, EmuELEC and KNULLI publish Batocera's `systems` dict.
|
||||
|
||||
Each scraper reads its list with batocera_scraper's parser, so a dict written
|
||||
with a key twice keeps the later value as Python does, paths keep the folder
|
||||
the OS's checker prefixes, and the cores come from the OS's own
|
||||
EmulationStation list. Trimmed copies of the three lists stand in for the
|
||||
upstream files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
from scraper import amberelec_scraper, emuelec_scraper, knulli_scraper # noqa: E402
|
||||
from scraper.es_emulators import es_cores, json_emulators, xml_emulators # noqa: E402
|
||||
|
||||
CHECKER = '''
|
||||
|
||||
def checkBios(systems, prefix):
|
||||
pass
|
||||
'''
|
||||
|
||||
AMBERELEC = '''#!/usr/bin/env python
|
||||
systems = {
|
||||
"atarilynx": {"name": "Lynx", "biosFiles": [{"md5": "fcd403db69f54290b51035d82f835e7b", "file": "bios/lynxboot.img"}]},
|
||||
# Neo Geo
|
||||
"neogeo": {"name": "NeoGeo", "biosFiles": [{"md5": "", "file": "neogeo/neogeo.zip"},
|
||||
{"md5": "", "file": "neogeo/aes.zip"}]},
|
||||
}
|
||||
''' + CHECKER
|
||||
|
||||
EMUELEC = '''#!/usr/bin/env python
|
||||
systems = {
|
||||
"naomi": { "name": "Naomi", "biosFiles": [ { "md5": "e63d892cdb8b532351dc7020bb60b6f4", "file": "bios/dc/naomi.zip" } ] },
|
||||
"fmtowns": { "name": "Fujistu FM-Towns", "biosFiles": [
|
||||
{ "md5": "34847786d7de94b5d1133c956ab1d75d", "zippedFile": "fmt_dic.rom", "file": "bios/fmtowns.zip"} ] },
|
||||
"Diablo": { "name": "Diablo 1", "biosFiles": [ { "md5": "", "file": "ports/diablo/diabdat.mpq" } ] },
|
||||
"naomi": { "name": "Naomi", "biosFiles": [ { "md5": "3bffafac42a7767d8dcecf771f5552ba", "file": "bios/dc/naomi_boot.bin" },
|
||||
{ "md5": "e63d892cdb8b532351dc7020bb60b6f4", "file": "bios/dc/naomi.zip" } ] },
|
||||
}
|
||||
''' + CHECKER
|
||||
|
||||
KNULLI = '''#!/usr/bin/env python
|
||||
systems = {
|
||||
"msx1": { "name": "MSX1", "biosFiles": [ { "md5": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "file": "bios/MSX.ROM" } ] },
|
||||
"msu-md": { "name": "MSU-MD", "biosFiles": [ { "md5": "e66fa1dc5820d254611fdcdba0662372", "file": "bios/bios_CD_E.bin" } ] },
|
||||
"msx1": { "name": "MSX1", "emulator": "libretro", "core": "bluemsx", "biosFiles": [
|
||||
{ "md5": "", "file": "bios/Machines/Shared Roms/MSX.rom" } ] },
|
||||
}
|
||||
''' + CHECKER
|
||||
|
||||
AMBERELEC_ES = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<systemList>
|
||||
<system><name>advision</name><emulators>
|
||||
<emulator name="mame"><cores><core default="true">advision</core></cores></emulator>
|
||||
</emulators></system>
|
||||
<system><name>atarist</name><emulators>
|
||||
<emulator name="retroarch"><cores><core>hatari</core></cores></emulator>
|
||||
<emulator name="HATARISA"><cores><core>HATARISA</core></cores></emulator>
|
||||
</emulators></system>
|
||||
<system><name>psp</name><emulators>
|
||||
<emulator name="retroarch"><cores><core>ppsspp</core></cores></emulator>
|
||||
<emulator name="PPSSPPSDL"><cores><core>PPSSPPSDL</core></cores></emulator>
|
||||
</emulators></system>
|
||||
<system><name>dreamcast</name><emulators>
|
||||
<emulator name="retrorun"><cores><core>flycast</core></cores></emulator>
|
||||
</emulators></system>
|
||||
</systemList>
|
||||
"""
|
||||
|
||||
EMUELEC_ES = """{"systems": [
|
||||
{"name": "imageviewer", "command": "ignore"},
|
||||
{"name": "saturn", "emulators": [
|
||||
{"name": "libretro", "cores": [{"name": "yabasanshiro"}]},
|
||||
{"name": "yabasanshiro", "cores": [{"name": "yabasanshiroSA"}]}]},
|
||||
{"name": "atari2600", "emulators": [{"name": "STELLASA", "cores": [{"name": "STELLASA"}]}]}
|
||||
]}"""
|
||||
|
||||
|
||||
def _scraper(module, raw: str):
|
||||
scraper = module.Scraper(url="https://example.test/list")
|
||||
scraper._raw_data = raw
|
||||
return scraper
|
||||
|
||||
|
||||
class TheListsReadAsPythonReadsThem(unittest.TestCase):
|
||||
def test_amberelec_keeps_paths_beside_bios(self):
|
||||
reqs = _scraper(amberelec_scraper, AMBERELEC).fetch_requirements()
|
||||
self.assertEqual(
|
||||
[(r.system, r.destination, r.native_id) for r in reqs],
|
||||
[("atari-lynx", "bios/lynxboot.img", "atarilynx"),
|
||||
("snk-neogeo", "neogeo/neogeo.zip", "neogeo"),
|
||||
("snk-neogeo", "neogeo/aes.zip", "neogeo")],
|
||||
)
|
||||
|
||||
def test_emuelec_keeps_the_later_of_a_repeated_key(self):
|
||||
reqs = _scraper(emuelec_scraper, EMUELEC).fetch_requirements()
|
||||
naomi = [r.destination for r in reqs if r.native_id == "naomi"]
|
||||
self.assertEqual(naomi, ["bios/dc/naomi_boot.bin", "bios/dc/naomi.zip"])
|
||||
self.assertEqual(len(reqs), 4)
|
||||
|
||||
def test_emuelec_files_a_port_under_its_engine_system(self):
|
||||
reqs = _scraper(emuelec_scraper, EMUELEC).fetch_requirements()
|
||||
diablo = next(r for r in reqs if r.native_id == "Diablo")
|
||||
self.assertEqual((diablo.system, diablo.destination),
|
||||
("diablo", "ports/diablo/diabdat.mpq"))
|
||||
|
||||
def test_emuelec_carries_the_member_its_list_names(self):
|
||||
reqs = _scraper(emuelec_scraper, EMUELEC).fetch_requirements()
|
||||
towns = next(r for r in reqs if r.native_id == "fmtowns")
|
||||
self.assertEqual(towns.zipped_file, "fmt_dic.rom")
|
||||
|
||||
def test_knulli_strips_bios_and_keeps_the_later_key(self):
|
||||
reqs = _scraper(knulli_scraper, KNULLI).fetch_requirements()
|
||||
self.assertEqual(
|
||||
[(r.system, r.destination, r.native_path) for r in reqs],
|
||||
[("microsoft-msx", "Machines/Shared Roms/MSX.rom", "bios/Machines/Shared Roms/MSX.rom"),
|
||||
("sega-mega-cd", "bios_CD_E.bin", "bios/bios_CD_E.bin")],
|
||||
)
|
||||
|
||||
def test_a_list_without_its_checker_is_refused(self):
|
||||
for module, raw in ((amberelec_scraper, AMBERELEC), (emuelec_scraper, EMUELEC),
|
||||
(knulli_scraper, KNULLI)):
|
||||
with self.subTest(scraper=module.__name__), self.assertRaises(ValueError):
|
||||
_scraper(module, raw.replace("def checkBios", "def other")).fetch_requirements()
|
||||
|
||||
|
||||
class TheCoresComeFromTheOsList(unittest.TestCase):
|
||||
def test_amberelec_runners_and_standalone_names(self):
|
||||
cores, standalone, dual = es_cores(
|
||||
xml_emulators(AMBERELEC_ES), amberelec_scraper.RUNNERS, amberelec_scraper.RENAMED
|
||||
)
|
||||
# mame lists MAME machines and loads the MAME core: advision is no core.
|
||||
self.assertIn("mame", cores)
|
||||
self.assertNotIn("advision", cores)
|
||||
self.assertNotIn("mame", standalone)
|
||||
self.assertIn("flycast", cores)
|
||||
self.assertTrue({"HATARISA", "hatari", "PPSSPPSDL", "ppsspp"} <= set(standalone))
|
||||
self.assertEqual(dual, ["hatari", "ppsspp"])
|
||||
|
||||
def test_emuelec_runners_and_standalone_names(self):
|
||||
cores, standalone, dual = es_cores(
|
||||
json_emulators(EMUELEC_ES), emuelec_scraper.RUNNERS, emuelec_scraper.RENAMED
|
||||
)
|
||||
self.assertIn("stella", standalone)
|
||||
self.assertNotIn("yabasanshiroSA", cores)
|
||||
self.assertEqual(dual, ["yabasanshiro"])
|
||||
|
||||
def test_the_newest_prerelease_names_its_commit(self):
|
||||
body = "This is a prerelease based on the commit: AmberELEC/AmberELEC@" + "a" * 40 + "."
|
||||
self.assertEqual(amberelec_scraper.prerelease_commit(body), "a" * 40)
|
||||
with self.assertRaises(RuntimeError):
|
||||
amberelec_scraper.prerelease_commit("no commit here")
|
||||
|
||||
|
||||
class CoreFilesGoUnderTheStatedFolder(unittest.TestCase):
|
||||
"""EmuELEC's ports/ entries are a quarter of its list, so the most common
|
||||
root of its destinations fell under the 90% rule and core files went to
|
||||
/storage/roms itself, where no core reads them."""
|
||||
|
||||
def test_core_destination_wins_over_the_guess(self):
|
||||
from common import declared_root
|
||||
|
||||
files = [{"destination": f"bios/f{i}"} for i in range(3)]
|
||||
files.append({"destination": "ports/diablo/diabdat.mpq"})
|
||||
config = {"base_destination": "", "systems": {"s": {"files": files}}}
|
||||
self.assertEqual(declared_root(config), "")
|
||||
self.assertEqual(declared_root({**config, "core_destination": "bios"}), "bios")
|
||||
self.assertEqual(declared_root({**config, "base_destination": "bios",
|
||||
"core_destination": "x"}), "")
|
||||
|
||||
def test_the_platforms_state_it(self):
|
||||
import yaml
|
||||
|
||||
for name in ("amberelec", "emuelec"):
|
||||
with self.subTest(platform=name):
|
||||
config = yaml.safe_load((REPO_ROOT / "platforms" / f"{name}.yml").read_text())
|
||||
self.assertEqual((config["base_destination"], config["core_destination"]),
|
||||
("", "bios"))
|
||||
|
||||
|
||||
class TheExportKeepsTheirShape(unittest.TestCase):
|
||||
def test_an_indented_closing_brace_is_found(self):
|
||||
from exporter.emuelec_exporter import Exporter
|
||||
|
||||
head, body, tail = Exporter._split_original(EMUELEC)
|
||||
self.assertEqual(head[-1], "systems = {")
|
||||
self.assertEqual(tail[0], " }")
|
||||
self.assertIn("Diablo", "\n".join(body))
|
||||
|
||||
def test_paths_start_where_the_list_starts(self):
|
||||
from exporter.amberelec_exporter import Exporter as Amber
|
||||
from exporter.baseline import NativeFile
|
||||
from exporter.knulli_exporter import Exporter as Knulli
|
||||
|
||||
fe = NativeFile("neogeo.zip", "neogeo/neogeo.zip", "neogeo")
|
||||
self.assertEqual(Amber.declared_path(fe), "neogeo/neogeo.zip")
|
||||
fe = NativeFile("bios_CD_E.bin", "bios_CD_E.bin", "msu-md")
|
||||
self.assertEqual(Knulli.declared_path(fe), "bios/bios_CD_E.bin")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -586,7 +586,7 @@ class ExporterRegistry(unittest.TestCase):
|
||||
def test_an_exporter_that_patches_code_says_it_needs_the_original(self):
|
||||
"""Regenerating a script from data alone drops the code around it."""
|
||||
for name in ("batocera", "rocknix", "emudeck", "retrodeck", "bizhawk",
|
||||
"misterfpga"):
|
||||
"misterfpga", "knulli", "amberelec", "emuelec"):
|
||||
with self.subTest(platform=name):
|
||||
self.assertTrue(discover_exporters()[name].needs_original())
|
||||
|
||||
|
||||
+26
-13
@@ -1088,7 +1088,7 @@ class DetectionFollowsTheRegistry(unittest.TestCase):
|
||||
lambda p: str(p) in ("/usr/share/knulli/knulli.version",
|
||||
"/usr/share/batocera/batocera.version"),
|
||||
):
|
||||
self.assertEqual(install._detect_embedded(), [("batocera", Path("/userdata/bios"))])
|
||||
self.assertEqual(install._detect_embedded(), [("knulli", Path("/userdata/bios"))])
|
||||
# What ROCKNIX writes since 2025: OS_NAME and HW_DEVICE, no ID.
|
||||
with unittest.mock.patch.object(install, "_parse_os_release",
|
||||
return_value={"OS_NAME": "ROCKNIX", "HW_DEVICE": "RK3566"}):
|
||||
@@ -4398,27 +4398,31 @@ class HandheldSystemsByTheirOwnFiles(unittest.TestCase):
|
||||
return root, found[0], names[0]
|
||||
|
||||
def _elec(self, release: str, name: str, extra: dict | None = None,
|
||||
plat: str = "retroarch"):
|
||||
plat: str = "retroarch", folder: str = "storage/roms/bios"):
|
||||
cfg = {self.ELEC_CFG: 'system_directory = "~/roms/bios"\n'}
|
||||
root, (found_plat, path), found_name = self._one({**cfg, **(extra or {})}, release)
|
||||
self.assertEqual((found_plat, path, found_name),
|
||||
(plat, root / "storage/roms/bios", name))
|
||||
(plat, root / folder, name))
|
||||
|
||||
# AmberELEC and EmuELEC publish their own lists, written against
|
||||
# /storage/roms (bios/, ports/ and neogeo/ side by side).
|
||||
def test_amberelec(self):
|
||||
self._elec('NAME="AmberELEC"\nID="coreelec"\n', "AmberELEC",
|
||||
plat=install._STANDIN_MANIFESTS["AmberELEC"])
|
||||
plat="amberelec", folder="storage/roms")
|
||||
|
||||
def test_chaos_names_itself_amberelec(self):
|
||||
# tech4bot/AmberELEC writes the same os-release as AmberELEC.
|
||||
self._elec('NAME="AmberELEC"\nID="coreelec"\nCOREELEC_DEVICE="RG552"\n', "AmberELEC")
|
||||
self._elec('NAME="AmberELEC"\nID="coreelec"\nCOREELEC_DEVICE="RG552"\n', "AmberELEC",
|
||||
plat="amberelec", folder="storage/roms")
|
||||
|
||||
def test_pan4elec_over_amberelec(self):
|
||||
self._elec('NAME="AmberELEC"\n', "PAN4ELEC",
|
||||
{"/usr/bin/batocera-info": 'echo "OPERATING SYSTEM: PAN4ELEC"\n'})
|
||||
{"/usr/bin/batocera-info": 'echo "OPERATING SYSTEM: PAN4ELEC"\n'},
|
||||
plat="amberelec", folder="storage/roms")
|
||||
|
||||
def test_emuelec(self):
|
||||
self._elec('NAME="EmuELEC"\nID="emuelec"\n', "EmuELEC",
|
||||
plat=install._STANDIN_MANIFESTS["EmuELEC"])
|
||||
plat="emuelec", folder="storage/roms")
|
||||
|
||||
def test_jelos(self):
|
||||
self._elec('OS_NAME="JELOS"\nHW_DEVICE="RK3566"\n', "JELOS")
|
||||
@@ -4560,9 +4564,9 @@ class HandheldSystemsByTheirOwnFiles(unittest.TestCase):
|
||||
self.assertIn("JELOS and Koriki are both detected here", err.getvalue())
|
||||
|
||||
def test_the_system_is_announced_by_its_own_name(self):
|
||||
root, found, _name = self._one({self.ELEC_CFG: ""}, 'NAME="AmberELEC"\n')
|
||||
root, found, _name = self._one({self.ELEC_CFG: ""}, 'OS_NAME="JELOS"\n')
|
||||
self.assertEqual(install.found_label(*found),
|
||||
f"AmberELEC at {root / 'storage/roms/bios'} (RetroArch files)")
|
||||
f"JELOS at {root / 'storage/roms/bios'} (RetroArch files)")
|
||||
|
||||
def test_list_platforms_names_every_system(self):
|
||||
with unittest.mock.patch.object(sys, "argv", ["install.py", "--list-platforms"]), \
|
||||
@@ -4592,16 +4596,25 @@ class BatoceraAndKnulliByWhatTheirBuildsWrite(unittest.TestCase):
|
||||
|
||||
def test_knulli_by_its_os_release_or_version_file(self):
|
||||
self.assertEqual(self._detect({"OS_NAME": "knulli"}, ()),
|
||||
[("batocera", Path("/userdata/bios"))])
|
||||
[("knulli", Path("/userdata/bios"))])
|
||||
self.assertEqual(self._detect({}, ("/usr/share/knulli/knulli.version",)),
|
||||
[("batocera", Path("/userdata/bios"))])
|
||||
[("knulli", Path("/userdata/bios"))])
|
||||
|
||||
def test_amberelec_and_emuelec_by_their_os_release(self):
|
||||
"""Both write os-release from DISTRONAME in scripts/image. AmberELEC
|
||||
keeps CoreELEC's ID, so its NAME tells it apart."""
|
||||
self.assertEqual(self._detect({"NAME": "AmberELEC", "ID": "coreelec"}, ()),
|
||||
[("amberelec", Path("/storage/roms"))])
|
||||
self.assertEqual(self._detect({"NAME": "EmuELEC", "ID": "emuelec"}, ()),
|
||||
[("emuelec", Path("/storage/roms"))])
|
||||
self.assertEqual(self._detect({"NAME": "CoreELEC", "ID": "coreelec"}, ()), [])
|
||||
|
||||
def test_a_knulli_board_is_named_and_not_narrowed(self):
|
||||
board = Path(_tempdir(self)) / "knulli.arch"
|
||||
board.write_text("a133")
|
||||
with unittest.mock.patch.dict(install._DERIVATIVE_BOARDS,
|
||||
{"batocera": ("Knulli", str(board))}):
|
||||
self.assertEqual(install.derivative_board("batocera"), ("Knulli", "a133"))
|
||||
{"knulli": ("Knulli", str(board))}):
|
||||
self.assertEqual(install.derivative_board("knulli"), ("Knulli", "a133"))
|
||||
self.assertIsNone(install.derivative_board("recalbox"))
|
||||
|
||||
def test_the_run_says_why_every_board_is_installed(self):
|
||||
|
||||
@@ -32,6 +32,7 @@ EMULATORS = REPO_ROOT / "emulators"
|
||||
PLATFORM_WORDS = (
|
||||
"retroarch", "batocera", "recalbox", "retrobat", "emudeck", "lakka",
|
||||
"retrodeck", "romm", "rocknix", "retropie", "bizhawk", "mister",
|
||||
"knulli", "amberelec", "emuelec",
|
||||
)
|
||||
|
||||
# Launcher sources: configgen and emulatorLauncher generators, EmuDeck and
|
||||
|
||||
@@ -558,6 +558,9 @@ class UnreadableReleaseStopsTheScrape(unittest.TestCase):
|
||||
"scraper.emudeck_scraper",
|
||||
"scraper.misterfpga_scraper",
|
||||
"scraper.retropie_scraper",
|
||||
"scraper.amberelec_scraper",
|
||||
"scraper.emuelec_scraper",
|
||||
"scraper.knulli_scraper",
|
||||
):
|
||||
module = importlib.import_module(name)
|
||||
with self.subTest(scraper=name), mock.patch(
|
||||
@@ -839,8 +842,11 @@ class OneNativeIdOneSystem(unittest.TestCase):
|
||||
# A platform system no profile names gets no core extra and answers no
|
||||
# --system. Named here: Batocera's family folds Naomi and Atomiswave into
|
||||
# one system the profiles keep apart, and MiSTer cores nothing profiles.
|
||||
# EmuELEC lists Duke Nukem 2's data for RigelEngine, which no profile
|
||||
# describes yet.
|
||||
UNMATCHED = {
|
||||
"*": {"sega-dreamcast-arcade"},
|
||||
"emuelec": {"RigelEngine"},
|
||||
"misterfpga": {"gameking", "interact", "marty", "studio-ii", "superacan",
|
||||
"videobrain"},
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user