"""Regression checks: our output must be byte-identical to sedonac.exe's, and a conversion must survive a round trip. sax -> sab check_encode() sab -> sax check_decode() sab -> sax -> sab round_trip_sab() bytes must come back identical sax -> sab -> sax -> sab round_trip_sax() the two sabs must be identical Usage: python verify.py [app.sax|app.sab] [sedona_home] With no file argument a chooser opens; cancel it and the built-in `testprogram/` regression runs instead (if those files are present). Sedona home comes from system.properties (see config.py) unless given. """ import os, sys, subprocess, tempfile, shutil import sab, sax, config HERE = os.path.dirname(os.path.abspath(__file__)) TESTS = os.path.join(HERE, "testprogram") HOME = None def home(): """Resolved once, on first use -- so importing this module opens no dialog.""" global HOME if HOME is None: HOME = config.get_sedona_home() return HOME def sedonac(src, tmp): """Run sedonac on a copy of `src`; return the file it produced (needs a JRE).""" work = os.path.join(tmp, os.path.basename(src)) shutil.copy(src, work) exe = os.path.join(home(), "bin", "sedonac.exe") r = subprocess.run([exe, work], capture_output=True, text=True) if r.returncode != 0: print(" sedonac failed:\n" + r.stdout + r.stderr) return None other = ".sax" if work.endswith(".sab") else ".sab" return os.path.splitext(work)[0] + other def compare(label, reference, mine, binary, names=("sedonac", "ours")): a = open(reference, "rb").read() b = open(mine, "rb").read() if a == b: print(" PASS %-34s %d bytes identical" % (label, len(a))) return True diffs = [i for i in range(min(len(a), len(b))) if a[i] != b[i]] print(" FAIL %-34s %s=%d %s=%d, %d byte diffs" % ( label, names[0], len(a), names[1], len(b), len(diffs))) if diffs: i = diffs[0] if binary: lo, hi = max(0, i - 12), i + 12 print(" first diff at offset %d" % i) print(" %-8s %s" % (names[0], a[lo:hi].hex())) print(" %-8s %s" % (names[1], b[lo:hi].hex())) else: line = a[:i].count(b"\n") + 1 print(" first diff at line %d" % line) print(" %-8s %s" % (names[0], a.splitlines()[line - 1])) print(" %-8s %s" % (names[1], b.splitlines()[line - 1])) return False # ---------- against sedonac ---------- def check_encode(sax_file, reference=None): """sax -> sab, against `reference` or a fresh sedonac run.""" tmp = tempfile.mkdtemp() try: reference = reference or sedonac(sax_file, tmp) if reference is None: return False mine = os.path.join(tmp, "mine.sab") sab.encode(sax_file, home(), mine) return compare(os.path.basename(sax_file) + " -> sab", reference, mine, True) finally: shutil.rmtree(tmp, ignore_errors=True) def check_decode(sab_file, reference=None): """sab -> sax, against `reference` or a fresh sedonac run.""" tmp = tempfile.mkdtemp() try: reference = reference or sedonac(sab_file, tmp) if reference is None: return False mine = os.path.join(tmp, "mine.sax") sax.decode(sab_file, home(), mine) return compare(os.path.basename(sab_file) + " -> sax", reference, mine, False) finally: shutil.rmtree(tmp, ignore_errors=True) # ---------- round trips ---------- def round_trip_sab(sab_file): """sab -> sax -> sab must come back to the same bytes.""" tmp = tempfile.mkdtemp() try: mid = os.path.join(tmp, "mid.sax") back = os.path.join(tmp, "back.sab") sax.decode(sab_file, home(), mid) sab.encode(mid, home(), back) return compare(os.path.basename(sab_file) + " sab->sax->sab", sab_file, back, True, ("original", "ours")) finally: shutil.rmtree(tmp, ignore_errors=True) def round_trip_sax(sax_file): """sax -> sab -> sax -> sab: the two sabs must match. The sax leg is not compared against the original file, because a .sab holds no runtime prop values and no formatting -- coming back through it is lossy on purpose. The binary is the thing that has to be stable, so we compare that. Any difference from the original sax is printed as a note, not a failure.""" tmp = tempfile.mkdtemp() try: first = os.path.join(tmp, "first.sab") mid = os.path.join(tmp, "mid.sax") second = os.path.join(tmp, "second.sab") sab.encode(sax_file, home(), first) sax.decode(first, home(), mid) sab.encode(mid, home(), second) ok = compare(os.path.basename(sax_file) + " sax->sab->sax->sab", first, second, True, ("1st sab", "2nd sab")) a = open(sax_file, "rb").read() b = open(mid, "rb").read() if a != b: al, bl = a.splitlines(), b.splitlines() print(" note: the regenerated sax differs from the original " "(%d vs %d lines) -- expected where the original carries runtime " "props or different formatting" % (len(al), len(bl))) return ok finally: shutil.rmtree(tmp, ignore_errors=True) def check_file(path): """Round-trip whatever was chosen, by extension.""" ext = os.path.splitext(path)[1].lower() if ext not in (".sax", ".sab"): print(" don't know what to do with %s -- expected .sax or .sab" % path) return False try: return round_trip_sax(path) if ext == ".sax" else round_trip_sab(path) except Exception as e: # a malformed file is a failure, not a traceback print(" FAIL %-34s %s" % (os.path.basename(path), e)) return False def builtin(): """The checked-in pair, when testprogram/ is populated locally.""" saxf = os.path.join(TESTS, "test_normal.sax") sabf = os.path.join(TESTS, "test_normal_sedonac.sab") missing = [f for f in (saxf, sabf) if not os.path.isfile(f)] if missing: # testprogram/ is deliberately not in the repo -- put the pair back to run this print(" SKIP no test files: %s" % ", ".join(os.path.basename(f) for f in missing)) return None ok = check_encode(saxf, sabf) ok &= check_decode(sabf, saxf) ok &= round_trip_sab(sabf) ok &= round_trip_sax(saxf) return ok def main(argv): global HOME print("%s %s" % (config.NAME, config.VERSION)) path = argv[1] if len(argv) > 1 else config.choose_input_file() if len(argv) > 2: HOME = argv[2] print("sedona home: %s" % home()) if not path: print("no file chosen -- running the built-in checks") ok = builtin() return 0 if ok is not False else 1 path = os.path.abspath(path) if not os.path.isfile(path): print("no such file: " + path) return 1 return 0 if check_file(path) else 1 if __name__ == "__main__": sys.exit(main(sys.argv))