feat: read required from romload for MAME sets

A set whose every ROM is NO_DUMP or ROM_OPTIONAL only warns in
handle_missing_file, so the machine runs without it. The parser now
keeps undumped and optional ROMs for that decision, the merge and the
release profiles write required false for such sets, and the text
patcher rewrites the required line it used to leave. A curated false
over a set with dumped ROMs is kept, except over no contents, which is
what an undumped set leaves once a later release dumps it.

pSxMAME builds mamedev/mame itself at 0.133 and left the current
refresh, which would have moved it onto 0.289 refs. Release profiles
keep their citations double-quoted on regeneration.
This commit is contained in:
Abdessamad Derraz committed 2026-10-22 14:06:18 +02:00
1 parent 2870c72ccb
commit d60fe814ed
5 files changed
+272 -19

No files matched your search

+109
View File
@@ -170,6 +170,85 @@ class TestMameMerge(unittest.TestCase):
self.assertEqual(entry["system"], "snk-neogeo-mvs")
self.assertFalse(entry["required"])
def test_merge_sets_required_false_for_a_set_the_machine_runs_without(self) -> None:
profile = _make_mame_profile()
profile["files"].append({
"name": "mac2bios.zip", "required": True, "category": "bios_zip",
"note": "kept",
})
hashes = _make_mame_hashes()
hashes["bios_sets"]["mac2bios"] = {
"source_file": "src/mame/seta/macs.cpp", "source_line": 776,
"roms": [], "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", hashes)
diff = compute_diff(profile_path, hashes_path, mode="mame")
self.assertIn("mac2bios", diff["updated"])
result = merge_mame_profile(profile_path, hashes_path)
entry = next(f for f in result["files"] if f["name"] == "mac2bios.zip")
self.assertFalse(entry["required"])
self.assertEqual(entry["note"], "kept")
def test_written_profile_carries_the_settled_required(self) -> None:
"""The text patcher rewrote contents and source_ref but left required."""
hashes = _make_mame_hashes()
hashes["bios_sets"] = {"mac2bios": {
"source_file": "src/mame/seta/macs.cpp", "source_line": 776,
"roms": [], "required": False,
}}
with tempfile.TemporaryDirectory() as td:
p = Path(td)
profile_path = str(p / "mame.yml")
Path(profile_path).write_text(
'emulator: MAME\ncore_version: "0.289"\nfiles:\n'
" - name: mac2bios.zip\n required: true\n category: bios_zip\n"
' source_ref: "upstream src/mame/seta/macs.cpp:776"\n',
encoding="utf-8",
)
hashes_path = _write_json(p / "hashes.json", hashes)
merge_mame_profile(profile_path, hashes_path, write=True)
written = yaml.safe_load(Path(profile_path).read_text(encoding="utf-8"))
entry = next(f for f in written["files"] if f["name"] == "mac2bios.zip")
self.assertIs(entry["required"], False)
def test_merge_keeps_a_curated_false_over_a_fatal_set(self) -> None:
profile = _make_mame_profile()
profile["files"][0]["required"] = False
hashes = _make_mame_hashes()
hashes["bios_sets"]["neogeo"]["required"] = True
with tempfile.TemporaryDirectory() as td:
p = Path(td)
profile_path = _write_yaml(p / "mame.yml", profile)
hashes_path = _write_json(p / "hashes.json", hashes)
result = merge_mame_profile(profile_path, hashes_path)
entry = next(f for f in result["files"] if f["name"] == "neogeo.zip")
self.assertFalse(entry["required"])
def test_merge_restores_required_once_an_undumped_set_is_dumped(self) -> None:
profile = _make_mame_profile()
profile["files"].append({
"name": "recel.zip", "required": False, "category": "bios_zip",
"contents": [],
})
hashes = _make_mame_hashes()
hashes["bios_sets"]["recel"] = {
"source_file": "src/mame/pinball/recel.cpp", "source_line": 546,
"roms": [{"name": "a2361.b1", "size": 1024, "crc32": "eeb8f3f0"}],
"required": True,
}
with tempfile.TemporaryDirectory() as td:
p = Path(td)
profile_path = _write_yaml(p / "mame.yml", profile)
hashes_path = _write_json(p / "hashes.json", hashes)
result = merge_mame_profile(profile_path, hashes_path)
entry = next(f for f in result["files"] if f["name"] == "recel.zip")
self.assertTrue(entry["required"])
def test_merge_adds_new_bios_set(self) -> None:
hashes = _make_mame_hashes()
hashes["bios_sets"]["pgm"] = {
@@ -405,6 +484,30 @@ class AReleaseProfileDescribesItsOwnRelease(unittest.TestCase):
self.assertNotIn("system", files["newset.zip"])
self.assertEqual(files["neogeo.zip"]["source_ref"], "upstream src/mame/neogeo/neogeo.cpp:10")
def test_a_set_the_release_runs_without_is_not_required(self):
"""recel is undumped at 0.258: the current profile's true does not apply."""
from scripts.scraper.mame_hash_scraper import release_profile
bios_sets = {
"recel": {"source_file": "src/mame/pinball/recel.cpp", "source_line": 505,
"roms": [], "required": False},
"neogeo": {"source_file": "src/mame/neogeo/neogeo.cpp", "source_line": 10,
"roms": [{"name": "sp-s2.sp1", "size": 131072, "crc32": "9036d879"}],
"required": True},
}
reference = {"files": [
{"name": "recel.zip", "category": "bios_zip", "required": True,
"contents": [{"name": "a2361.b1", "size": 1024, "crc32": "eeb8f3f0"}]},
{"name": "neogeo.zip", "category": "bios_zip", "required": True},
]}
profile = release_profile(
"mame0258", bios_sets, "a" * 40, "b" * 40, ["mame0258"], reference,
["recalbox"],
)
files = {f["name"]: f for f in profile["files"]}
self.assertFalse(files["recel.zip"]["required"])
self.assertTrue(files["neogeo.zip"]["required"])
def test_a_release_profile_is_left_out_of_the_current_refresh(self):
from scripts.scraper.mame_hash_scraper import _find_mame_profiles
@@ -412,6 +515,12 @@ class AReleaseProfileDescribesItsOwnRelease(unittest.TestCase):
self.assertIn("mame.yml", names)
self.assertFalse({"mame0258.yml", "mame0278.yml"} & names)
def test_a_pinned_build_of_the_mame_tree_is_left_out(self):
"""pSxMAME builds mamedev/mame at 0.133: refreshed, it would carry 0.289 refs."""
from scripts.scraper.mame_hash_scraper import _find_mame_profiles
self.assertNotIn("psxmame.yml", {path.name for path in _find_mame_profiles()})
class TestFbneoMerge(unittest.TestCase):
"""Tests for merge_fbneo_profile."""
+71
View File
@@ -10,6 +10,7 @@ from scripts.scraper.mame_parser import (
find_bios_root_sets,
parse_mame_source_tree,
parse_rom_block,
set_is_required,
)
# Standard GAME macro with MACHINE_IS_BIOS_ROOT, multiple ROM entries, BIOS variants
@@ -51,6 +52,35 @@ ROM_END
GAME( 2000, testnd, 0, testnd, testnd, test_state, init_test, ROT0, "Test", "Test ND", MACHINE_IS_BIOS_ROOT )
"""
# Every ROM undumped, as macs.cpp declares mac2bios: the machine runs without the set
ALL_NODUMP_FIXTURE = """\
#define MACS2_BIOS \\
ROM_REGION( 0x400000, "bios", 0 ) \\
ROM_LOAD16_BYTE( "macs2os_l.bin", 0x00000, 0x80000, NO_DUMP ) \\
ROM_LOAD16_BYTE( "macs2os_h.bin", 0x00001, 0x80000, NO_DUMP )
ROM_START( mac2bios )
MACS2_BIOS
ROM_REGION( 0x1000000, "maincpu", 0 )
ROM_COPY( "bios", 0x000000, 0x000000, 0x400000 )
ROM_END
GAME( 1995, mac2bios, 0, macs, macs_m, macs_state, init_macs2, ROT0, "I'Max", "Multi Amenity Cassette System 2 BIOS", MACHINE_IS_BIOS_ROOT | MACHINE_NOT_WORKING )
"""
# Every ROM optional, through both spellings romentry.h offers, plus one undumped
ALL_OPTIONAL_FIXTURE = """\
ROM_START( testopt )
ROM_REGION( 0x10000, "maincpu", 0 )
ROM_LOAD_OPTIONAL( "opt1.rom", 0x00000, 0x4000, CRC(aabbccdd) SHA1(1122334455667788990011223344556677889900) )
ROMX_LOAD( "opt2.rom", 0x04000, 0x4000, CRC(aabbccde) SHA1(1122334455667788990011223344556677889901), ROM_OPTIONAL )
ROM_LOAD( "gone.rom", 0x08000, 0x4000, NO_DUMP )
ROM_END
GAME( 2000, testopt, 0, testopt, testopt, test_state, init_test, ROT0, "Test", "Test Optional", MACHINE_IS_BIOS_ROOT )
"""
# ROM_LOAD with BAD_DUMP
BADDUMP_FIXTURE = """\
ROM_START( testbd )
@@ -284,6 +314,47 @@ ROM_END
self.assertEqual(roms[0]["sha1"], "aabbccddeeff00112233aabbccddeeff00112233")
class TestSetIsRequired(unittest.TestCase):
"""A set is required only if romload.cpp errors when it is missing."""
def test_all_no_dump_set_is_not_required(self) -> None:
entries = parse_rom_block(ALL_NODUMP_FIXTURE, "mac2bios", keep_no_dump=True)
self.assertEqual([e["name"] for e in entries], ["macs2os_l.bin", "macs2os_h.bin"])
self.assertTrue(all(e["no_dump"] for e in entries))
self.assertIs(set_is_required(entries), False)
def test_optional_and_no_dump_set_is_not_required(self) -> None:
entries = parse_rom_block(ALL_OPTIONAL_FIXTURE, "testopt", keep_no_dump=True)
self.assertEqual(len(entries), 3)
self.assertTrue(entries[0]["optional"] and entries[1]["optional"])
self.assertIs(set_is_required(entries), False)
def test_one_dumped_rom_makes_the_set_required(self) -> None:
entries = parse_rom_block(NODUMP_FIXTURE, "testnd", keep_no_dump=True)
self.assertIs(set_is_required(entries), True)
def test_bad_dump_is_still_fatal(self) -> None:
entries = parse_rom_block(BADDUMP_FIXTURE, "testbd", keep_no_dump=True)
self.assertIs(set_is_required(entries), True)
def test_no_rom_read_decides_nothing(self) -> None:
self.assertIsNone(set_is_required([]))
def test_no_dump_stays_out_of_the_default_parse(self) -> None:
self.assertEqual(parse_rom_block(ALL_NODUMP_FIXTURE, "mac2bios"), [])
def test_tree_records_required_per_set(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
mame_dir = os.path.join(tmpdir, "src", "mame", "seta")
os.makedirs(mame_dir)
with open(os.path.join(mame_dir, "macs.cpp"), "w") as f:
f.write(ALL_NODUMP_FIXTURE + NEOGEO_FIXTURE)
results = parse_mame_source_tree(tmpdir)
self.assertIs(results["mac2bios"]["required"], False)
self.assertEqual(results["mac2bios"]["roms"], [])
self.assertIs(results["neogeo"]["required"], True)
class TestParseMameSourceTree(unittest.TestCase):
"""Tests for parse_mame_source_tree."""