gameboy-rust/tools/prepare_gba_tests.py
0xrsydn a28b830b38 feat(testing): pin public ARM fixture and document compatibility baseline
Add a standard-library Python adapter and SHA-256/size lock for jsmolka/gba-tests ARM at a7113b67e63f83a9b321696ddd7042ccfad6c881. Retain sources and license locally, refuse tracked output paths or overwrites, verify offline inputs, and generate the source-verified eval/r12 suite checkpoint without patching the ROM.

Add Python to the pinned Nix shell, offline adapter regressions, and public-test provenance/results documentation. Verified on Darwin arm64: Rust debug/release workspace tests, adapter tests, Clippy, rustdoc, native ROM and all demo window checks. The public test remains a failing baseline at instruction 0xe5b00004, PC 0x0800132c, in test 360.
2026-10-05 20:01:08 +07:00

112 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Prepare a hash-verified, pinned public ARM test outside tracked source paths.
This adapter is original. Upstream sources, license, and ROM bytes are downloaded
only to the requested local directory. They are not embedded in this repository.
"""
import argparse
import hashlib
import json
from pathlib import Path
import struct
import sys
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parent.parent
LOCK = Path(__file__).with_name("gba-tests-arm.lock.json")
MAX_FILE_BYTES = 1024 * 1024
def fetch(url):
request = Request(url, headers={"User-Agent": "gameboy-rust-public-test-adapter"})
with urlopen(request, timeout=30) as response:
return response.read(MAX_FILE_BYTES + 1)
def verify(name, data, expected):
if len(data) > MAX_FILE_BYTES or len(data) != expected["bytes"]:
raise ValueError(f"size mismatch for {name}")
if hashlib.sha256(data).hexdigest() != expected["sha256"]:
raise ValueError(f"SHA-256 mismatch for {name}")
def destination(path):
path = Path(path).resolve()
# Public ROM headers can contain third-party assets. Keep them untracked.
if path.is_relative_to(ROOT) and not path.is_relative_to(ROOT / "roms"):
raise ValueError("output must be outside the repository or inside ignored roms/")
if path.exists():
raise ValueError(f"output directory already exists: {path}")
if not path.parent.is_dir():
raise ValueError("output parent directory must already exist")
return path
def prepare(output, source=None, download=fetch):
output = destination(output)
lock = json.loads(LOCK.read_text(encoding="utf-8"))
base = f"https://raw.githubusercontent.com/{lock['repository']}/{lock['revision']}/"
files = {}
for name, expected in lock["files"].items():
if source is None:
data = download(base + name)
else:
path = Path(source) / name
if not path.is_file() or path.stat().st_size > MAX_FILE_BYTES:
raise ValueError(f"source must be a bounded regular file: {name}")
with path.open("rb") as stream:
data = stream.read(MAX_FILE_BYTES + 1)
verify(name, data, expected)
files[name] = data
# The source-defined eval label starts with m_vsync's register save. All
# failure exits branch here with r12 set; successful fallthrough keeps r12=0.
# Hash pinning is authoritative; this check also catches adapter-offset mistakes.
pc = int(lock["completion_pc"], 16)
offset = pc - 0x08000000
rom = files[lock["rom"]]
if offset < 0 or offset + 4 > len(rom) or struct.unpack_from("<I", rom, offset)[0] != 0xE92D0003:
raise ValueError("pinned evaluation checkpoint does not match the ROM")
manifest = {
"version": 1,
"cases": [{
"name": "jsmolka-arm",
"rom": lock["rom"],
"step_limit": 1_000_000,
"completion": {"pc": lock["completion_pc"], "instruction_set": "arm"},
"checks": [{"kind": "register", "index": lock["result_register"], "equals": 0}],
}],
}
# Validate every file before creating output. Existing destinations are never
# reused, and file creation is exclusive. Disk errors can leave partial output.
output.mkdir()
for name, data in files.items():
target = output / name
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("xb") as stream:
stream.write(data)
for name, value in [("suite.json", manifest), ("source-lock.json", lock)]:
with (output / name).open("x", encoding="utf-8") as stream:
json.dump(value, stream, indent=2)
stream.write("\n")
return output / "suite.json"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("output", type=Path, help="new directory outside the repository or under roms/")
parser.add_argument("--source-dir", type=Path, help="offline upstream tree; every selected file must match the lock")
args = parser.parse_args()
try:
manifest = prepare(args.output, args.source_dir)
except (OSError, ValueError) as error:
print(f"cannot prepare public ARM test: {error}", file=sys.stderr)
return 1
print(f"Prepared pinned public ARM suite: {manifest}")
print("Run with gameboy-rust --test-suite PATH. Preparation does not mean the CPU tests passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())