sots-engine/tests/mars_stream/oracle/compare.py

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())