mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
feat: keep a replaced BIOS file beside its folder
A file in place whose contents differ (another revision, a personal dump) was overwritten and lost. It is now moved to retrobios-backup/<date-time>/ beside the folder written to, under the same path, before the verified copy replaces it. Beside, not inside: DuckStation, SwanStation and Rustation read every file of the BIOS folder and BizHawk walks its firmware folder recursively. Identical files are never kept. --dry-run counts what would be kept, the summary and the summary event name the folder, files and bytes, and --restore FOLDER puts the files back. Data trees and standalone copies keep their replaced files the same way.
This commit is contained in:
1 parent
2870c72ccb
commit
468b5e1d3e
13 files changed
+695
-41
No files matched your search
+235
-2
@@ -4966,7 +4966,8 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree):
|
||||
installer = events[0]["installer"]
|
||||
self.assertEqual(events, self._head("check", installer) + [
|
||||
{"event": "summary", "mode": "check", "installed": 0, "up_to_date": 1,
|
||||
"pending": 3, "failed": [], "skipped": self.SKIPPED, "left_out": [], "interrupted": False},
|
||||
"pending": 3, "failed": [], "skipped": self.SKIPPED, "left_out": [], "kept": [],
|
||||
"interrupted": False},
|
||||
{"event": "end", "code": 1},
|
||||
])
|
||||
self.assertIn("2/4 present (1 verified, 1 wrong hash)", proc.stderr)
|
||||
@@ -4976,6 +4977,9 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree):
|
||||
proc, events = self._run()
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
installer = events[0]["installer"]
|
||||
kept = events[-2]["kept"]
|
||||
self.assertEqual(len(kept), 1, kept)
|
||||
self.assertEqual(Path(kept[0]["folder"]).parent, self.home / install.BACKUP_DIR)
|
||||
progress = [event for event in events if event["event"] == "progress"]
|
||||
for event in progress:
|
||||
self.assertEqual(list(event), ["event", *install.EVENT_KEYS["progress"]])
|
||||
@@ -4989,7 +4993,9 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree):
|
||||
{"event": "download", "platform": "retroarch", "files": 3, "bytes": 245},
|
||||
*done,
|
||||
{"event": "summary", "mode": "install", "installed": 3, "up_to_date": 1,
|
||||
"pending": 0, "failed": [], "skipped": self.SKIPPED, "left_out": [], "interrupted": False},
|
||||
"pending": 0, "failed": [], "skipped": self.SKIPPED, "left_out": [],
|
||||
"kept": [{"folder": kept[0]["folder"], "files": 1, "bytes": 11}],
|
||||
"interrupted": False},
|
||||
{"event": "end", "code": 0},
|
||||
])
|
||||
self.assertEqual((self.bios / "b.bin").read_bytes(), self.FILES["bios/A/b.bin"])
|
||||
@@ -5031,5 +5037,232 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree):
|
||||
self.assertIn(f"`{key}`", row, f"{kind}.{key}")
|
||||
|
||||
|
||||
class AFileYouHadIsKept(unittest.TestCase):
|
||||
"""A BIOS file whose contents differ was replaced and lost: a personal
|
||||
dump or another revision could not be brought back by deleting what the
|
||||
installer wrote. It is moved beside the BIOS folder first, never inside,
|
||||
where a core that finds its BIOS by content could take it."""
|
||||
|
||||
NEW = b"the collection's copy"
|
||||
OLD = b"a dump of my own"
|
||||
|
||||
def setUp(self):
|
||||
install.KEPT.reset()
|
||||
self.addCleanup(install.KEPT.reset)
|
||||
self.home = Path(_tempdir(self))
|
||||
self.bios = self.home / "system"
|
||||
self.bios.mkdir()
|
||||
|
||||
def _entry(self, dest: str) -> dict:
|
||||
return {"dest": dest, "repo_path": f"bios/{dest}", "size": len(self.NEW),
|
||||
"sha1": hashlib.sha1(self.NEW).hexdigest(),
|
||||
"sha256": hashlib.sha256(self.NEW).hexdigest()}
|
||||
|
||||
def _download(self, dest: str):
|
||||
with unittest.mock.patch.object(install.urllib.request, "urlopen",
|
||||
lambda *a, **k: _FakeResponse(self.NEW)):
|
||||
return install._download_one(self._entry(dest), self.bios)
|
||||
|
||||
def _run_folder(self) -> Path:
|
||||
runs = list((self.home / install.BACKUP_DIR).iterdir())
|
||||
self.assertEqual(len(runs), 1, runs)
|
||||
return runs[0]
|
||||
|
||||
def test_a_differing_file_is_kept_with_its_relative_path(self):
|
||||
(self.bios / "sub").mkdir()
|
||||
(self.bios / "sub" / "x.bin").write_bytes(self.OLD)
|
||||
self.assertEqual(self._download("sub/x.bin"), ("sub/x.bin", True, ""))
|
||||
self.assertEqual((self.bios / "sub" / "x.bin").read_bytes(), self.NEW)
|
||||
run = self._run_folder()
|
||||
self.assertEqual((run / "system" / "sub" / "x.bin").read_bytes(), self.OLD)
|
||||
self.assertEqual(install.KEPT.summary(),
|
||||
[{"folder": str(run), "files": 1, "bytes": len(self.OLD)}])
|
||||
# Nothing new sits inside the BIOS folder, where a scan would read it.
|
||||
self.assertEqual(sorted(p.name for p in self.bios.rglob("*")), ["sub", "x.bin"])
|
||||
|
||||
def test_an_identical_file_is_not_copied(self):
|
||||
(self.bios / "x.bin").write_bytes(self.NEW)
|
||||
self.assertEqual(self._download("x.bin"), ("x.bin", True, ""))
|
||||
self.assertFalse((self.home / install.BACKUP_DIR).exists())
|
||||
self.assertEqual(install.KEPT.summary(), [])
|
||||
|
||||
def test_a_missing_file_keeps_nothing(self):
|
||||
self.assertEqual(self._download("x.bin"), ("x.bin", True, ""))
|
||||
self.assertFalse((self.home / install.BACKUP_DIR).exists())
|
||||
|
||||
def test_a_second_replacement_keeps_the_first_file(self):
|
||||
# Two lists in one run writing one path: what is kept is the user's.
|
||||
(self.bios / "x.bin").write_bytes(self.OLD)
|
||||
self._download("x.bin")
|
||||
(self.bios / "x.bin").write_bytes(b"what this run wrote first")
|
||||
self._download("x.bin")
|
||||
self.assertEqual((self._run_folder() / "system" / "x.bin").read_bytes(), self.OLD)
|
||||
|
||||
def test_a_file_that_cannot_be_kept_stays(self):
|
||||
(self.bios / "x.bin").write_bytes(self.OLD)
|
||||
|
||||
def refuse(*_args):
|
||||
raise PermissionError(13, "Permission denied")
|
||||
|
||||
with unittest.mock.patch.object(install, "_move_aside", refuse):
|
||||
_dest, ok, reason = self._download("x.bin")
|
||||
self.assertFalse(ok)
|
||||
self.assertIn(install.NOT_KEPT, reason)
|
||||
self.assertEqual((self.bios / "x.bin").read_bytes(), self.OLD)
|
||||
self.assertEqual(sorted(p.name for p in self.bios.iterdir()), ["x.bin"])
|
||||
|
||||
def test_a_symlinked_subfolder_keeps_working(self):
|
||||
elsewhere = self.home / "shadPS4" / "sys_modules"
|
||||
elsewhere.mkdir(parents=True)
|
||||
(elsewhere / "lib.sprx").write_bytes(self.OLD)
|
||||
(self.bios / "shadps4").mkdir()
|
||||
(self.bios / "shadps4" / "sys_modules").symlink_to(elsewhere)
|
||||
self.assertTrue(self._download("shadps4/sys_modules/lib.sprx")[1])
|
||||
self.assertEqual((elsewhere / "lib.sprx").read_bytes(), self.NEW)
|
||||
self.assertTrue((self.bios / "shadps4" / "sys_modules").is_symlink())
|
||||
run = self._run_folder()
|
||||
kept = run / "system" / "shadps4" / "sys_modules" / "lib.sprx"
|
||||
self.assertEqual(kept.read_bytes(), self.OLD)
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
self.assertEqual(install.restore_kept(run), 0)
|
||||
self.assertEqual((elsewhere / "lib.sprx").read_bytes(), self.OLD)
|
||||
self.assertTrue((self.bios / "shadps4" / "sys_modules").is_symlink())
|
||||
|
||||
def test_a_link_at_the_name_is_not_kept(self):
|
||||
# The replace takes the link's place: the file it points to is untouched.
|
||||
target = self.home / "mine.bin"
|
||||
target.write_bytes(self.OLD)
|
||||
(self.bios / "x.bin").symlink_to(target)
|
||||
self.assertTrue(self._download("x.bin")[1])
|
||||
self.assertEqual(target.read_bytes(), self.OLD)
|
||||
self.assertFalse((self.home / install.BACKUP_DIR).exists())
|
||||
|
||||
@unittest.skipIf(hasattr(os, "geteuid") and os.geteuid() == 0, "root writes anywhere")
|
||||
def test_without_room_beside_the_folder_it_goes_inside(self):
|
||||
(self.bios / "x.bin").write_bytes(self.OLD)
|
||||
self.home.chmod(0o555)
|
||||
self.addCleanup(self.home.chmod, 0o755)
|
||||
self.assertTrue(self._download("x.bin")[1])
|
||||
runs = list((self.bios / install.BACKUP_DIR).iterdir())
|
||||
self.assertEqual(len(runs), 1)
|
||||
self.assertEqual((runs[0] / "x.bin").read_bytes(), self.OLD)
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
self.assertEqual(install.restore_kept(runs[0]), 0)
|
||||
self.assertEqual((self.bios / "x.bin").read_bytes(), self.OLD)
|
||||
|
||||
def test_a_data_tree_file_is_kept(self):
|
||||
import zipfile
|
||||
|
||||
tree = self.bios / "dolphin-emu" / "Sys"
|
||||
tree.mkdir(parents=True)
|
||||
(tree / "a.ini").write_bytes(self.OLD)
|
||||
archive = self.home / "src.zip"
|
||||
with zipfile.ZipFile(archive, "w") as zf:
|
||||
zf.writestr("Sys/a.ini", self.NEW)
|
||||
entry = {"ref": "d", "dest": "dolphin-emu", "url": "https://example.invalid/d.zip",
|
||||
"type": "zip", "files": {"Sys/a.ini": {
|
||||
"sha1": hashlib.sha1(self.NEW).hexdigest(), "size": len(self.NEW)}}}
|
||||
|
||||
def fetch(url, directory):
|
||||
copy = Path(directory) / "a.zip"
|
||||
shutil.copy(archive, copy)
|
||||
return copy
|
||||
|
||||
with unittest.mock.patch.object(install, "_fetch_archive", fetch):
|
||||
counts = install.install_data_directory(entry, self.bios)
|
||||
self.assertEqual(counts["installed"], 1)
|
||||
self.assertEqual((tree / "a.ini").read_bytes(), self.NEW)
|
||||
kept = self._run_folder() / "system" / "dolphin-emu" / "Sys" / "a.ini"
|
||||
self.assertEqual(kept.read_bytes(), self.OLD)
|
||||
|
||||
def test_a_standalone_copy_keeps_the_emulators_file(self):
|
||||
(self.bios / "ps2-0230a.bin").write_bytes(self.NEW)
|
||||
pcsx2 = self.home / "PCSX2" / "bios"
|
||||
pcsx2.mkdir(parents=True)
|
||||
(pcsx2 / "ps2-0230a.bin").write_bytes(self.OLD)
|
||||
manifest = {"standalone_copies": [
|
||||
{"pattern": "ps2-*.bin", "emulator": "pcsx2", "targets": {"linux": [str(pcsx2)]}}]}
|
||||
self.assertEqual(install.do_standalone_copies(manifest, self.bios, "linux"), (1, 0, 0))
|
||||
self.assertEqual((pcsx2 / "ps2-0230a.bin").read_bytes(), self.NEW)
|
||||
runs = list((self.home / "PCSX2" / install.BACKUP_DIR).iterdir())
|
||||
self.assertEqual((runs[0] / "bios" / "ps2-0230a.bin").read_bytes(), self.OLD)
|
||||
# Copied again, the file is the same: nothing more is kept.
|
||||
self.assertEqual(install.do_standalone_copies(manifest, self.bios, "linux"), (1, 0, 0))
|
||||
self.assertEqual(install.KEPT.summary()[0]["files"], 1)
|
||||
|
||||
def test_a_kept_file_inside_the_tree_is_no_copy_source(self):
|
||||
inside = self.bios / install.BACKUP_DIR / "20261022-153000"
|
||||
inside.mkdir(parents=True)
|
||||
(inside / "ps2-old.bin").write_bytes(self.OLD)
|
||||
pcsx2 = self.home / "PCSX2" / "bios"
|
||||
pcsx2.mkdir(parents=True)
|
||||
manifest = {"standalone_copies": [
|
||||
{"pattern": "ps2-*.bin", "emulator": "pcsx2", "targets": {"linux": [str(pcsx2)]}}]}
|
||||
self.assertEqual(install.do_standalone_copies(manifest, self.bios, "linux"), (0, 0, 0))
|
||||
|
||||
def test_restore_refuses_a_folder_that_is_no_backup(self):
|
||||
with contextlib.redirect_stderr(io.StringIO()) as err:
|
||||
self.assertEqual(install.restore_kept(self.bios), 2)
|
||||
self.assertIn(install.BACKUP_DIR, err.getvalue())
|
||||
|
||||
|
||||
class KeptFilesEndToEnd(unittest.TestCase, _SyntheticTree):
|
||||
"""The run says what it kept and where, --dry-run what it would keep
|
||||
and writes nothing, and --restore puts the original bytes back."""
|
||||
|
||||
def setUp(self):
|
||||
self.env = self.serve_tree(self)
|
||||
|
||||
def test_dry_run_says_what_would_be_kept_and_writes_nothing(self):
|
||||
proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(self.bios),
|
||||
"--dry-run", "--no-input")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertIn("Would keep 1 file you have (11 B)", proc.stdout)
|
||||
self.assertIn(str(self.home / install.BACKUP_DIR), proc.stdout)
|
||||
self.assertFalse((self.home / install.BACKUP_DIR).exists())
|
||||
self.assertEqual((self.bios / "b.bin").read_bytes(), b"other bytes")
|
||||
events = _events(_install_run(self.env, "--platform", "retroarch", "--dest",
|
||||
str(self.bios), "--dry-run", "--events"))
|
||||
kept = events[-2]["kept"]
|
||||
self.assertEqual([(item["files"], item["bytes"]) for item in kept], [(1, 11)])
|
||||
self.assertFalse((self.home / install.BACKUP_DIR).exists())
|
||||
|
||||
def test_install_then_restore(self):
|
||||
proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(self.bios),
|
||||
"--no-input", "--jobs", "1")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual((self.bios / "b.bin").read_bytes(), self.FILES["bios/A/b.bin"])
|
||||
runs = list((self.home / install.BACKUP_DIR).iterdir())
|
||||
self.assertEqual(len(runs), 1)
|
||||
self.assertIn("Kept 1 file you had (11 B), which the collection's copies "
|
||||
f"replaced, in {runs[0]}", proc.stdout)
|
||||
self.assertIn(f"--restore {runs[0]}", proc.stdout)
|
||||
# a.bin held the expected bytes: it is not among the kept files.
|
||||
self.assertEqual([p.name for p in (runs[0] / "bios").iterdir()], ["b.bin"])
|
||||
|
||||
refused = _install_run(self.env, "--restore", str(runs[0]), "--platform", "retroarch")
|
||||
self.assertEqual(refused.returncode, 2)
|
||||
self.assertIn("--restore", refused.stderr)
|
||||
|
||||
looked = _install_run(self.env, "--restore", str(runs[0]), "--dry-run")
|
||||
self.assertEqual(looked.returncode, 0, looked.stderr)
|
||||
self.assertIn(f"would put back {self.bios / 'b.bin'}", looked.stdout)
|
||||
self.assertEqual((self.bios / "b.bin").read_bytes(), self.FILES["bios/A/b.bin"])
|
||||
|
||||
back = _install_run(self.env, "--restore", str(runs[0]))
|
||||
self.assertEqual(back.returncode, 0, back.stdout + back.stderr)
|
||||
self.assertEqual((self.bios / "b.bin").read_bytes(), b"other bytes")
|
||||
self.assertEqual((self.bios / "a.bin").read_bytes(), self.FILES["bios/A/a.bin"])
|
||||
self.assertIn("Put back 1 file", back.stdout)
|
||||
# The backup stays until the user deletes it.
|
||||
self.assertTrue((runs[0] / "bios" / "b.bin").is_file())
|
||||
|
||||
# A second install keeps the restored file in a new folder.
|
||||
again = _install_run(self.env, "--platform", "retroarch", "--dest", str(self.bios),
|
||||
"--no-input", "--jobs", "1")
|
||||
self.assertEqual(again.returncode, 0, again.stderr)
|
||||
self.assertEqual(len(list((self.home / install.BACKUP_DIR).iterdir())), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user