Skip to content

Commit 8e38881

Browse files
committed
feat(cli): Add diff_opcode_counts command
1 parent 15325d4 commit 8e38881

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ fillerconvert = "cli.fillerconvert.fillerconvert:main"
106106
groupstats = "cli.show_pre_alloc_group_stats:main"
107107
extract_config = "cli.extract_config:extract_config"
108108
compare_fixtures = "cli.compare_fixtures:main"
109+
diff_opcode_counts = "cli.diff_opcode_counts:main"
109110

110111
[tool.setuptools.packages.find]
111112
where = ["src"]

src/cli/diff_opcode_counts.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#!/usr/bin/env python
2+
"""
3+
Compare opcode counts between two folders of JSON fixtures.
4+
5+
This script crawls two folders for JSON files, parses them using the Fixtures model,
6+
and compares the opcode_count field from the info section between fixtures with the same name.
7+
"""
8+
9+
import sys
10+
from pathlib import Path
11+
from typing import Dict, List, Optional
12+
13+
import click
14+
15+
from ethereum_clis.types import OpcodeCount
16+
from ethereum_test_fixtures.file import Fixtures
17+
18+
19+
def find_json_files(directory: Path) -> List[Path]:
20+
"""Find all JSON files in a directory, excluding index.json files."""
21+
json_files = []
22+
if directory.is_dir():
23+
for file_path in directory.rglob("*.json"):
24+
if file_path.name != "index.json":
25+
json_files.append(file_path)
26+
return json_files
27+
28+
29+
def load_fixtures_from_file(
30+
file_path: Path, remove_from_fixture_names: List[str]
31+
) -> Optional[Fixtures]:
32+
"""Load fixtures from a JSON file using the Fixtures model."""
33+
try:
34+
fixtures = Fixtures.model_validate_json(file_path.read_text())
35+
renames = []
36+
for k in fixtures.root:
37+
new_name = None
38+
for s in remove_from_fixture_names:
39+
if s in k:
40+
if new_name is None:
41+
new_name = k.replace(s, "")
42+
else:
43+
new_name = new_name.replace(s, "")
44+
if new_name is not None:
45+
renames.append((k, new_name))
46+
for old_name, new_name in renames:
47+
fixtures.root[new_name] = fixtures.root.pop(old_name)
48+
return fixtures
49+
except Exception as e:
50+
print(f"Error loading {file_path}: {e}", file=sys.stderr)
51+
return None
52+
53+
54+
def extract_opcode_counts_from_fixtures(fixtures: Fixtures) -> Dict[str, OpcodeCount]:
55+
"""Extract opcode_count from info field for each fixture."""
56+
opcode_counts = {}
57+
for fixture_name, fixture in fixtures.items():
58+
if hasattr(fixture, "info") and fixture.info and "opcode_count" in fixture.info:
59+
try:
60+
opcode_count = OpcodeCount.model_validate(fixture.info["opcode_count"])
61+
opcode_counts[fixture_name] = opcode_count
62+
except Exception as e:
63+
print(f"Error parsing opcode_count for {fixture_name}: {e}", file=sys.stderr)
64+
return opcode_counts
65+
66+
67+
def load_all_opcode_counts(
68+
directory: Path, remove_from_fixture_names: List[str]
69+
) -> Dict[str, OpcodeCount]:
70+
"""Load all opcode counts from all JSON files in a directory."""
71+
all_opcode_counts = {}
72+
json_files = find_json_files(directory)
73+
74+
for json_file in json_files:
75+
fixtures = load_fixtures_from_file(
76+
json_file, remove_from_fixture_names=remove_from_fixture_names
77+
)
78+
if fixtures:
79+
file_opcode_counts = extract_opcode_counts_from_fixtures(fixtures)
80+
# Use fixture name as key, if there are conflicts, the last one wins
81+
all_opcode_counts.update(file_opcode_counts)
82+
83+
return all_opcode_counts
84+
85+
86+
def compare_opcode_counts(count1: OpcodeCount, count2: OpcodeCount) -> Dict[str, int]:
87+
"""Compare two opcode counts and return the differences."""
88+
differences = {}
89+
90+
# Get all unique opcodes from both counts
91+
all_opcodes = set(count1.root.keys()) | set(count2.root.keys())
92+
93+
for opcode in all_opcodes:
94+
val1 = count1.root.get(opcode, 0)
95+
val2 = count2.root.get(opcode, 0)
96+
diff = val2 - val1
97+
if diff != 0:
98+
differences[str(opcode)] = diff
99+
100+
return differences
101+
102+
103+
@click.command()
104+
@click.argument("base", type=click.Path(exists=True, file_okay=False, path_type=Path))
105+
@click.argument("patch", type=click.Path(exists=True, file_okay=False, path_type=Path))
106+
@click.option(
107+
"--show-common",
108+
is_flag=True,
109+
help="Print fixtures that contain identical opcode counts.",
110+
)
111+
@click.option(
112+
"--show-missing",
113+
is_flag=True,
114+
help="Print fixtures only found in one of the folders.",
115+
)
116+
@click.option(
117+
"--remove-from-fixture-names",
118+
"-r",
119+
multiple=True,
120+
help="String to be removed from the fixture name, in case the fixture names have changed, "
121+
"in order to make the comparison easier. "
122+
"Can be specified multiple times.",
123+
)
124+
def main(
125+
base: Path,
126+
patch: Path,
127+
show_common: bool,
128+
show_missing: bool,
129+
remove_from_fixture_names: List[str],
130+
):
131+
"""Crawl two folders, compare and print the opcode count diffs."""
132+
print(f"Loading opcode counts from {base}...")
133+
opcode_counts1 = load_all_opcode_counts(base, remove_from_fixture_names)
134+
print(f"Found {len(opcode_counts1)} fixtures with opcode counts")
135+
136+
print(f"Loading opcode counts from {patch}...")
137+
opcode_counts2 = load_all_opcode_counts(patch, remove_from_fixture_names)
138+
print(f"Found {len(opcode_counts2)} fixtures with opcode counts")
139+
140+
# Find common fixture names
141+
common_names = set(opcode_counts1.keys()) & set(opcode_counts2.keys())
142+
only_in_1 = set(opcode_counts1.keys()) - set(opcode_counts2.keys())
143+
only_in_2 = set(opcode_counts2.keys()) - set(opcode_counts1.keys())
144+
145+
print("\nSummary:")
146+
print(f" Common fixtures: {len(common_names)}")
147+
print(f" Only in {base.name}: {len(only_in_1)}")
148+
print(f" Only in {patch.name}: {len(only_in_2)}")
149+
150+
# Show missing fixtures if requested
151+
if show_missing:
152+
if only_in_1:
153+
print(f"\nFixtures only in {base.name}:")
154+
for name in sorted(only_in_1):
155+
print(f" {name}")
156+
157+
if only_in_2:
158+
print(f"\nFixtures only in {patch.name}:")
159+
for name in sorted(only_in_2):
160+
print(f" {name}")
161+
162+
# Compare common fixtures
163+
differences_found = False
164+
common_with_same_counts = 0
165+
166+
for fixture_name in sorted(common_names):
167+
count1 = opcode_counts1[fixture_name]
168+
count2 = opcode_counts2[fixture_name]
169+
170+
differences = compare_opcode_counts(count1, count2)
171+
172+
if differences:
173+
differences_found = True
174+
print(f"\n{fixture_name}:")
175+
for opcode, diff in sorted(differences.items()):
176+
if diff > 0:
177+
print(f" +{diff} {opcode}")
178+
else:
179+
print(f" {diff} {opcode}")
180+
elif show_common:
181+
print(f"\n{fixture_name}: No differences")
182+
common_with_same_counts += 1
183+
184+
if not differences_found:
185+
print("\nNo differences found in opcode counts between common fixtures!")
186+
elif show_common:
187+
print(f"\n{common_with_same_counts} fixtures have identical opcode counts")
188+
189+
190+
if __name__ == "__main__":
191+
main()

0 commit comments

Comments
 (0)