102 lines
4.1 KiB
Python
Executable file
102 lines
4.1 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""compare.py -- oracle check of the C++ walker against the reference reader.
|
|
|
|
usage: compare.py --reader SAVE_READER.py --saves DIR --dumps DIR [--python PY]
|
|
|
|
For every DIR/*.sav it runs `save_reader.py SAVE --dump --padding joint` and
|
|
reads the C++ dump written by test_save (`<name>.cpp.dump` in --dumps), then
|
|
reports two agreement figures per save:
|
|
|
|
exact lines identical (including the `?` guess marks and `(alt ...)`)
|
|
canonical lines identical after normalising what is presentation only:
|
|
guess marks and alt readings dropped, 4-byte words compared by
|
|
their raw bytes (an int reading and a float reading of the same
|
|
word are the same datum), strings compared by content.
|
|
|
|
The typed summary (game name, turn, numSys, players) is compared as well.
|
|
Exit status 1 when any canonical line differs or a summary disagrees.
|
|
"""
|
|
import argparse
|
|
import ast
|
|
import glob
|
|
import json
|
|
import os
|
|
import re
|
|
import shlex
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
|
|
LINE = re.compile(r"^@([0-9a-f]{8}) (\s*)(\S+|) (\S+?)(\??) ?(.*)$")
|
|
|
|
|
|
def canon(line: str) -> str:
|
|
m = LINE.match(line)
|
|
if not m:
|
|
return line
|
|
off, indent, name, kind, _guess, rest = m.groups()
|
|
rest = re.sub(r"\s+\(alt .*\)$", "", rest)
|
|
if kind in ("int", "float"):
|
|
try:
|
|
raw = struct.pack("<i", int(rest)) if kind == "int" else struct.pack("<f", float(rest))
|
|
return f"@{off} {indent}{name} word {raw.hex()}"
|
|
except (ValueError, struct.error):
|
|
pass
|
|
return f"@{off} {indent}{name} {kind} {rest}"
|
|
|
|
|
|
def summary_of_reader(py, reader, save):
|
|
out = subprocess.run([*shlex.split(py), reader, save, "--padding", "joint"], capture_output=True, text=True, check=True).stdout
|
|
m = re.search(r"summary: game=(.*?) turn=(\d+) numSys=(\d+) players=(\d+)", out)
|
|
return (ast.literal_eval(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)))
|
|
|
|
|
|
def summary_of_cpp(path):
|
|
txt = open(path, encoding="utf-8").read()
|
|
m = re.search(r"summary: game=(\".*?\") turn=(\d+) numSys=(\d+) players=(\d+)", txt)
|
|
return (json.loads(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)))
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--reader", required=True)
|
|
ap.add_argument("--saves", required=True)
|
|
ap.add_argument("--dumps", required=True)
|
|
ap.add_argument("--python", default=sys.executable)
|
|
a = ap.parse_args()
|
|
bad = 0
|
|
saves = sorted(glob.glob(os.path.join(a.saves, "*.sav")))
|
|
if not saves:
|
|
print("compare: no saves")
|
|
return 0
|
|
for save in saves:
|
|
base = os.path.basename(save)
|
|
cpp_dump = os.path.join(a.dumps, base + ".cpp.dump")
|
|
if not os.path.exists(cpp_dump):
|
|
print(f"{base}: no C++ dump at {cpp_dump}")
|
|
bad += 1
|
|
continue
|
|
ref = subprocess.run([*shlex.split(a.python), a.reader, save, "--dump", "--padding", "joint"],
|
|
capture_output=True, text=True, check=True).stdout.splitlines()
|
|
cpp = open(cpp_dump, encoding="utf-8").read().splitlines()
|
|
n = max(len(ref), len(cpp))
|
|
exact = sum(1 for x, y in zip(ref, cpp) if x == y)
|
|
cref, ccpp = [canon(l) for l in ref], [canon(l) for l in cpp]
|
|
canonical = sum(1 for x, y in zip(cref, ccpp) if x == y)
|
|
first = next((i for i, (x, y) in enumerate(zip(cref, ccpp)) if x != y), None)
|
|
rs = summary_of_reader(a.python, a.reader, save)
|
|
cs = summary_of_cpp(os.path.join(a.dumps, base + ".cpp.summary"))
|
|
ok = canonical == n and len(ref) == len(cpp) and rs == cs
|
|
print(f"{base}: lines ref={len(ref)} cpp={len(cpp)} exact {exact}/{n} ({100.0 * exact / n:.2f}%) "
|
|
f"canonical {canonical}/{n} ({100.0 * canonical / n:.2f}%) summary {'agree' if rs == cs else 'DIFFER'}"
|
|
f" {cs}")
|
|
if first is not None:
|
|
print(f" first canonical difference at line {first + 1}:\n ref: {ref[first]}\n cpp: {cpp[first]}")
|
|
if not ok:
|
|
bad += 1
|
|
print("compare:", "ok" if not bad else f"{bad} save(s) disagree")
|
|
return 1 if bad else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|