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.
This commit is contained in:
parent
75fb98bc55
commit
a28b830b38
13 changed files with 460 additions and 8 deletions
112
tools/prepare_gba_tests.py
Normal file
112
tools/prepare_gba_tests.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue