Skip to content

Commit e7e4d1e

Browse files
committed
feat: scripts for regression testing
1 parent f659f04 commit e7e4d1e

2 files changed

Lines changed: 189 additions & 0 deletions

File tree

script/diffjson.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import json
4+
import sys
5+
from pathlib import Path
6+
from typing import Literal
7+
8+
from deepdiff import DeepDiff
9+
10+
# Define status types for clarity
11+
Status = Literal["OK", "BAD", "FILE_ERROR"]
12+
13+
14+
def format_diff_custom(diff: DeepDiff) -> str:
15+
"""
16+
Formats a DeepDiff object into a custom human-readable string.
17+
This provides a clear, indented view of changes.
18+
"""
19+
output = []
20+
21+
# Helper to format a value for printing. Pretty-prints dicts/lists.
22+
def format_value(value):
23+
if isinstance(value, (dict, list)):
24+
return json.dumps(value, indent=2)
25+
return repr(value)
26+
27+
# Handle changed values
28+
if "values_changed" in diff:
29+
for path, changes in diff["values_changed"].items():
30+
output.append(f"Value Changed at: {path}")
31+
output.append(f" - old: {format_value(changes['old_value'])}")
32+
output.append(f" + new: {format_value(changes['new_value'])}")
33+
output.append("--------------------")
34+
35+
# Handle added items to lists/sets
36+
if "iterable_item_added" in diff:
37+
for path, value in diff["iterable_item_added"].items():
38+
output.append(f"Item Added at: {path}")
39+
output.append(f" + new: {format_value(value)}")
40+
output.append("--------------------")
41+
42+
# Handle removed items from lists/sets
43+
if "iterable_item_removed" in diff:
44+
for path, value in diff["iterable_item_removed"].items():
45+
output.append(f"Item Removed at: {path}")
46+
output.append(f" - old: {format_value(value)}")
47+
output.append("--------------------")
48+
49+
# Handle added keys in dictionaries
50+
if "dictionary_item_added" in diff:
51+
for path in diff["dictionary_item_added"]:
52+
output.append(f"Dictionary Key Added: {path}")
53+
output.append("--------------------")
54+
55+
# Handle removed keys in dictionaries
56+
if "dictionary_item_removed" in diff:
57+
for path in diff["dictionary_item_removed"]:
58+
output.append(f"Dictionary Key Removed: {path}")
59+
output.append("--------------------")
60+
61+
# Clean up the last separator for a tidy output
62+
if output and output[-1] == "--------------------":
63+
output.pop()
64+
65+
return "\n".join(output)
66+
67+
68+
def compare_json_files(file1_path: Path, file2_path: Path) -> Status:
69+
"""
70+
Compares the content of two JSON files without printing output.
71+
72+
Returns:
73+
"OK" if they match, "BAD" if they don't, "FILE_ERROR" on read/parse error.
74+
"""
75+
try:
76+
with open(file1_path, "r", encoding="utf-8") as f1:
77+
json1 = json.load(f1)
78+
with open(file2_path, "r", encoding="utf-8") as f2:
79+
json2 = json.load(f2)
80+
except (FileNotFoundError, json.JSONDecodeError):
81+
return "FILE_ERROR"
82+
83+
diff = DeepDiff(json1, json2, ignore_order=True)
84+
85+
return "BAD" if diff else "OK"
86+
87+
88+
def process_directory_comparison(old_dir: Path, new_dir: Path) -> bool:
89+
"""
90+
Compares JSON files across two directories and prints results in a list format.
91+
"""
92+
results: dict[str, list[str]] = {"OK": [], "BAD": [], "MISS": [], "NEW": []}
93+
old_files = {p.name for p in old_dir.glob("*.json")}
94+
new_files = {p.name for p in new_dir.glob("*.json")}
95+
96+
for filename in sorted(old_files.intersection(new_files)):
97+
status = compare_json_files(old_dir / filename, new_dir / filename)
98+
results["BAD" if status != "OK" else "OK"].append(filename)
99+
100+
for filename in sorted(old_files - new_files):
101+
results["MISS"].append(filename)
102+
103+
for filename in sorted(new_files - old_files):
104+
results["NEW"].append(filename)
105+
106+
for filename in results["OK"]:
107+
print(f"[OK ] {filename}")
108+
for filename in results["NEW"]:
109+
print(f"[NEW ] {filename}")
110+
for filename in results["BAD"]:
111+
print(f"[BAD ] {filename}", file=sys.stderr)
112+
for filename in results["MISS"]:
113+
print(f"[MISS] {filename}", file=sys.stderr)
114+
115+
return bool(results["BAD"] or results["MISS"])
116+
117+
118+
def main():
119+
parser = argparse.ArgumentParser(
120+
description="Compare two JSON files or two directories of JSON files."
121+
)
122+
parser.add_argument(
123+
"path1", type=Path, help="Path to the first file or 'old' directory."
124+
)
125+
parser.add_argument(
126+
"path2", type=Path, help="Path to the second file or 'new' directory."
127+
)
128+
args = parser.parse_args()
129+
130+
path1, path2 = args.path1, args.path2
131+
132+
if not path1.exists() or not path2.exists():
133+
print(
134+
f"Error: Path does not exist: {path1 if not path1.exists() else path2}",
135+
file=sys.stderr,
136+
)
137+
return 1
138+
139+
# --- Handle Directory Comparison ---
140+
if path1.is_dir() and path2.is_dir():
141+
print(f"Comparing directories:\n- Old: {path1}\n- New: {path2}\n")
142+
if process_directory_comparison(path1, path2):
143+
print("\nComparison finished with errors.", file=sys.stderr)
144+
return 1
145+
else:
146+
print("\nComparison finished successfully.")
147+
return 0
148+
149+
# --- Handle Single File Comparison ---
150+
elif path1.is_file() and path2.is_file():
151+
try:
152+
with open(path1, "r", encoding="utf-8") as f1:
153+
json1 = json.load(f1)
154+
with open(path2, "r", encoding="utf-8") as f2:
155+
json2 = json.load(f2)
156+
except (FileNotFoundError, json.JSONDecodeError) as e:
157+
print(f"Error reading or parsing file: {e}", file=sys.stderr)
158+
return 1
159+
160+
diff = DeepDiff(json1, json2, ignore_order=True)
161+
162+
if diff:
163+
print(
164+
f"Differences found between '{path1.name}' and '{path2.name}':\n",
165+
file=sys.stderr,
166+
)
167+
# Format the diff into a custom readable format and print to stderr
168+
custom_output = format_diff_custom(diff)
169+
print(custom_output, file=sys.stderr)
170+
return 1
171+
else:
172+
print(f"Files '{path1.name}' and '{path2.name}' are identical.")
173+
return 0
174+
175+
# --- Handle Invalid Input ---
176+
else:
177+
print(
178+
"Error: Both arguments must be files or both must be directories.",
179+
file=sys.stderr,
180+
)
181+
return 1
182+
183+
184+
if __name__ == "__main__":
185+
sys.exit(main())
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ detect_jobs() {
2727
done
2828
}
2929

30+
if [[ ! -x "$ABCEXE" ]]; then
31+
echo "Error: The specified abcoder executable '$ABCEXE' does not exist or is not executable." >&2
32+
exit 1
33+
fi
3034
mkdir -pv "$OUTDIR"
3135
detect_jobs | parallel $PARALLEL_FLAGS echo {}
3236
echo

0 commit comments

Comments
 (0)