"""UX-13: record every `validate_startup_config()` call the test suite makes, and compare two validators over those calls. python3 tools/ux13_validator_calls.py record OUT.jsonl.gz python3 tools/ux13_validator_calls.py compare OLD_APP_PARENT OUT.jsonl.gz `record` runs each test_*.py with the validator wrapped (in this directory, so run it where the suite runs: the CI image). `compare` runs the recorded calls through the validator under OLD_APP_PARENT (a directory holding an `app/`, e.g. from `git archive goodwe_controller/app`) and through this tree's, and prints every call whose refusals differ. P0 of UX-13 used it to show that adding the warning tier changed no refusal. scenarios/ux13/ holds the record test_uimodel.py round-trips. """ import gzip import json import os import subprocess import sys HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _WRAP = r""" import json, runpy, sys sys.path.insert(0, ".") from app import validate real = validate.validate_startup_config out = open(sys.argv[1], "a") def rec(opts, dev_min=None, dev_max=None, *a, **kw): try: out.write(json.dumps({"opts": opts, "dev_min": dev_min, "dev_max": dev_max}) + "\n") except (TypeError, ValueError): pass return real(opts, dev_min, dev_max, *a, **kw) validate.validate_startup_config = rec sys.argv = sys.argv[2:] try: runpy.run_path(sys.argv[0], run_name="__main__") except SystemExit: pass """ def record(out_path): tmp = out_path + ".part" open(tmp, "w").close() rows, seen = [], set() for name in sorted(f for f in os.listdir(HERE) if f.startswith("test_") and f.endswith(".py")): before = sum(1 for _ in open(tmp)) subprocess.run([sys.executable, "-B", "-c", _WRAP, tmp, name], cwd=HERE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) with open(tmp) as fh: for line in list(fh)[before:]: call = json.loads(line) key = json.dumps(call, sort_keys=True) if key not in seen: seen.add(key) rows.append(dict(call, source=name)) os.remove(tmp) with gzip.open(out_path, "wt") as fh: for row in rows: fh.write(json.dumps(row, sort_keys=True) + "\n") print(f"{len(rows)} distinct validator calls -> {out_path}") def _run(app_parent, path): code = r""" import gzip, json, logging, sys logging.disable(logging.CRITICAL) sys.path.insert(0, sys.argv[1]) from app.validate import validate_startup_config as v out = [] for line in gzip.open(sys.argv[2], "rt"): c = json.loads(line) try: out.append(v(c["opts"], c["dev_min"], c["dev_max"])) except Exception as e: out.append("raised " + type(e).__name__) print(json.dumps(out)) """ got = subprocess.run([sys.executable, "-B", "-c", code, app_parent, path], capture_output=True, text=True, check=True) return json.loads(got.stdout) def compare(old_parent, path): old, new = _run(old_parent, path), _run(HERE, path) differ = [i for i, (a, b) in enumerate(zip(old, new)) if a != b] print(f"{len(old)} calls, {sum(1 for r in old if r)} refused by the old " f"validator, {len(differ)} with different refusals") for i in differ: print(f" call {i}:\n old {old[i]}\n new {new[i]}") return 1 if differ else 0 if __name__ == "__main__": if len(sys.argv) == 3 and sys.argv[1] == "record": record(sys.argv[2]) elif len(sys.argv) == 4 and sys.argv[1] == "compare": sys.exit(compare(sys.argv[2], sys.argv[3])) else: sys.exit(__doc__)