File size: 2,979 Bytes
350392a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
"""
Compare two render-model JSON snapshots and report differences.
"""

import argparse
import json
import math
import sys
from pathlib import Path
from typing import Any, List


def load_json(path: Path) -> Any:
    with path.open("r", encoding="utf-8") as f:
        return json.load(f)


def is_number(value: Any) -> bool:
    return isinstance(value, (int, float)) and not isinstance(value, bool)


def nearly_equal(a: float, b: float, tol: float) -> bool:
    if math.isnan(a) and math.isnan(b):
        return True
    return abs(a - b) <= tol


def compare(a: Any, b: Any, path: str, diffs: List[str], tol: float, max_diffs: int) -> None:
    if len(diffs) >= max_diffs:
        return

    if type(a) != type(b):
        diffs.append(f"{path}: type {type(a).__name__} != {type(b).__name__}")
        return

    if isinstance(a, dict):
        a_keys = set(a.keys())
        b_keys = set(b.keys())
        for key in sorted(a_keys - b_keys):
            diffs.append(f"{path}.{key}: missing in candidate")
            if len(diffs) >= max_diffs:
                return
        for key in sorted(b_keys - a_keys):
            diffs.append(f"{path}.{key}: extra in candidate")
            if len(diffs) >= max_diffs:
                return
        for key in sorted(a_keys & b_keys):
            compare(a[key], b[key], f"{path}.{key}", diffs, tol, max_diffs)
            if len(diffs) >= max_diffs:
                return
        return

    if isinstance(a, list):
        if len(a) != len(b):
            diffs.append(f"{path}: list length {len(a)} != {len(b)}")
        min_len = min(len(a), len(b))
        for idx in range(min_len):
            compare(a[idx], b[idx], f"{path}[{idx}]", diffs, tol, max_diffs)
            if len(diffs) >= max_diffs:
                return
        return

    if is_number(a) and is_number(b) and tol > 0:
        if not nearly_equal(float(a), float(b), tol):
            diffs.append(f"{path}: {a} != {b} (tol={tol})")
        return

    if a != b:
        diffs.append(f"{path}: {a!r} != {b!r}")


def main() -> int:
    parser = argparse.ArgumentParser(description="Compare render-model JSON snapshots.")
    parser.add_argument("--baseline", type=Path, required=True, help="Baseline JSON path")
    parser.add_argument("--candidate", type=Path, required=True, help="Candidate JSON path")
    parser.add_argument("--float-tol", type=float, default=0.0, help="Float comparison tolerance")
    parser.add_argument("--max-diffs", type=int, default=200, help="Max diffs to display")
    args = parser.parse_args()

    baseline = load_json(args.baseline)
    candidate = load_json(args.candidate)

    diffs: List[str] = []
    compare(baseline, candidate, "$", diffs, args.float_tol, args.max_diffs)

    if diffs:
        print(f"Differences found: {len(diffs)}")
        for line in diffs:
            print(line)
        return 1

    print("No differences found.")
    return 0


if __name__ == "__main__":
    sys.exit(main())