mirror of
https://github.com/Abdess/retrobios.git
synced 2026-06-26 12:52:48 +00:00
239 lines
8.2 KiB
Python
239 lines
8.2 KiB
Python
|
|
"""Tests for pack generation logic in generate_pack.py."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import hashlib
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
import zipfile
|
||
|
|
|
||
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
|
||
|
|
from common import compute_hashes
|
||
|
|
from generate_pack import build_zip_contents_index
|
||
|
|
|
||
|
|
|
||
|
|
class TestBuildZipContentsIndex(unittest.TestCase):
|
||
|
|
"""Test build_zip_contents_index: maps inner ROM MD5 to container SHA1."""
|
||
|
|
|
||
|
|
def setUp(self):
|
||
|
|
self.tmpdir = tempfile.mkdtemp()
|
||
|
|
self.inner_content = b"inner rom data for index test"
|
||
|
|
self.inner_md5 = hashlib.md5(self.inner_content).hexdigest()
|
||
|
|
|
||
|
|
self.zip_path = os.path.join(self.tmpdir, "container.zip")
|
||
|
|
with zipfile.ZipFile(self.zip_path, "w") as zf:
|
||
|
|
zf.writestr("rom.bin", self.inner_content)
|
||
|
|
|
||
|
|
hashes = compute_hashes(self.zip_path)
|
||
|
|
self.zip_sha1 = hashes["sha1"]
|
||
|
|
self.zip_md5 = hashes["md5"]
|
||
|
|
|
||
|
|
self.db = {
|
||
|
|
"files": {
|
||
|
|
self.zip_sha1: {
|
||
|
|
"path": self.zip_path,
|
||
|
|
"name": "container.zip",
|
||
|
|
"md5": self.zip_md5,
|
||
|
|
"size": os.path.getsize(self.zip_path),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"indexes": {
|
||
|
|
"by_md5": {self.zip_md5: self.zip_sha1},
|
||
|
|
"by_name": {"container.zip": [self.zip_sha1]},
|
||
|
|
"by_crc32": {},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
def tearDown(self):
|
||
|
|
import shutil
|
||
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
||
|
|
|
||
|
|
def test_inner_md5_maps_to_container_sha1(self):
|
||
|
|
index = build_zip_contents_index(self.db)
|
||
|
|
self.assertIn(self.inner_md5, index)
|
||
|
|
self.assertEqual(index[self.inner_md5], self.zip_sha1)
|
||
|
|
|
||
|
|
def test_non_zip_files_skipped(self):
|
||
|
|
"""Non-ZIP files in db don't appear in index."""
|
||
|
|
plain_path = os.path.join(self.tmpdir, "plain.bin")
|
||
|
|
with open(plain_path, "wb") as f:
|
||
|
|
f.write(b"not a zip")
|
||
|
|
hashes = compute_hashes(plain_path)
|
||
|
|
self.db["files"][hashes["sha1"]] = {
|
||
|
|
"path": plain_path,
|
||
|
|
"name": "plain.bin",
|
||
|
|
"md5": hashes["md5"],
|
||
|
|
"size": 9,
|
||
|
|
}
|
||
|
|
index = build_zip_contents_index(self.db)
|
||
|
|
# Only the inner_md5 from the ZIP should be present
|
||
|
|
self.assertEqual(len(index), 1)
|
||
|
|
|
||
|
|
def test_missing_file_skipped(self):
|
||
|
|
"""ZIP path that doesn't exist on disk is skipped."""
|
||
|
|
self.db["files"]["fake_sha1"] = {
|
||
|
|
"path": "/nonexistent/file.zip",
|
||
|
|
"name": "file.zip",
|
||
|
|
"md5": "a" * 32,
|
||
|
|
"size": 0,
|
||
|
|
}
|
||
|
|
index = build_zip_contents_index(self.db)
|
||
|
|
self.assertEqual(len(index), 1)
|
||
|
|
|
||
|
|
def test_bad_zip_skipped(self):
|
||
|
|
"""Corrupt ZIP file is skipped without error."""
|
||
|
|
bad_path = os.path.join(self.tmpdir, "bad.zip")
|
||
|
|
with open(bad_path, "wb") as f:
|
||
|
|
f.write(b"corrupt data")
|
||
|
|
hashes = compute_hashes(bad_path)
|
||
|
|
self.db["files"][hashes["sha1"]] = {
|
||
|
|
"path": bad_path,
|
||
|
|
"name": "bad.zip",
|
||
|
|
"md5": hashes["md5"],
|
||
|
|
"size": 12,
|
||
|
|
}
|
||
|
|
index = build_zip_contents_index(self.db)
|
||
|
|
self.assertEqual(len(index), 1)
|
||
|
|
|
||
|
|
|
||
|
|
class TestFileStatusAggregation(unittest.TestCase):
|
||
|
|
"""Test worst-status-wins logic for pack file aggregation."""
|
||
|
|
|
||
|
|
def test_worst_status_wins(self):
|
||
|
|
"""Simulate the worst-status-wins dict pattern from generate_pack."""
|
||
|
|
sev_order = {"ok": 0, "untested": 1, "missing": 2}
|
||
|
|
file_status = {}
|
||
|
|
|
||
|
|
def update_status(dest, status):
|
||
|
|
prev = file_status.get(dest)
|
||
|
|
if prev is None or sev_order.get(status, 0) > sev_order.get(prev, 0):
|
||
|
|
file_status[dest] = status
|
||
|
|
|
||
|
|
update_status("system/bios.bin", "ok")
|
||
|
|
update_status("system/bios.bin", "missing")
|
||
|
|
self.assertEqual(file_status["system/bios.bin"], "missing")
|
||
|
|
|
||
|
|
update_status("system/other.bin", "untested")
|
||
|
|
update_status("system/other.bin", "ok")
|
||
|
|
self.assertEqual(file_status["system/other.bin"], "untested")
|
||
|
|
|
||
|
|
def test_dedup_same_destination_packed_once(self):
|
||
|
|
"""Same destination from multiple systems: only first is packed."""
|
||
|
|
seen = set()
|
||
|
|
packed = []
|
||
|
|
entries = [
|
||
|
|
{"dest": "shared/bios.bin", "source": "sys1"},
|
||
|
|
{"dest": "shared/bios.bin", "source": "sys2"},
|
||
|
|
{"dest": "unique/other.bin", "source": "sys3"},
|
||
|
|
]
|
||
|
|
for e in entries:
|
||
|
|
if e["dest"] in seen:
|
||
|
|
continue
|
||
|
|
seen.add(e["dest"])
|
||
|
|
packed.append(e["dest"])
|
||
|
|
self.assertEqual(len(packed), 2)
|
||
|
|
self.assertIn("shared/bios.bin", packed)
|
||
|
|
self.assertIn("unique/other.bin", packed)
|
||
|
|
|
||
|
|
|
||
|
|
class TestEmuDeckNoDestination(unittest.TestCase):
|
||
|
|
"""EmuDeck entries with no destination are counted as checks."""
|
||
|
|
|
||
|
|
def test_no_destination_counted_as_check(self):
|
||
|
|
"""EmuDeck-style entries (md5 whitelist, no filename) are tracked."""
|
||
|
|
file_status = {}
|
||
|
|
# Simulate generate_pack logic for empty dest
|
||
|
|
sys_id = "psx"
|
||
|
|
name = ""
|
||
|
|
md5 = "abc123"
|
||
|
|
by_md5 = {"abc123": "sha1_match"}
|
||
|
|
|
||
|
|
dest = "" # empty destination
|
||
|
|
if not dest:
|
||
|
|
fkey = f"{sys_id}/{name}"
|
||
|
|
if md5 and md5 in by_md5:
|
||
|
|
file_status.setdefault(fkey, "ok")
|
||
|
|
else:
|
||
|
|
file_status[fkey] = "missing"
|
||
|
|
|
||
|
|
self.assertIn("psx/", file_status)
|
||
|
|
self.assertEqual(file_status["psx/"], "ok")
|
||
|
|
|
||
|
|
def test_no_destination_missing(self):
|
||
|
|
file_status = {}
|
||
|
|
sys_id = "psx"
|
||
|
|
name = ""
|
||
|
|
md5 = "abc123"
|
||
|
|
by_md5 = {}
|
||
|
|
|
||
|
|
dest = ""
|
||
|
|
if not dest:
|
||
|
|
fkey = f"{sys_id}/{name}"
|
||
|
|
if md5 and md5 in by_md5:
|
||
|
|
file_status.setdefault(fkey, "ok")
|
||
|
|
else:
|
||
|
|
file_status[fkey] = "missing"
|
||
|
|
|
||
|
|
self.assertEqual(file_status["psx/"], "missing")
|
||
|
|
|
||
|
|
|
||
|
|
class TestUserProvidedEntries(unittest.TestCase):
|
||
|
|
"""Test user_provided storage handling."""
|
||
|
|
|
||
|
|
def test_user_provided_creates_instruction_file(self):
|
||
|
|
"""Simulate user_provided entry packing logic."""
|
||
|
|
tmpdir = tempfile.mkdtemp()
|
||
|
|
try:
|
||
|
|
zip_path = os.path.join(tmpdir, "test_pack.zip")
|
||
|
|
with zipfile.ZipFile(zip_path, "w") as zf:
|
||
|
|
entry = {
|
||
|
|
"name": "PS3UPDAT.PUP",
|
||
|
|
"storage": "user_provided",
|
||
|
|
"instructions": "Download from sony.com",
|
||
|
|
}
|
||
|
|
instr_name = f"INSTRUCTIONS_{entry['name']}.txt"
|
||
|
|
zf.writestr(instr_name, f"File needed: {entry['name']}\n\n{entry['instructions']}\n")
|
||
|
|
|
||
|
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
||
|
|
names = zf.namelist()
|
||
|
|
self.assertIn("INSTRUCTIONS_PS3UPDAT.PUP.txt", names)
|
||
|
|
content = zf.read("INSTRUCTIONS_PS3UPDAT.PUP.txt").decode()
|
||
|
|
self.assertIn("PS3UPDAT.PUP", content)
|
||
|
|
self.assertIn("sony.com", content)
|
||
|
|
finally:
|
||
|
|
import shutil
|
||
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
||
|
|
|
||
|
|
|
||
|
|
class TestZippedFileHashMismatch(unittest.TestCase):
|
||
|
|
"""Test zipped_file with hash_mismatch triggers check_inside_zip."""
|
||
|
|
|
||
|
|
def setUp(self):
|
||
|
|
self.tmpdir = tempfile.mkdtemp()
|
||
|
|
self.inner_content = b"correct inner rom"
|
||
|
|
self.inner_md5 = hashlib.md5(self.inner_content).hexdigest()
|
||
|
|
self.zip_path = os.path.join(self.tmpdir, "game.zip")
|
||
|
|
with zipfile.ZipFile(self.zip_path, "w") as zf:
|
||
|
|
zf.writestr("rom.bin", self.inner_content)
|
||
|
|
|
||
|
|
def tearDown(self):
|
||
|
|
import shutil
|
||
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
||
|
|
|
||
|
|
def test_hash_mismatch_zip_inner_ok(self):
|
||
|
|
"""hash_mismatch on container, but inner ROM MD5 matches."""
|
||
|
|
from verify import check_inside_zip, Status
|
||
|
|
result = check_inside_zip(self.zip_path, "rom.bin", self.inner_md5)
|
||
|
|
self.assertEqual(result, Status.OK)
|
||
|
|
|
||
|
|
def test_hash_mismatch_zip_inner_not_found(self):
|
||
|
|
from verify import check_inside_zip
|
||
|
|
result = check_inside_zip(self.zip_path, "missing.bin", self.inner_md5)
|
||
|
|
self.assertEqual(result, "not_in_zip")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|