Files
libretro/tests/test_install_cores.py
T

298 lines
13 KiB
Python

"""The one-liner narrows to the cores a RetroArch already has.
RetroArch keeps its cores in libretro_directory, named the buildbot's way
(`mednafen_psx_libretro.so`), while a file in the manifest names the profile
that reads it (`beetle_psx`). The manifest maps one to the other and says
which cores keep each system, the rule a pack built for a target follows.
"""
from __future__ import annotations
import contextlib
import io
import json
import shlex
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(REPO_ROOT / "tests"))
import install # noqa: E402
from test_install_selection import FakeRepository # noqa: E402
class CoreDirectories(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(lambda: __import__("shutil").rmtree(self.tmp, ignore_errors=True))
def test_the_config_detection_read_names_the_cores(self):
cfg = self.tmp / "retroarch.cfg"
cfg.write_text(
f'system_directory = "{self.tmp / "sys"}"\n'
f'libretro_directory = "{self.tmp / "my cores"}"\n'
)
system_dir = install._parse_retroarch_system_dir(cfg)
self.assertEqual(
install.core_dirs("retroarch", system_dir), [self.tmp / "my cores"]
)
def test_a_portable_layout_defaults_to_cores_beside_the_config(self):
(self.tmp / "retroarch.cfg").write_text('libretro_directory = "default"\n')
self.assertEqual(
install.core_dirs("retroarch", self.tmp / "system"), [self.tmp / "cores"]
)
(self.tmp / "retroarch.cfg").write_text('video_driver = "gl"\n')
self.assertEqual(
install.core_dirs("retroarch", self.tmp / "system"), [self.tmp / "cores"]
)
def test_retropie_and_lakka_keep_their_own_folders(self):
self.assertIn(
Path("/opt/retropie/libretrocores"),
install.core_dirs("retropie", self.tmp / "none"),
)
self.assertIn(Path("/storage/cores"), install.core_dirs("lakka", self.tmp / "none"))
def test_core_files_of_every_system_and_retropie_folders(self):
cores = self.tmp / "cores"
(cores / "lr-fceumm").mkdir(parents=True)
for name in (
"mednafen_psx_libretro.so", "snes9x_libretro.dll",
"mgba_libretro.dylib", "genesis_plus_gx_libretro_android.so",
"lr-fceumm/fceumm_libretro.so", "mgba_libretro.info", "readme.txt",
):
(cores / name).write_bytes(b"")
self.assertEqual(
install.installed_cores([cores, self.tmp / "missing"]),
["fceumm", "genesis_plus_gx", "mednafen_psx", "mgba", "snes9x"],
)
MANIFEST = {
"core_names": {"beetle_psx": ["mednafen_psx"]},
"system_cores": {"sony-playstation": ["beetle_psx", "pcsx_rearmed"],
"nintendo-gba": ["mgba"]},
"files": [
{"dest": "scph5501.bin", "cores": None, "systems": ["sony-playstation"]},
{"dest": "gba_bios.bin", "cores": None, "systems": ["nintendo-gba"]},
{"dest": "unlisted.bin", "cores": None, "systems": ["arcade"]},
{"dest": "psx/extra.bin", "cores": ["beetle_psx"], "systems": ["sony-playstation"]},
{"dest": "mgba/extra.bin", "cores": ["mgba"], "systems": ["nintendo-gba"]},
],
"omitted_files": [
{"dest": "gone.bin", "cores": ["mgba"], "systems": ["nintendo-gba"]},
],
}
class NarrowToCores(unittest.TestCase):
def test_a_buildbot_name_keeps_its_profile_and_its_systems(self):
files, omitted = install.narrow_to_cores(MANIFEST, ["mednafen_psx"])
self.assertEqual(
[f["dest"] for f in files],
["scph5501.bin", "unlisted.bin", "psx/extra.bin"],
)
self.assertEqual(omitted, [])
def test_an_older_list_keeps_the_platform_files(self):
older = {k: v for k, v in MANIFEST.items() if k in ("files", "omitted_files")}
files, _ = install.narrow_to_cores(older, ["mgba"])
self.assertEqual(
[f["dest"] for f in files],
["scph5501.bin", "gba_bios.bin", "unlisted.bin", "mgba/extra.bin"],
)
class TheBuildersOrder(unittest.TestCase):
"""RetroPie reads megaplay.zip as advancemame (copy A), mame (copy B),
then mame2003 (copy A). Without advancemame the builder ships mame's
copy; merging mame2003 into the first copy gave it A."""
ENTRY = {
"dest": "megaplay.zip", "sha1": "a" * 40, "size": 1,
"repo_path": "bios/a.zip", "cores": ["advancemame"], "systems": ["arcade"],
"alternatives": [
{"sha1": "b" * 40, "sha256": "", "size": 2, "repo_path": "bios/b.zip",
"cores": ["mame"]},
{"sha1": "a" * 40, "sha256": "", "size": 1, "repo_path": "bios/a.zip",
"cores": ["mame2003"]},
],
}
def test_the_first_chosen_claim_wins(self):
manifest = {"files": [self.ENTRY], "omitted_files": []}
for names, sha in ((["mame", "mame2003"], "b"), (["mame2003"], "a"),
(["advancemame", "mame"], "a")):
with self.subTest(names=names):
files, _ = install.narrow_to_cores(manifest, names)
self.assertEqual(files[0]["sha1"], sha * 40)
self.assertNotIn("alternatives", files[0] if sha == "b" else {})
def test_the_parity_covers_a_core_set(self):
"""A real build for mame and mame2003 on RetroPie, the shape
--installed-cores produces there."""
db_path = REPO_ROOT / "database.json"
if not db_path.exists():
self.skipTest("database.json is not built")
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import generate_pack as gp
from common import load_emulator_profiles
db = json.loads(db_path.read_text(encoding="utf-8"))
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
def manifest(cores):
return gp.generate_manifest(
"retropie", str(REPO_ROOT / "platforms"), db, str(REPO_ROOT / "bios"),
str(REPO_ROOT / "platforms" / "_registry.yml"),
emulators_dir=str(REPO_ROOT / "emulators"), emu_profiles=profiles,
target_cores=cores, offline=True,
)
names = ["mame", "mame2003"]
files, _ = install.narrow_to_cores(manifest(None), names)
built = manifest(set(names))
self.assertEqual(
{(f["dest"], f["sha1"]) for f in files},
{(f["dest"], f["sha1"]) for f in built["files"]},
)
class TheBoundaryBoundsAlternatives(unittest.TestCase):
def _manifest(self, alternative: dict) -> dict:
entry = {"dest": "a.bin", "sha1": "a" * 40, "size": 1,
"repo_path": "bios/a.bin", "cores": ["x"],
"alternatives": [alternative]}
return {"manifest_version": 2, "platform": "retroarch", "files": [entry]}
def test_an_alternative_carries_only_a_copy(self):
good = {"sha1": "b" * 40, "size": 1, "repo_path": "bios/b.bin", "cores": ["y"]}
install._validate_manifest(self._manifest(good), "retroarch")
for extra in ({"dest": "../../evil"}, {"regions": 5}, {"cores": []},
{"repo_path": "../x"}):
with self.subTest(extra=extra), self.assertRaises(ValueError):
install._validate_manifest(self._manifest({**good, **extra}), "retroarch")
def test_the_cap_counts_the_largest_copy(self):
big = {"sha1": "b" * 40, "size": install.MAX_DOWNLOAD_SIZE,
"repo_path": "bios/b.bin", "cores": ["y"]}
manifest = self._manifest(big)
manifest["files"] = [
{**manifest["files"][0], "dest": f"f{i}.bin"} for i in range(70)
]
with self.assertRaises(ValueError):
install._validate_manifest(manifest, "retroarch")
class RefusedBeforeAnyInstall(unittest.TestCase):
def test_a_platform_shipping_its_cores_stops_the_run_first(self):
tmp = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, tmp, True)
detected = [("retroarch", tmp / "system"), ("batocera", tmp / "bios")]
(tmp / "retroarch.cfg").write_text("")
(tmp / "cores").mkdir()
(tmp / "cores" / "mgba_libretro.so").write_bytes(b"")
with mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.object(install, "detect_frontends", return_value=[]), \
mock.patch.object(install, "detect_platforms", return_value=detected), \
mock.patch.object(install, "fetch_manifest") as fetch, \
mock.patch.object(sys, "argv", ["install.py", "--installed-cores", "--no-input"]), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()) as err:
with self.assertRaises(SystemExit) as stop:
install.main()
self.assertEqual(stop.exception.code, 1)
self.assertIn("batocera ships its cores", err.getvalue())
fetch.assert_not_called()
class _CoreRepository(FakeRepository):
"""The fake repository with the two maps and a portable RetroArch."""
def __enter__(self):
super().__enter__()
path = self.root / "install" / "retroarch.json"
manifest = json.loads(path.read_text())
manifest["core_names"] = {"pcsx_rearmed": ["pcsx_rearmed_neon"]}
manifest["system_cores"] = {
"sony-playstation": ["pcsx_rearmed"], "nintendo-gba": ["mgba"],
}
path.write_text(json.dumps(manifest))
(self.root / "retroarch.cfg").write_text('libretro_directory = ":/cores"\n')
(self.root / "cores").mkdir()
return self
def install_core(self, name: str) -> None:
(self.root / "cores" / f"{name}_libretro.so").write_bytes(b"")
class TheOneLinerReadsInstalledCores(unittest.TestCase):
def test_only_what_the_installed_cores_read(self):
with _CoreRepository() as repo:
repo.install_core("pcsx_rearmed_neon")
proc = repo.run("--installed-cores")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("1 core installed", proc.stdout)
self.assertEqual(
repo.installed(), ["pcsx/extra.bin", "scph5500.bin", "scph5501.bin"]
)
def test_no_core_found_is_an_error_not_everything(self):
with _CoreRepository() as repo:
proc = repo.run("--installed-cores")
self.assertEqual(proc.returncode, 1)
self.assertIn("no installed core found", proc.stderr)
self.assertEqual(repo.installed(), [])
def test_an_older_file_list_is_refused(self):
with FakeRepository() as repo:
(repo.root / "retroarch.cfg").write_text("")
(repo.root / "cores").mkdir()
(repo.root / "cores" / "mgba_libretro.so").write_bytes(b"")
proc = repo.run("--installed-cores")
self.assertEqual(proc.returncode, 1)
self.assertIn("needs a newer one", proc.stderr)
def test_a_platform_that_ships_its_cores_refuses_the_flag(self):
with _CoreRepository() as repo:
batocera = repo.root / "install" / "batocera.json"
manifest = json.loads((repo.root / "install" / "retroarch.json").read_text())
manifest["platform"] = "batocera"
batocera.write_text(json.dumps(manifest))
command = repo.command("--installed-cores")
command[command.index("retroarch")] = "batocera"
proc = subprocess.run(
command, env=repo.env(), capture_output=True, text=True,
timeout=60, check=False, stdin=subprocess.DEVNULL,
)
self.assertEqual(proc.returncode, 1)
self.assertIn("ships its cores with the system", proc.stderr)
@unittest.skipUnless(shutil.which("script"), "script(1) provides the terminal")
def test_the_prompt_offers_the_installed_cores(self):
with _CoreRepository() as repo:
repo.install_core("mgba")
command = " ".join(shlex.quote(part) for part in repo.command())
proc = subprocess.run(
["script", "-qec", command, "/dev/null"],
input="i\n", env=repo.env(), capture_output=True,
text=True, timeout=60, check=False,
)
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("i) only what the installed cores read: 2 files", proc.stdout)
self.assertIn("--platform retroarch --installed-cores", proc.stdout)
self.assertEqual(repo.installed(), ["gba_bios.bin", "mgba/extra.bin"])
if __name__ == "__main__":
unittest.main()