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