Files
libretro/tests/test_install_filesystems.py
T
Abdessamad Derraz 36745abf10 feat: line mode says details only with --verbose
By default a run prints what it installs, the BIOS folder, the progress
at a terminal, the result and the next step. The steps, the per-file log
lines, the counts and the reasons a file is left out come back with
--verbose. --check gives the count and the folder, and lists each file
with --verbose. The installer page shows the default run and walks
through the --verbose one.
2026-11-10 00:21:37 +01:00

136 lines
5.9 KiB
Python

"""What an install does for the device and file system it lands on."""
from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import contextlib
import errno
import hashlib
import io
import json
import os
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 AndroidMediaScanner(unittest.TestCase):
"""Without a .nomedia the BIOS folder's samples, overlays and videos
showed in the phone's gallery and music apps."""
def setUp(self):
self.repo = FakeRepository().__enter__()
self.addCleanup(self.repo.__exit__)
def _run(self, os_name: str, *options: str):
proc = subprocess.run(self.repo.command("--yes", "--no-input", *options),
env={**self.repo.env(), "RETROBIOS_OS": os_name},
capture_output=True, text=True, timeout=60,
stdin=subprocess.DEVNULL)
return self.repo, proc
def test_an_android_install_writes_one(self):
repo, proc = self._run("android", "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
marker = repo.dest / ".nomedia"
self.assertTrue(marker.is_file())
self.assertEqual(marker.stat().st_size, 0)
self.assertIn("Added .nomedia", proc.stdout)
def test_a_second_run_leaves_it_and_says_nothing(self):
self._run("android")
_, proc = self._run("android")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertNotIn("Added .nomedia", proc.stdout)
# The marker is not a file of the list: the check finds all in place.
_, check = self._run("android", "--check")
self.assertEqual(check.returncode, 0, check.stdout + check.stderr)
def test_a_check_and_a_dry_run_write_nothing(self):
for options in (("--check",), ("--dry-run",)):
with self.subTest(options=options):
self._run("android", *options)
self.assertFalse((self.repo.dest / ".nomedia").exists())
def test_other_systems_get_none(self):
repo, proc = self._run("linux")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertFalse((repo.dest / ".nomedia").exists())
class NamesAFatCardCannotHold(unittest.TestCase):
"""RetroDECK names three files with a colon (zxs:48.rom): on an exFAT
card each write failed with EINVAL, on every run."""
def test_the_probe_reads_einval_as_a_refusal(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
refused = OSError(errno.EINVAL, "Invalid argument")
closed = PermissionError(errno.EACCES, "Permission denied")
with mock.patch.object(install.os, "name", "posix"):
self.assertTrue(install.holds_fat_refused_names(Path(tmp) / "a" / "b"))
with mock.patch.object(install.tempfile, "mkstemp", side_effect=refused):
self.assertFalse(install.holds_fat_refused_names(Path(tmp)))
with mock.patch.object(install.tempfile, "mkstemp", side_effect=closed):
self.assertTrue(install.holds_fat_refused_names(Path(tmp)))
self.assertEqual(os.listdir(tmp), [])
def test_windows_refuses_them_on_any_drive(self):
with mock.patch.object(install.os, "name", "nt"):
self.assertFalse(install.holds_fat_refused_names(Path(".")))
def _install(self, holds: bool) -> tuple[str, list[str]]:
root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, root, True)
old, dest = root / "old", root / "dest"
old.mkdir()
files = []
for name, payload in (("zxs:48.rom", b"zx 48"), ("gba_bios.bin", b"gba")):
(old / name.replace(":", "_")).write_bytes(payload)
files.append({"dest": name, "sha1": hashlib.sha1(payload).hexdigest(),
"sha256": hashlib.sha256(payload).hexdigest(),
"size": len(payload), "repo_path": f"bios/{name}"})
listed = root / "list.json"
listed.write_text(json.dumps({"manifest_version": 2, "platform": "retrodeck",
"files": files}))
out = io.StringIO()
argv = ["install.py", "--platform", "retrodeck", "--dest", str(dest),
"--manifest", str(listed), "--from", str(old), "--no-input", "--verbose"]
with mock.patch.object(install, "holds_fat_refused_names", return_value=holds), \
mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch("sys.argv", argv), contextlib.redirect_stdout(out), \
contextlib.redirect_stderr(io.StringIO()):
try:
install.main()
except SystemExit as exc:
self.assertIn(exc.code, (0, None), out.getvalue())
self.addCleanup(install.LOCAL.reset)
return (" ".join(out.getvalue().split()),
sorted(p.name for p in dest.iterdir()) if dest.is_dir() else [])
def test_the_names_are_left_out_and_said(self):
said, installed = self._install(holds=False)
self.assertEqual(installed, ["gba_bios.bin"])
self.assertIn("1 file left out: the file system of", said)
self.assertIn('cannot hold a name with any of < > : " | ? * (zxs:48.rom)', said)
def test_a_folder_that_holds_them_gets_them(self):
said, installed = self._install(holds=True)
self.assertEqual(installed, ["gba_bios.bin", "zxs:48.rom"])
self.assertNotIn("left out", said)
if __name__ == "__main__":
unittest.main()