#!/usr/bin/env python """Compare endpoint_diff captures byte-for-byte and write a verdict report. Three-capture (A-B-A) analysis. The captures are taken in this time order: after_extract server at (t0, during manifest extraction) before server at (t1) after_replay server at (t2) so the two captures *bracket* the "before" capture in time. Per request, with body sha256s b=before a=after_replay x=after_extract: * byte-identical <=> b == a * reproduced itself <=> a == x (and x not flagged intra-run-flaky) A before/after difference is attributed to the **commit range** only when the after-ref reproduced itself (a == x): then the data and the harness were stable across the window, so the only thing that changed is the code. If a != x as well, the endpoint is intrinsically nondeterministic or the warehouse data drifted, and the before/after difference cannot be blamed on the commit range. Usage: compare.py [--out _out] [--before before] [--after after_replay] [--baseline after_extract] Exit code is 0 unless there is at least one commit-attributable difference. """ import argparse import difflib import json import os import sys from collections import Counter from urllib.parse import urlsplit HERE = os.path.dirname(os.path.abspath(__file__)) def load_capture(out_dir, label): path = os.path.join(out_dir, f"capture_{label}.json") if not os.path.exists(path): return None with open(path) as fh: return json.load(fh) def parse_body(resp): """Return (json_or_None, parse_ok).""" body = resp.get("body") if body is None: return None, False try: return json.loads(body), True except (ValueError, TypeError): return None, False def classify(resp_a, resp_b): """Classify how response A differs from response B (assumed not byte-equal). Uses the canonical-form hashes captured by replay.py / build_manifest.py -- ``sha256_canon`` (equal iff the JSON value is equal) and ``sha256_sorted`` (equal iff the data matches up to array element-order) -- so no response body is needed. Returns one of: STATUS, WHITESPACE, ARRAY-ORDER, CONTENT, NON-JSON, ERROR. """ if resp_a.get("error") or resp_b.get("error"): return "ERROR" if resp_a.get("status") != resp_b.get("status"): return "STATUS" canon_a, canon_b = resp_a.get("sha256_canon"), resp_b.get("sha256_canon") if canon_a is None or canon_b is None: return "NON-JSON" # body did not parse as JSON on one/both sides if canon_a == canon_b: return "WHITESPACE" # equal JSON value; only bytes/key-order differ if resp_a.get("sha256_sorted") == resp_b.get("sha256_sorted"): return "ARRAY-ORDER" # equal once every list is sorted return "CONTENT" def body_diff_snippet(resp_a, resp_b, label_a, label_b, max_lines=40): ja, ok_a = parse_body(resp_a) jb, ok_b = parse_body(resp_b) if ok_a and ok_b: text_a = json.dumps(ja, indent=2, sort_keys=True).splitlines() text_b = json.dumps(jb, indent=2, sort_keys=True).splitlines() else: text_a = (resp_a.get("body") or "").splitlines() text_b = (resp_b.get("body") or "").splitlines() diff = list( difflib.unified_diff(text_a, text_b, fromfile=label_a, tofile=label_b, n=2) ) if len(diff) > max_lines: diff = diff[:max_lines] + [f"... ({len(diff) - max_lines} more diff lines)"] return "\n".join(diff) def short_request(req): u = urlsplit(req["url"]) path = u.path + (("?" + u.query) if u.query else "") if len(path) > 96: path = path[:93] + "..." extra = "" if req.get("json") is not None: extra = f" json={json.dumps(req['json'], sort_keys=True)}" return f"{req['method']} {path}{extra}" def main(): ap = argparse.ArgumentParser() ap.add_argument("--out", default=os.path.join(HERE, "_out")) ap.add_argument("--before", default="before") ap.add_argument("--after", default="after_replay") ap.add_argument("--baseline", default="after_extract") args = ap.parse_args() with open(os.path.join(args.out, "manifest.json")) as fh: manifest = json.load(fh) requests_by_id = {r["id"]: r for r in manifest["requests"]} cap_before = load_capture(args.out, args.before) cap_after = load_capture(args.out, args.after) cap_base = load_capture(args.out, args.baseline) if cap_before is None or cap_after is None: raise SystemExit( f"need capture_{args.before}.json and capture_{args.after}.json " f"in {args.out}" ) resp_before = cap_before["responses"] resp_after = cap_after["responses"] resp_base = cap_base["responses"] if cap_base else {} # replay.py stores response bodies only under --keep-bodies; without them the # report still classifies every diff from hashes but cannot show line diffs. have_bodies = bool(cap_before["meta"].get("bodies")) and bool( cap_after["meta"].get("bodies") ) rows = [] for rid in sorted(requests_by_id): rb = resp_before.get(rid) ra = resp_after.get(rid) rx = resp_base.get(rid) if rb is None or ra is None: rows.append({"id": rid, "category": "MISSING", "attribution": "-"}) continue byte_identical = ( rb.get("sha256") == ra.get("sha256") and rb.get("status") == ra.get("status") and not rb.get("error") and not ra.get("error") ) # after-ref self-consistency across the bracket (t0 vs t2) after_self_consistent = None if rx is not None: after_self_consistent = ( ra.get("sha256") == rx.get("sha256") and ra.get("status") == rx.get("status") and not ra.get("error") and not rx.get("error") and not rx.get("intra_run_distinct_sha") ) if byte_identical: category = "IDENTICAL" attribution = "-" else: category = classify(rb, ra) if after_self_consistent is True: attribution = "COMMIT" # data+harness stable -> code changed it elif after_self_consistent is False: attribution = "FLAKY" # the after-ref differs from itself too else: attribution = "UNKNOWN" # no baseline capture supplied rows.append( { "id": rid, "category": category, "attribution": attribution, "byte_identical": byte_identical, "after_self_consistent": after_self_consistent, "status_before": rb.get("status"), "status_after": ra.get("status"), "len_before": rb.get("len"), "len_after": ra.get("len"), "sha_before": rb.get("sha256"), "sha_after": ra.get("sha256"), } ) total = len(rows) cat_counts = Counter(r["category"] for r in rows) n_identical = cat_counts.get("IDENTICAL", 0) diffs = [r for r in rows if r["category"] not in ("IDENTICAL", "MISSING")] n_commit = sum(1 for r in diffs if r["attribution"] == "COMMIT") n_flaky = sum(1 for r in diffs if r["attribution"] == "FLAKY") n_unknown = sum(1 for r in diffs if r["attribution"] == "UNKNOWN") # A commit-attributed ARRAY-ORDER / WHITESPACE diff carries identical data # (only the byte serialization differs); only a content or status diff is a # real behaviour change. DATA_CATEGORIES = ("CONTENT", "STATUS", "NON-JSON", "ERROR") n_commit_data = sum( 1 for r in diffs if r["attribution"] == "COMMIT" and r["category"] in DATA_CATEGORIES ) n_commit_order = n_commit - n_commit_data # after-vs-after determinism floor (after_replay vs after_extract) after_floor = None if resp_base: same = diff = 0 for rid in requests_by_id: ra, rx = resp_after.get(rid), resp_base.get(rid) if ra is None or rx is None: continue if ( ra.get("sha256") == rx.get("sha256") and ra.get("status") == rx.get("status") and not rx.get("intra_run_distinct_sha") ): same += 1 else: diff += 1 after_floor = (same, diff) # ---- verdict ----------------------------------------------------------- b_rev = (cap_before["meta"].get("git_rev") or "?")[:12] a_rev = (cap_after["meta"].get("git_rev") or "?")[:12] if total == n_identical: verdict = ( f"PERFECT -- every one of the {total} requests is byte-for-byte " f"identical between `{b_rev}` and `{a_rev}`. The commit range " f"changes no endpoint response." ) emoji, exit_code = "PASS", 0 elif n_commit_data == 0: verdict = ( f"PASS -- the commit range changes no response DATA. " f"{n_identical}/{total} requests are byte-for-byte identical; " f"{len(diffs)} differ. Of the differences, {n_commit_order} are " f"commit-attributed but array element-ordering ONLY (identical data " f"-- these queries carry no total ORDER BY), and {n_flaky} differ " f"nondeterministically (the same difference appears after-vs-after). " f"0 commit-attributable content or status changes." ) emoji, exit_code = "PASS", 0 else: verdict = ( f"DIFFERENCES FOUND -- {n_commit_data} request(s) have a " f"commit-attributable content/status change (the after-ref " f"reproduced itself across the bracket, so the code is the cause). " f"{n_identical}/{total} byte-identical; {n_commit_order} " f"commit-attributed order-only; {n_flaky} nondeterministic." ) emoji, exit_code = "FAIL", 1 # ---- markdown report --------------------------------------------------- lines = [] lines.append("# endpoint_diff report") lines.append("") lines.append(f"**Verdict ({emoji}):** {verdict}") lines.append("") lines.append("## Captures") lines.append("") lines.append("| label | mechanism | git rev | when | requests |") lines.append("|---|---|---|---|---|") for cap, lbl in ( (cap_base, args.baseline), (cap_before, args.before), (cap_after, args.after), ): if cap is None: continue m = cap["meta"] lines.append( f"| {lbl} | {m.get('mechanism')} | `{(m.get('git_rev') or '?')[:12]}` " f"| {m.get('finished_at', '?')} | {m.get('request_count')} |" ) lines.append("") lines.append("## after-vs-after determinism floor") lines.append("") if after_floor: same, diff = after_floor lines.append( f"`{args.after}` vs `{args.baseline}` (both at the after-ref, " f"bracketing the `before` capture in time): " f"**{same} identical, {diff} differ**. " + ( "The two after-ref captures are byte-identical, so the window " "had zero data drift -- any before/after difference is purely " "the code." if diff == 0 else f"{diff} endpoint(s) are nondeterministic on a fixed build; " "before/after differences on those ids are discounted." ) ) else: lines.append("_No baseline capture supplied -- attribution is UNKNOWN._") lines.append("") lines.append("## before vs after") lines.append("") lines.append(f"- total requests compared: **{total}**") lines.append(f"- byte-for-byte identical: **{n_identical}**") lines.append(f"- differ: **{len(diffs)}**") lines.append( f" - commit-attributable **content/status** change: " f"**{n_commit_data}**" ) lines.append( f" - commit-attributable **array-order only** (identical " f"data): **{n_commit_order}**" ) lines.append( f" - nondeterministic (also differs after-vs-after): " f"**{n_flaky}**" ) if n_unknown: lines.append(f" - unknown (no baseline capture): **{n_unknown}**") lines.append("") lines.append("category breakdown of differing requests:") lines.append("") lines.append("| category | count | meaning |") lines.append("|---|---|---|") meaning = { "WHITESPACE": "same JSON value, only byte serialization/key order differs", "ARRAY-ORDER": "equal once every list is sorted (row-order nondeterminism)", "CONTENT": "genuinely different JSON content", "STATUS": "different HTTP status code", "NON-JSON": "non-JSON bodies that differ", "ERROR": "a transport error on one side", "MISSING": "request absent from a capture", } for cat, cnt in sorted(cat_counts.items(), key=lambda kv: -kv[1]): if cat == "IDENTICAL": continue lines.append(f"| {cat} | {cnt} | {meaning.get(cat, '')} |") lines.append("") if diffs: lines.append("## Differing requests") lines.append("") lines.append( "| id | attribution | category | status b/a | bytes b/a | request |" ) lines.append("|---|---|---|---|---|---|") for r in sorted( diffs, key=lambda r: (r["attribution"] != "COMMIT", r["category"], r["id"]) ): req = requests_by_id[r["id"]] lines.append( f"| {r['id']} | {r['attribution']} | {r['category']} " f"| {r['status_before']}/{r['status_after']} " f"| {r['len_before']}/{r['len_after']} | {short_request(req)} |" ) lines.append("") # full body diffs for the requests that matter most focus = [r for r in diffs if r["attribution"] == "COMMIT"] or [ r for r in diffs if r["category"] in ("CONTENT", "STATUS", "NON-JSON") ] if focus and not have_bodies: lines.append("## Body diffs") lines.append("") lines.append( "_Captures store hashes only -- re-run the `before` and `after` " "replays with `replay.py --keep-bodies` for line-level body " "diffs of these requests._" ) lines.append("") elif focus: lines.append("## Body diffs") lines.append("") for r in sorted(focus, key=lambda r: r["id"])[:60]: req = requests_by_id[r["id"]] lines.append(f"### {r['id']} ({r['attribution']} / {r['category']})") lines.append("") lines.append(f"`{short_request(req)}`") lines.append("") snippet = body_diff_snippet( resp_before[r["id"]], resp_after[r["id"]], args.before, args.after ) lines.append("```diff") lines.append(snippet or "(no line-level diff)") lines.append("```") lines.append("") report_md = "\n".join(lines) + "\n" with open(os.path.join(args.out, "report.md"), "w") as fh: fh.write(report_md) with open(os.path.join(args.out, "report.json"), "w") as fh: json.dump( { "verdict": verdict, "emoji": emoji, "total": total, "identical": n_identical, "differ": len(diffs), "commit_attributable": n_commit, "flaky": n_flaky, "unknown": n_unknown, "after_floor": after_floor, "rows": rows, }, fh, indent=2, sort_keys=True, ) print(report_md) print(f"wrote {args.out}/report.md and report.json") sys.exit(exit_code) if __name__ == "__main__": main()