test(cpu): integrate pinned public Thumb compatibility suite

Add --suite thumb while retaining default ARM preparation. Verify hashes, the Thumb-to-ARM evaluation bridge, and the result checkpoint; retain upstream provenance outside tracked files. Expand offline synthetic adapter and CLI regressions.

Darwin arm64 baseline: the unmodified Thumb ROM reaches ARM eval at 0x08000934 after 771 steps with r7=229 in debug and release. ARM remains passing. Online/offline preparation agrees and all 21 Python tests pass. CPU behavior is unchanged.
This commit is contained in:
0xrsydn 2026-10-05 20:40:37 +07:00
commit 08c8705904
11 changed files with 304 additions and 50 deletions

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Prepare a hash-verified, pinned public ARM test outside tracked source paths.
"""Prepare a hash-verified, pinned public ARM or Thumb test outside tracked 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.
@ -14,7 +14,8 @@ import sys
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parent.parent
LOCK = Path(__file__).with_name("gba-tests-arm.lock.json")
LOCKS = {suite: Path(__file__).with_name(f"gba-tests-{suite}.lock.json")
for suite in ("arm", "thumb")}
MAX_FILE_BYTES = 1024 * 1024
@ -43,9 +44,11 @@ def destination(path):
return path
def prepare(output, source=None, download=fetch):
def prepare(output, source=None, download=fetch, suite="arm"):
if suite not in LOCKS:
raise ValueError(f"unsupported public suite: {suite}")
output = destination(output)
lock = json.loads(LOCK.read_text(encoding="utf-8"))
lock = json.loads(LOCKS[suite].read_text(encoding="utf-8"))
base = f"https://raw.githubusercontent.com/{lock['repository']}/{lock['revision']}/"
files = {}
for name, expected in lock["files"].items():
@ -61,17 +64,20 @@ def prepare(output, source=None, download=fetch):
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.
# failure exits arrive with the result register set (ARM r12, Thumb r7).
# Thumb tests switch back to ARM before eval. Success leaves the result zero.
# Hash pinning is authoritative; these checks catch 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:
if offset < 0 or offset % 4 or offset + 4 > len(rom) or struct.unpack_from("<I", rom, offset)[0] != 0xE92D0003:
raise ValueError("pinned evaluation checkpoint does not match the ROM")
if suite == "thumb" and (offset < 4 or struct.unpack_from("<HH", rom, offset - 4) != (0xA000, 0x4700)):
raise ValueError("pinned Thumb-to-ARM checkpoint bridge does not match the ROM")
manifest = {
"version": 1,
"cases": [{
"name": "jsmolka-arm",
"name": f"jsmolka-{suite}",
"rom": lock["rom"],
"step_limit": 1_000_000,
"completion": {"pc": lock["completion_pc"], "instruction_set": "arm"},
@ -96,14 +102,15 @@ def prepare(output, source=None, download=fetch):
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("--suite", choices=tuple(LOCKS), default="arm", help="public suite to prepare (default: arm)")
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)
manifest = prepare(args.output, args.source_dir, suite=args.suite)
except (OSError, ValueError) as error:
print(f"cannot prepare public ARM test: {error}", file=sys.stderr)
print(f"cannot prepare public {args.suite} test: {error}", file=sys.stderr)
return 1
print(f"Prepared pinned public ARM suite: {manifest}")
print(f"Prepared pinned public {args.suite} suite: {manifest}")
print("Run with gameboy-rust --test-suite PATH. Preparation does not mean the CPU tests passed.")
return 0