mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-04-13 12:22:33 -05:00
sparse clone upstream repos, parse BIOS root sets from C source, cache as JSON, merge into emulator profiles with backup. covers macro expansion, version detection, subset profile protection.
424 lines
15 KiB
Python
424 lines
15 KiB
Python
"""Tests for the hash merge module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from scripts.scraper._hash_merge import (
|
|
compute_diff,
|
|
merge_fbneo_profile,
|
|
merge_mame_profile,
|
|
)
|
|
|
|
|
|
def _write_yaml(path: Path, data: dict) -> str:
|
|
p = str(path)
|
|
with open(p, 'w', encoding='utf-8') as f:
|
|
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
return p
|
|
|
|
|
|
def _write_json(path: Path, data: dict) -> str:
|
|
p = str(path)
|
|
with open(p, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f)
|
|
return p
|
|
|
|
|
|
def _make_mame_profile(**overrides: object) -> dict:
|
|
base = {
|
|
'emulator': 'MAME',
|
|
'core_version': '0.285',
|
|
'files': [
|
|
{
|
|
'name': 'neogeo.zip',
|
|
'required': True,
|
|
'category': 'bios_zip',
|
|
'system': 'snk-neogeo-mvs',
|
|
'source_ref': 'src/mame/neogeo/neogeo.cpp:2400',
|
|
'contents': [
|
|
{
|
|
'name': 'sp-s2.sp1',
|
|
'size': 131072,
|
|
'crc32': 'oldcrc32',
|
|
'description': 'Europe MVS (Ver. 2)',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
def _make_mame_hashes(**overrides: object) -> dict:
|
|
base = {
|
|
'source': 'mamedev/mame',
|
|
'version': '0.286',
|
|
'commit': 'abc123',
|
|
'fetched_at': '2026-03-30T12:00:00Z',
|
|
'bios_sets': {
|
|
'neogeo': {
|
|
'source_file': 'src/mame/neogeo/neogeo.cpp',
|
|
'source_line': 2432,
|
|
'roms': [
|
|
{
|
|
'name': 'sp-s2.sp1',
|
|
'size': 131072,
|
|
'crc32': '9036d879',
|
|
'sha1': '4f834c55',
|
|
'region': 'mainbios',
|
|
'bios_label': 'euro',
|
|
'bios_description': 'Europe MVS (Ver. 2)',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
def _make_fbneo_profile(**overrides: object) -> dict:
|
|
base = {
|
|
'emulator': 'FinalBurn Neo',
|
|
'core_version': 'v1.0.0.02',
|
|
'files': [
|
|
{
|
|
'name': 'sp-s2.sp1',
|
|
'archive': 'neogeo.zip',
|
|
'system': 'snk-neogeo-mvs',
|
|
'required': True,
|
|
'size': 131072,
|
|
'crc32': 'oldcrc32',
|
|
'source_ref': 'src/burn/drv/neogeo/d_neogeo.cpp:1605',
|
|
},
|
|
{
|
|
'name': 'hiscore.dat',
|
|
'required': False,
|
|
},
|
|
],
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
def _make_fbneo_hashes(**overrides: object) -> dict:
|
|
base = {
|
|
'source': 'finalburnneo/FBNeo',
|
|
'version': 'v1.0.0.03',
|
|
'commit': 'def456',
|
|
'fetched_at': '2026-03-30T12:00:00Z',
|
|
'bios_sets': {
|
|
'neogeo': {
|
|
'source_file': 'src/burn/drv/neogeo/d_neogeo.cpp',
|
|
'source_line': 1604,
|
|
'roms': [
|
|
{
|
|
'name': 'sp-s2.sp1',
|
|
'size': 131072,
|
|
'crc32': '9036d879',
|
|
'sha1': 'aabbccdd',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
class TestMameMerge(unittest.TestCase):
|
|
"""Tests for merge_mame_profile."""
|
|
|
|
def test_merge_updates_contents(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'mame.yml', _make_mame_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', _make_mame_hashes())
|
|
|
|
result = merge_mame_profile(profile_path, hashes_path)
|
|
|
|
bios_files = [f for f in result['files'] if f.get('category') == 'bios_zip']
|
|
self.assertEqual(len(bios_files), 1)
|
|
contents = bios_files[0]['contents']
|
|
self.assertEqual(contents[0]['crc32'], '9036d879')
|
|
self.assertEqual(contents[0]['sha1'], '4f834c55')
|
|
self.assertEqual(contents[0]['description'], 'Europe MVS (Ver. 2)')
|
|
|
|
def test_merge_preserves_manual_fields(self) -> None:
|
|
profile = _make_mame_profile()
|
|
profile['files'][0]['note'] = 'manually curated note'
|
|
profile['files'][0]['system'] = 'snk-neogeo-mvs'
|
|
profile['files'][0]['required'] = False
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'mame.yml', profile)
|
|
hashes_path = _write_json(p / 'hashes.json', _make_mame_hashes())
|
|
|
|
result = merge_mame_profile(profile_path, hashes_path)
|
|
|
|
entry = [f for f in result['files'] if f.get('category') == 'bios_zip'][0]
|
|
self.assertEqual(entry['note'], 'manually curated note')
|
|
self.assertEqual(entry['system'], 'snk-neogeo-mvs')
|
|
self.assertFalse(entry['required'])
|
|
|
|
def test_merge_adds_new_bios_set(self) -> None:
|
|
hashes = _make_mame_hashes()
|
|
hashes['bios_sets']['pgm'] = {
|
|
'source_file': 'src/mame/igs/pgm.cpp',
|
|
'source_line': 5515,
|
|
'roms': [
|
|
{'name': 'pgm_t01s.rom', 'size': 2097152, 'crc32': '1a7123a0'},
|
|
],
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'mame.yml', _make_mame_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', hashes)
|
|
|
|
result = merge_mame_profile(profile_path, hashes_path)
|
|
|
|
bios_files = [f for f in result['files'] if f.get('category') == 'bios_zip']
|
|
names = {f['name'] for f in bios_files}
|
|
self.assertIn('pgm.zip', names)
|
|
|
|
pgm = next(f for f in bios_files if f['name'] == 'pgm.zip')
|
|
self.assertIsNone(pgm['system'])
|
|
self.assertTrue(pgm['required'])
|
|
self.assertEqual(pgm['category'], 'bios_zip')
|
|
|
|
def test_merge_preserves_non_bios_files(self) -> None:
|
|
profile = _make_mame_profile()
|
|
profile['files'].append({'name': 'hiscore.dat', 'required': False})
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'mame.yml', profile)
|
|
hashes_path = _write_json(p / 'hashes.json', _make_mame_hashes())
|
|
|
|
result = merge_mame_profile(profile_path, hashes_path)
|
|
|
|
non_bios = [f for f in result['files'] if f.get('category') != 'bios_zip']
|
|
self.assertEqual(len(non_bios), 1)
|
|
self.assertEqual(non_bios[0]['name'], 'hiscore.dat')
|
|
|
|
def test_merge_keeps_unmatched_bios_set(self) -> None:
|
|
"""Entries not in scraper scope stay untouched (no _upstream_removed)."""
|
|
hashes = _make_mame_hashes()
|
|
hashes['bios_sets'] = {} # nothing from scraper
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'mame.yml', _make_mame_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', hashes)
|
|
|
|
result = merge_mame_profile(profile_path, hashes_path)
|
|
|
|
bios_files = [f for f in result['files'] if f.get('category') == 'bios_zip']
|
|
self.assertEqual(len(bios_files), 1)
|
|
self.assertNotIn('_upstream_removed', bios_files[0])
|
|
self.assertEqual(bios_files[0]['name'], 'neogeo.zip')
|
|
|
|
def test_merge_updates_core_version(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'mame.yml', _make_mame_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', _make_mame_hashes())
|
|
|
|
result = merge_mame_profile(profile_path, hashes_path)
|
|
|
|
self.assertEqual(result['core_version'], '0.286')
|
|
|
|
def test_merge_backup_created(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'mame.yml', _make_mame_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', _make_mame_hashes())
|
|
|
|
merge_mame_profile(profile_path, hashes_path, write=True)
|
|
|
|
backup = p / 'mame.old.yml'
|
|
self.assertTrue(backup.exists())
|
|
|
|
with open(backup, encoding='utf-8') as f:
|
|
old = yaml.safe_load(f)
|
|
self.assertEqual(old['core_version'], '0.285')
|
|
|
|
def test_merge_updates_source_ref(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'mame.yml', _make_mame_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', _make_mame_hashes())
|
|
|
|
result = merge_mame_profile(profile_path, hashes_path)
|
|
|
|
entry = [f for f in result['files'] if f.get('category') == 'bios_zip'][0]
|
|
self.assertEqual(entry['source_ref'], 'src/mame/neogeo/neogeo.cpp:2432')
|
|
|
|
|
|
class TestFbneoMerge(unittest.TestCase):
|
|
"""Tests for merge_fbneo_profile."""
|
|
|
|
def test_merge_updates_rom_entries(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'fbneo.yml', _make_fbneo_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', _make_fbneo_hashes())
|
|
|
|
result = merge_fbneo_profile(profile_path, hashes_path)
|
|
|
|
archive_files = [f for f in result['files'] if 'archive' in f]
|
|
self.assertEqual(len(archive_files), 1)
|
|
self.assertEqual(archive_files[0]['crc32'], '9036d879')
|
|
self.assertEqual(archive_files[0]['system'], 'snk-neogeo-mvs')
|
|
|
|
def test_merge_adds_new_roms(self) -> None:
|
|
hashes = _make_fbneo_hashes()
|
|
hashes['bios_sets']['neogeo']['roms'].append({
|
|
'name': 'sp-s3.sp1',
|
|
'size': 131072,
|
|
'crc32': '91b64be3',
|
|
})
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'fbneo.yml', _make_fbneo_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', hashes)
|
|
|
|
result = merge_fbneo_profile(profile_path, hashes_path)
|
|
|
|
archive_files = [f for f in result['files'] if 'archive' in f]
|
|
self.assertEqual(len(archive_files), 2)
|
|
new_rom = next(f for f in archive_files if f['name'] == 'sp-s3.sp1')
|
|
self.assertEqual(new_rom['archive'], 'neogeo.zip')
|
|
self.assertTrue(new_rom['required'])
|
|
|
|
def test_merge_preserves_non_archive_files(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'fbneo.yml', _make_fbneo_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', _make_fbneo_hashes())
|
|
|
|
result = merge_fbneo_profile(profile_path, hashes_path)
|
|
|
|
non_archive = [f for f in result['files'] if 'archive' not in f]
|
|
self.assertEqual(len(non_archive), 1)
|
|
self.assertEqual(non_archive[0]['name'], 'hiscore.dat')
|
|
|
|
def test_merge_keeps_unmatched_roms(self) -> None:
|
|
"""Entries not in scraper scope stay untouched (no _upstream_removed)."""
|
|
hashes = _make_fbneo_hashes()
|
|
hashes['bios_sets'] = {}
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'fbneo.yml', _make_fbneo_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', hashes)
|
|
|
|
result = merge_fbneo_profile(profile_path, hashes_path)
|
|
|
|
archive_files = [f for f in result['files'] if 'archive' in f]
|
|
self.assertEqual(len(archive_files), 1)
|
|
self.assertNotIn('_upstream_removed', archive_files[0])
|
|
|
|
def test_merge_updates_core_version(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'fbneo.yml', _make_fbneo_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', _make_fbneo_hashes())
|
|
|
|
result = merge_fbneo_profile(profile_path, hashes_path)
|
|
|
|
self.assertEqual(result['core_version'], 'v1.0.0.03')
|
|
|
|
|
|
class TestDiff(unittest.TestCase):
|
|
"""Tests for compute_diff."""
|
|
|
|
def test_diff_mame_detects_changes(self) -> None:
|
|
hashes = _make_mame_hashes()
|
|
hashes['bios_sets']['pgm'] = {
|
|
'source_file': 'src/mame/igs/pgm.cpp',
|
|
'source_line': 5515,
|
|
'roms': [
|
|
{'name': 'pgm_t01s.rom', 'size': 2097152, 'crc32': '1a7123a0'},
|
|
],
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'mame.yml', _make_mame_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', hashes)
|
|
|
|
diff = compute_diff(profile_path, hashes_path, mode='mame')
|
|
|
|
self.assertIn('pgm', diff['added'])
|
|
self.assertIn('neogeo', diff['updated'])
|
|
self.assertEqual(len(diff['removed']), 0)
|
|
self.assertEqual(diff['unchanged'], 0)
|
|
|
|
def test_diff_mame_out_of_scope(self) -> None:
|
|
"""Items in profile but not in scraper output = out of scope, not removed."""
|
|
hashes = _make_mame_hashes()
|
|
hashes['bios_sets'] = {}
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'mame.yml', _make_mame_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', hashes)
|
|
|
|
diff = compute_diff(profile_path, hashes_path, mode='mame')
|
|
|
|
self.assertEqual(diff['removed'], [])
|
|
self.assertEqual(diff['out_of_scope'], 1)
|
|
self.assertEqual(len(diff['added']), 0)
|
|
|
|
def test_diff_fbneo_detects_changes(self) -> None:
|
|
hashes = _make_fbneo_hashes()
|
|
hashes['bios_sets']['neogeo']['roms'].append({
|
|
'name': 'sp-s3.sp1',
|
|
'size': 131072,
|
|
'crc32': '91b64be3',
|
|
})
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'fbneo.yml', _make_fbneo_profile())
|
|
hashes_path = _write_json(p / 'hashes.json', hashes)
|
|
|
|
diff = compute_diff(profile_path, hashes_path, mode='fbneo')
|
|
|
|
self.assertIn('neogeo.zip:sp-s3.sp1', diff['added'])
|
|
self.assertIn('neogeo.zip:sp-s2.sp1', diff['updated'])
|
|
self.assertEqual(len(diff['removed']), 0)
|
|
|
|
def test_diff_fbneo_unchanged(self) -> None:
|
|
profile = _make_fbneo_profile()
|
|
profile['files'][0]['crc32'] = '9036d879'
|
|
profile['files'][0]['size'] = 131072
|
|
|
|
hashes = _make_fbneo_hashes()
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
p = Path(td)
|
|
profile_path = _write_yaml(p / 'fbneo.yml', profile)
|
|
hashes_path = _write_json(p / 'hashes.json', hashes)
|
|
|
|
diff = compute_diff(profile_path, hashes_path, mode='fbneo')
|
|
|
|
self.assertEqual(diff['unchanged'], 1)
|
|
self.assertEqual(len(diff['added']), 0)
|
|
self.assertEqual(len(diff['updated']), 0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|