mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-04-13 12:22:33 -05:00
Moved shared functions to common.py (single source of truth): - check_inside_zip (was in verify.py, imported by generate_pack) - build_zip_contents_index (was duplicated in verify + generate_pack) - load_emulator_profiles (was in verify, cross_reference, generate_site) - group_identical_platforms (was in verify + generate_pack) Added tests/ with 83 unit tests covering: - resolve_local_file: SHA1, MD5, name, alias, truncated, zip_contents - verify: existence, md5, zipped_file, multi-hash, severity mapping - aliases: field parsing, by_name indexing, beetle_psx field rename - pack: dedup, file_status, zipped_file inner check, EmuDeck entries - severity: all 12 combinations, platform-native behavior 0 regressions: pipeline.py --all produces identical results.
167 lines
6.2 KiB
Python
167 lines
6.2 KiB
Python
"""Tests for resolve_local_file from common.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 resolve_local_file, compute_hashes, md5_composite
|
|
|
|
|
|
class TestResolveLocalFile(unittest.TestCase):
|
|
"""Test resolve_local_file resolution chain."""
|
|
|
|
def setUp(self):
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
# Create a fake BIOS file
|
|
self.bios_content = b"fake bios data for testing"
|
|
self.bios_path = os.path.join(self.tmpdir, "bios.bin")
|
|
with open(self.bios_path, "wb") as f:
|
|
f.write(self.bios_content)
|
|
hashes = compute_hashes(self.bios_path)
|
|
self.sha1 = hashes["sha1"]
|
|
self.md5 = hashes["md5"]
|
|
self.crc32 = hashes["crc32"]
|
|
|
|
# Create a second file in .variants/
|
|
self.variant_path = os.path.join(self.tmpdir, ".variants", "bios.bin.abcd1234")
|
|
os.makedirs(os.path.dirname(self.variant_path), exist_ok=True)
|
|
self.variant_content = b"variant bios data"
|
|
with open(self.variant_path, "wb") as f:
|
|
f.write(self.variant_content)
|
|
variant_hashes = compute_hashes(self.variant_path)
|
|
self.variant_sha1 = variant_hashes["sha1"]
|
|
self.variant_md5 = variant_hashes["md5"]
|
|
|
|
# Create a ZIP file with an inner ROM
|
|
self.zip_path = os.path.join(self.tmpdir, "game.zip")
|
|
self.inner_content = b"inner rom data"
|
|
self.inner_md5 = hashlib.md5(self.inner_content).hexdigest()
|
|
with zipfile.ZipFile(self.zip_path, "w") as zf:
|
|
zf.writestr("rom.bin", self.inner_content)
|
|
zip_hashes = compute_hashes(self.zip_path)
|
|
self.zip_sha1 = zip_hashes["sha1"]
|
|
self.zip_md5 = zip_hashes["md5"]
|
|
|
|
# Build a minimal database
|
|
self.db = {
|
|
"files": {
|
|
self.sha1: {
|
|
"path": self.bios_path,
|
|
"name": "bios.bin",
|
|
"md5": self.md5,
|
|
"size": len(self.bios_content),
|
|
},
|
|
self.variant_sha1: {
|
|
"path": self.variant_path,
|
|
"name": "bios.bin",
|
|
"md5": self.variant_md5,
|
|
"size": len(self.variant_content),
|
|
},
|
|
self.zip_sha1: {
|
|
"path": self.zip_path,
|
|
"name": "game.zip",
|
|
"md5": self.zip_md5,
|
|
"size": os.path.getsize(self.zip_path),
|
|
},
|
|
},
|
|
"indexes": {
|
|
"by_md5": {
|
|
self.md5: self.sha1,
|
|
self.variant_md5: self.variant_sha1,
|
|
self.zip_md5: self.zip_sha1,
|
|
},
|
|
"by_name": {
|
|
"bios.bin": [self.sha1, self.variant_sha1],
|
|
"game.zip": [self.zip_sha1],
|
|
"alias.bin": [self.sha1],
|
|
},
|
|
"by_crc32": {
|
|
self.crc32: self.sha1,
|
|
},
|
|
},
|
|
}
|
|
|
|
def tearDown(self):
|
|
import shutil
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
def test_sha1_exact_match(self):
|
|
entry = {"sha1": self.sha1, "name": "bios.bin"}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertEqual(status, "exact")
|
|
self.assertEqual(path, self.bios_path)
|
|
|
|
def test_md5_direct_match(self):
|
|
entry = {"md5": self.md5, "name": "something_else.bin"}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertEqual(status, "md5_exact")
|
|
self.assertEqual(path, self.bios_path)
|
|
|
|
def test_name_match_no_md5(self):
|
|
"""No MD5 provided: resolve by name from by_name index."""
|
|
entry = {"name": "bios.bin"}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertEqual(status, "exact")
|
|
# Primary (non-.variants/) path preferred
|
|
self.assertEqual(path, self.bios_path)
|
|
|
|
def test_alias_match_no_md5(self):
|
|
"""Alias name in by_name index resolves the file."""
|
|
entry = {"name": "unknown.bin", "aliases": ["alias.bin"]}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertEqual(status, "exact")
|
|
self.assertEqual(path, self.bios_path)
|
|
|
|
def test_not_found(self):
|
|
entry = {"sha1": "0000000000000000000000000000000000000000", "name": "missing.bin"}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertIsNone(path)
|
|
self.assertEqual(status, "not_found")
|
|
|
|
def test_hash_mismatch_fallback(self):
|
|
"""File found by name but MD5 doesn't match -> hash_mismatch."""
|
|
wrong_md5 = "a" * 32
|
|
entry = {"name": "bios.bin", "md5": wrong_md5}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertEqual(status, "hash_mismatch")
|
|
# Should prefer primary over .variants/
|
|
self.assertEqual(path, self.bios_path)
|
|
|
|
def test_zipped_file_resolution_via_zip_contents(self):
|
|
"""zipped_file entry resolved through zip_contents index."""
|
|
zip_contents = {self.inner_md5: self.zip_sha1}
|
|
entry = {
|
|
"name": "nonexistent_zip.zip",
|
|
"md5": self.inner_md5,
|
|
"zipped_file": "rom.bin",
|
|
}
|
|
path, status = resolve_local_file(entry, self.db, zip_contents)
|
|
self.assertEqual(status, "zip_exact")
|
|
self.assertEqual(path, self.zip_path)
|
|
|
|
def test_variants_deprioritized(self):
|
|
"""Primary path preferred over .variants/ path."""
|
|
# Both bios_path and variant_path have name "bios.bin" in by_name
|
|
entry = {"name": "bios.bin"}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertEqual(status, "exact")
|
|
self.assertNotIn(".variants", path)
|
|
|
|
def test_truncated_md5_match(self):
|
|
"""Batocera truncated MD5 (29 chars) matches via prefix."""
|
|
truncated = self.md5[:29]
|
|
entry = {"md5": truncated, "name": "something.bin"}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertEqual(status, "md5_exact")
|
|
self.assertEqual(path, self.bios_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|