Skip to content

Commit 057fe10

Browse files
committed
fix(tests): revamp cache
fix(tests): Don't cache fixtures Try to implement cache Fix caching feat(tests): Manage cache during execution
1 parent 0fb8a26 commit 057fe10

File tree

6 files changed

+134
-54
lines changed

6 files changed

+134
-54
lines changed

tests/json_infra/conftest.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from requests_cache.backends.sqlite import SQLiteCache
2626

2727
from . import TEST_FIXTURES
28-
from .helpers import FixturesFile
28+
from .helpers import FixturesFile, FixtureTestItem
2929

3030
try:
3131
from xdist import get_xdist_worker_id
@@ -301,3 +301,16 @@ def pytest_collect_file(
301301
if file_path.suffix == ".json":
302302
return FixturesFile.from_parent(parent, path=file_path)
303303
return None
304+
305+
306+
def pytest_runtest_teardown(item: Item, nextitem: Item) -> None:
307+
"""
308+
Drop cache from a `FixtureTestItem` if the next one is not of the
309+
same type or does not belong to the same fixtures file.
310+
"""
311+
if isinstance(item, FixtureTestItem):
312+
if not isinstance(nextitem, FixtureTestItem):
313+
item.fixtures_file.clear_data_cache()
314+
else:
315+
if item.fixtures_file != nextitem.fixtures_file:
316+
item.fixtures_file.clear_data_cache()
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""Helpers to load tests from JSON files."""
22

3-
from .fixtures import ALL_FIXTURE_TYPES, Fixture, FixturesFile
3+
from .fixtures import ALL_FIXTURE_TYPES, Fixture, FixturesFile, FixtureTestItem
44
from .load_blockchain_tests import BlockchainTestFixture
55
from .load_state_tests import StateTestFixture
66

77
ALL_FIXTURE_TYPES.append(BlockchainTestFixture)
88
ALL_FIXTURE_TYPES.append(StateTestFixture)
99

10-
__all__ = ["ALL_FIXTURE_TYPES", "Fixture", "FixturesFile"]
10+
__all__ = ["ALL_FIXTURE_TYPES", "Fixture", "FixturesFile", "FixtureTestItem"]

tests/json_infra/helpers/exceptional_test_patterns.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import re
77
from dataclasses import dataclass
8-
from functools import lru_cache
98
from typing import Pattern, Tuple
109

1110

@@ -21,7 +20,6 @@ class TestPatterns:
2120
big_memory: Tuple[Pattern[str], ...]
2221

2322

24-
@lru_cache
2523
def exceptional_blockchain_test_patterns(
2624
json_fork: str, eels_fork: str
2725
) -> TestPatterns:
@@ -106,7 +104,6 @@ def exceptional_blockchain_test_patterns(
106104
)
107105

108106

109-
@lru_cache
110107
def exceptional_state_test_patterns(
111108
json_fork: str, eels_fork: str
112109
) -> TestPatterns:

tests/json_infra/helpers/fixtures.py

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,24 @@
22

33
import json
44
from abc import ABC, abstractmethod
5+
from functools import cached_property
56
from typing import Any, Dict, Generator, List, Self, Type
67

78
from _pytest.nodes import Node
89
from pytest import Collector, File, Item
910

1011

12+
class FixtureTestItem(Item):
13+
"""
14+
Test item that comes from a fixture file.
15+
"""
16+
17+
@property
18+
def fixtures_file(self) -> "FixturesFile":
19+
"""Return the fixtures file from which the test was extracted."""
20+
raise NotImplementedError()
21+
22+
1123
class Fixture(ABC):
1224
"""
1325
Single fixture from a JSON file.
@@ -18,20 +30,17 @@ class Fixture(ABC):
1830

1931
test_file: str
2032
test_key: str
21-
test_dict: Dict[str, Any]
2233

2334
def __init__(
2435
self,
2536
*args: Any,
2637
test_file: str,
2738
test_key: str,
28-
test_dict: Dict[str, Any],
2939
**kwargs: Any,
3040
):
3141
super().__init__(*args, **kwargs)
3242
self.test_file = test_file
3343
self.test_key = test_key
34-
self.test_dict = test_dict
3544

3645
@classmethod
3746
def from_parent(
@@ -57,30 +66,41 @@ def is_format(cls, test_dict: Dict[str, Any]) -> bool:
5766
class FixturesFile(File):
5867
"""Single JSON file containing fixtures."""
5968

69+
@cached_property
70+
def data(self) -> Dict[str, Any]:
71+
"""Return the JSON data of the full file."""
72+
# loaded once per worker per file (thanks to cached_property)
73+
with self.fspath.open("r", encoding="utf-8") as f:
74+
return json.load(f)
75+
76+
def clear_data_cache(self) -> None:
77+
"""Drop the data cache."""
78+
del self.data
79+
6080
def collect(
6181
self: Self,
6282
) -> Generator[Item | Collector, None, None]:
6383
"""Collect test cases from a single JSON fixtures file."""
64-
with open(self.path, "r") as file:
65-
try:
66-
loaded_file = json.load(file)
67-
except Exception:
68-
return # Skip *.json files that are unreadable.
69-
if not isinstance(loaded_file, dict):
70-
return
71-
for key, test_dict in loaded_file.items():
72-
if not isinstance(test_dict, dict):
84+
try:
85+
loaded_file = self.data
86+
except Exception:
87+
return # Skip *.json files that are unreadable.
88+
if not isinstance(loaded_file, dict):
89+
return
90+
for key, test_dict in loaded_file.items():
91+
if not isinstance(test_dict, dict):
92+
continue
93+
for fixture_type in ALL_FIXTURE_TYPES:
94+
if not fixture_type.is_format(test_dict):
7395
continue
74-
for fixture_type in ALL_FIXTURE_TYPES:
75-
if not fixture_type.is_format(test_dict):
76-
continue
77-
name = key
78-
if "::" in name:
79-
name = name.split("::")[1]
80-
yield fixture_type.from_parent( # type: ignore
81-
parent=self,
82-
name=name,
83-
test_file=str(self.path),
84-
test_key=key,
85-
test_dict=test_dict,
86-
)
96+
name = key
97+
if "::" in name:
98+
name = name.split("::")[1]
99+
yield fixture_type.from_parent( # type: ignore
100+
parent=self,
101+
name=name,
102+
test_file=str(self.path),
103+
test_key=key,
104+
)
105+
# Make sure we don't keep anything from collection in memory.
106+
self.clear_data_cache()

tests/json_infra/helpers/load_blockchain_tests.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from ethereum_rlp import rlp
1010
from ethereum_rlp.exceptions import RLPException
1111
from ethereum_types.numeric import U64
12-
from pytest import Item
1312

1413
from ethereum.crypto.hash import keccak256
1514
from ethereum.exceptions import EthereumException, StateWithEmptyAccount
@@ -18,7 +17,7 @@
1817

1918
from .. import FORKS
2019
from .exceptional_test_patterns import exceptional_blockchain_test_patterns
21-
from .fixtures import Fixture
20+
from .fixtures import Fixture, FixturesFile, FixtureTestItem
2221

2322

2423
class NoTestsFoundError(Exception):
@@ -59,7 +58,7 @@ def add_block_to_chain(
5958
)
6059

6160

62-
class BlockchainTestFixture(Fixture, Item):
61+
class BlockchainTestFixture(Fixture, FixtureTestItem):
6362
"""Single blockchain test fixture from a JSON file."""
6463

6564
fork_name: str
@@ -91,6 +90,20 @@ def __init__(
9190
if any(x.search(_identifier) for x in test_patterns.big_memory):
9291
self.add_marker("bigmem")
9392

93+
@property
94+
def fixtures_file(self) -> FixturesFile:
95+
"""Fixtures file from which the test fixture was collected."""
96+
parent = self.parent
97+
assert parent is not None
98+
assert isinstance(parent, FixturesFile)
99+
return parent
100+
101+
@property
102+
def test_dict(self) -> Dict[str, Any]:
103+
"""Load test from disk."""
104+
loaded_file = self.fixtures_file.data
105+
return loaded_file[self.test_key]
106+
94107
def runtest(self) -> None:
95108
"""Run a blockchain state test from JSON test case data."""
96109
json_data = self.test_dict

tests/json_infra/helpers/load_state_tests.py

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,51 +12,73 @@
1212
from ethereum.exceptions import StateWithEmptyAccount
1313
from ethereum.utils.hexadecimal import hex_to_bytes
1414
from ethereum_spec_tools.evm_tools import create_parser
15-
from ethereum_spec_tools.evm_tools.statetest import TestCase, read_test_case
15+
from ethereum_spec_tools.evm_tools.statetest import read_test_case
1616
from ethereum_spec_tools.evm_tools.t8n import T8N
1717

1818
from .. import FORKS
1919
from .exceptional_test_patterns import (
2020
exceptional_state_test_patterns,
2121
)
22-
from .fixtures import Fixture
22+
from .fixtures import Fixture, FixturesFile, FixtureTestItem
2323

2424
parser = create_parser()
2525

2626

27-
class StateTest(Item):
27+
class StateTest(FixtureTestItem):
2828
"""Single state test case item."""
2929

30-
test_case: TestCase
31-
test_dict: Dict[str, Any]
30+
index: int
31+
fork_name: str
3232

3333
def __init__(
3434
self,
3535
*args: Any,
36-
test_case: TestCase,
37-
test_dict: Dict[str, Any],
36+
index: int,
37+
fork_name: str,
38+
key: str,
3839
**kwargs: Any,
3940
) -> None:
4041
"""Initialize a single test case item."""
4142
super().__init__(*args, **kwargs)
42-
self.test_case = test_case
43-
self.test_dict = test_dict
44-
self.add_marker(pytest.mark.fork(self.test_case.fork_name))
43+
self.index = index
44+
self.fork_name = fork_name
45+
self.add_marker(pytest.mark.fork(self.fork_name))
4546
self.add_marker("evm_tools")
4647
self.add_marker("json_state_tests")
47-
eels_fork = FORKS[test_case.fork_name]["eels_fork"]
48-
test_patterns = exceptional_state_test_patterns(
49-
test_case.fork_name, eels_fork
50-
)
51-
if any(x.search(test_case.key) for x in test_patterns.slow):
48+
eels_fork = FORKS[fork_name]["eels_fork"]
49+
test_patterns = exceptional_state_test_patterns(fork_name, eels_fork)
50+
if any(x.search(key) for x in test_patterns.slow):
5251
self.add_marker("slow")
5352

53+
@property
54+
def state_test_fixture(self) -> "StateTestFixture":
55+
"""Return the state test fixture this test belongs to."""
56+
parent = self.parent
57+
assert parent is not None
58+
assert isinstance(parent, StateTestFixture)
59+
return parent
60+
61+
@property
62+
def test_key(self) -> str:
63+
"""Return the key of the state test fixture in the fixture file."""
64+
return self.state_test_fixture.test_key
65+
66+
@property
67+
def fixtures_file(self) -> FixturesFile:
68+
"""Fixtures file from which the test fixture was collected."""
69+
return self.state_test_fixture.fixtures_file
70+
71+
@property
72+
def test_dict(self) -> Dict[str, Any]:
73+
"""Load test from disk."""
74+
loaded_file = self.fixtures_file.data
75+
return loaded_file[self.test_key]
76+
5477
def runtest(self) -> None:
5578
"""
5679
Runs a single general state test.
5780
"""
58-
index = self.test_case.index
59-
json_fork = self.test_case.fork_name
81+
json_fork = self.fork_name
6082
test_dict = self.test_dict
6183

6284
env = test_dict["env"]
@@ -68,7 +90,7 @@ def runtest(self) -> None:
6890

6991
alloc = test_dict["pre"]
7092

71-
post = test_dict["post"][json_fork][index]
93+
post = test_dict["post"][self.fork_name][self.index]
7294
post_hash = post["hash"]
7395
d = post["indexes"]["data"]
7496
g = post["indexes"]["gas"]
@@ -144,6 +166,20 @@ def is_format(cls, test_dict: Dict[str, Any]) -> bool:
144166
return False
145167
return True
146168

169+
@property
170+
def fixtures_file(self) -> FixturesFile:
171+
"""Fixtures file from which the test fixture was collected."""
172+
parent = self.parent
173+
assert parent is not None
174+
assert isinstance(parent, FixturesFile)
175+
return parent
176+
177+
@property
178+
def test_dict(self) -> Dict[str, Any]:
179+
"""Load test from disk."""
180+
loaded_file = self.fixtures_file.data
181+
return loaded_file[self.test_key]
182+
147183
def collect(self) -> Iterable[Item | Collector]:
148184
"""Collect state test cases inside of this fixture."""
149185
for test_case in read_test_case(
@@ -157,6 +193,7 @@ def collect(self) -> Iterable[Item | Collector]:
157193
yield StateTest.from_parent(
158194
parent=self,
159195
name=name,
160-
test_case=test_case,
161-
test_dict=self.test_dict,
196+
index=test_case.index,
197+
fork_name=test_case.fork_name,
198+
key=self.test_key,
162199
)

0 commit comments

Comments
 (0)