From 9c993318c84aaf06bf96ac2a4f508fa17d870994 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 30 Oct 2025 14:46:20 +0100 Subject: [PATCH 1/2] fix(tests): use lru cache for json reads --- tests/json_infra/helpers/__init__.py | 29 ++++++++++++ .../helpers/load_blockchain_tests.py | 46 +++++++++---------- tests/json_infra/helpers/load_state_tests.py | 4 +- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/tests/json_infra/helpers/__init__.py b/tests/json_infra/helpers/__init__.py index 3214c2cc14..88e0857e3b 100644 --- a/tests/json_infra/helpers/__init__.py +++ b/tests/json_infra/helpers/__init__.py @@ -1 +1,30 @@ """Helpers to load tests from JSON files.""" + +import json +from functools import lru_cache +from typing import Dict + + +@lru_cache(maxsize=100) +def load_json_file(test_file: str) -> Dict: + """ + Load and cache a JSON fixture file. + + Uses LRU (Least Recently Used) cache to avoid re-reading the same + JSON files multiple times during test execution. This is especially + important when running tests in parallel, as multiple test cases + from the same file would otherwise cause redundant file I/O. + + The cache is bounded to 100 files, which provides a good balance + between memory usage and performance. When the cache is full, the + least recently accessed file will be evicted. + + Args: + test_file: Path to the JSON fixture file + + Returns: + Parsed JSON data as a dictionary + + """ + with open(test_file, "r") as fp: + return json.load(fp) diff --git a/tests/json_infra/helpers/load_blockchain_tests.py b/tests/json_infra/helpers/load_blockchain_tests.py index 990e941a35..fd478e7b36 100644 --- a/tests/json_infra/helpers/load_blockchain_tests.py +++ b/tests/json_infra/helpers/load_blockchain_tests.py @@ -1,7 +1,6 @@ """Helpers to load and run blockchain tests from JSON files.""" import importlib -import json import os.path from glob import glob from typing import Any, Dict, Generator @@ -19,6 +18,7 @@ from ethereum_spec_tools.evm_tools.loaders.fixture_loader import Load from .. import FORKS +from . import load_json_file from .exceptional_test_patterns import exceptional_blockchain_test_patterns @@ -34,8 +34,7 @@ def run_blockchain_st_test(test_case: Dict, load: Load) -> None: test_file = test_case["test_file"] test_key = test_case["test_key"] - with open(test_file, "r") as fp: - data = json.load(fp) + data = load_json_file(test_file) json_data = data[test_key] @@ -140,27 +139,26 @@ def load_json_fixture(test_file: str, json_fork: str) -> Generator: # Ex: Extract "world.json" from "path/to/file/world.json" # Extract the filename without the extension. Ex: Extract "world" from # "world.json" - with open(test_file, "r") as fp: - data = json.load(fp) - - # Search tests by looking at the `network` attribute - found_keys = [] - for key, test in data.items(): - if "network" not in test: - continue - - if test["network"] == json_fork: - found_keys.append(key) - - if not any(found_keys): - raise NoTestsFoundError - - for _key in found_keys: - yield { - "test_file": test_file, - "test_key": _key, - "json_fork": json_fork, - } + data = load_json_file(test_file) + + # Search tests by looking at the `network` attribute + found_keys = [] + for key, test in data.items(): + if "network" not in test: + continue + + if test["network"] == json_fork: + found_keys.append(key) + + if not any(found_keys): + raise NoTestsFoundError + + for _key in found_keys: + yield { + "test_file": test_file, + "test_key": _key, + "json_fork": json_fork, + } def fetch_blockchain_tests( diff --git a/tests/json_infra/helpers/load_state_tests.py b/tests/json_infra/helpers/load_state_tests.py index 37e6813402..ccd5b4e552 100644 --- a/tests/json_infra/helpers/load_state_tests.py +++ b/tests/json_infra/helpers/load_state_tests.py @@ -16,6 +16,7 @@ from ethereum_spec_tools.evm_tools.t8n import T8N from .. import FORKS +from . import load_json_file from .exceptional_test_patterns import exceptional_state_test_patterns parser = create_parser() @@ -79,8 +80,7 @@ def run_state_test(test_case: Dict[str, str]) -> None: test_key = test_case["test_key"] index = test_case["index"] json_fork = test_case["json_fork"] - with open(test_file) as f: - tests = json.load(f) + tests = load_json_file(test_file) env = tests[test_key]["env"] try: From 6115b7b6cefadfca53768d7bb0ee38a5d28e8813 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Mon, 3 Nov 2025 13:44:36 +0100 Subject: [PATCH 2/2] fix(tests): add xdist group marker based on fixture file --- tests/json_infra/helpers/load_blockchain_tests.py | 12 +++++++++--- tests/json_infra/helpers/load_state_tests.py | 8 ++++++-- tox.ini | 2 ++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/json_infra/helpers/load_blockchain_tests.py b/tests/json_infra/helpers/load_blockchain_tests.py index fd478e7b36..e24ca93f83 100644 --- a/tests/json_infra/helpers/load_blockchain_tests.py +++ b/tests/json_infra/helpers/load_blockchain_tests.py @@ -200,18 +200,24 @@ def fetch_blockchain_tests( + ")" ) _test_case["eels_fork"] = eels_fork + + # Build marks list with xdist_group for all tests + marks = [pytest.mark.xdist_group(_test_case["test_file"])] + if any( x.search(_identifier) for x in test_patterns.expected_fail ): continue elif any(x.search(_identifier) for x in test_patterns.slow): - yield pytest.param(_test_case, marks=pytest.mark.slow) + marks.append(pytest.mark.slow) + yield pytest.param(_test_case, marks=marks) elif any( x.search(_identifier) for x in test_patterns.big_memory ): - yield pytest.param(_test_case, marks=pytest.mark.bigmem) + marks.append(pytest.mark.bigmem) + yield pytest.param(_test_case, marks=marks) else: - yield _test_case + yield pytest.param(_test_case, marks=marks) except NoTestsFoundError: # file doesn't contain tests for the given fork continue diff --git a/tests/json_infra/helpers/load_state_tests.py b/tests/json_infra/helpers/load_state_tests.py index ccd5b4e552..7492eef7d0 100644 --- a/tests/json_infra/helpers/load_state_tests.py +++ b/tests/json_infra/helpers/load_state_tests.py @@ -54,10 +54,14 @@ def fetch_state_tests(json_fork: str) -> Generator: "json_fork": json_fork, } + # Build marks list with xdist_group for all tests + marks = [pytest.mark.xdist_group(test_case_dict["test_file"])] + if any(x.search(test_case.key) for x in test_patterns.slow): - yield pytest.param(test_case_dict, marks=pytest.mark.slow) + marks.append(pytest.mark.slow) + yield pytest.param(test_case_dict, marks=marks) else: - yield test_case_dict + yield pytest.param(test_case_dict, marks=marks) def idfn(test_case: Dict) -> str: diff --git a/tox.ini b/tox.ini index e52433e286..e34bbf25ca 100644 --- a/tox.ini +++ b/tox.ini @@ -57,6 +57,7 @@ commands = --cov-branch \ --ignore-glob='tests/json_infra/fixtures/*' \ --basetemp="{temp_dir}/pytest" \ + --dist=loadgroup \ tests/json_infra [testenv:py3] @@ -105,6 +106,7 @@ commands = --ignore-glob='eest_tests/*' \ --basetemp="{temp_dir}/pytest" \ --optimized \ + --dist=loadgroup \ tests/json_infra [testenv:doc]