Skip to content

Commit f287901

Browse files
fix(unit_tests): fixed blob unit tests when run with xdist (#1946)
* fixed blob unit tests for running with xdist * codespell is a ********** * Update src/ethereum_test_types/tests/test_blob_types.py --------- Co-authored-by: Mario Vega <[email protected]>
1 parent 50fdf39 commit f287901

File tree

2 files changed

+97
-17
lines changed

2 files changed

+97
-17
lines changed

src/ethereum_test_types/blob_types.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,19 @@ def clear_blob_cache(cached_blobs_folder_path: Path):
2626
"""Delete all cached blobs."""
2727
if not cached_blobs_folder_path.is_dir():
2828
return
29-
for f in cached_blobs_folder_path.glob("*.json"): # only delete .json files
29+
30+
json_files = list(cached_blobs_folder_path.glob("*.json"))
31+
32+
for f in json_files:
33+
lock_file_path = f.with_suffix(".lock")
34+
3035
try:
31-
f.unlink() # permanently delete this file
36+
# get file lock for what you want to delete
37+
with FileLock(lock_file_path):
38+
f.unlink()
3239
except Exception as e:
3340
print(
34-
f"Critical error while trying to delete file {f}:{e}.. "
41+
f"Error while trying to delete file {f}:{e}. "
3542
"Aborting clearing of blob cache folder."
3643
)
3744
return
@@ -186,11 +193,15 @@ def get_cells(fork: Fork, data: Bytes) -> List[Bytes] | None:
186193
# (blob related constants are needed and only available for normal forks)
187194
fork = fork.fork_at(timestamp=timestamp)
188195

189-
# if this blob already exists then load from file
196+
# if this blob already exists then load from file. use lock
190197
blob_location: Path = Blob.get_filepath(fork, seed)
191-
if blob_location.exists():
192-
logger.debug(f"Blob exists already, reading it from file {blob_location}")
193-
return Blob.from_file(Blob.get_filename(fork, seed))
198+
199+
# use lock to avoid race conditions
200+
lock_file_path = blob_location.with_suffix(".lock")
201+
with FileLock(lock_file_path):
202+
if blob_location.exists():
203+
logger.debug(f"Blob exists already, reading it from file {blob_location}")
204+
return Blob.from_file(Blob.get_filename(fork, seed))
194205

195206
assert fork.supports_blobs(), f"Provided fork {fork.name()} does not support blobs!"
196207

@@ -237,17 +248,14 @@ def from_file(file_name: str) -> "Blob":
237248
# determine path where this blob would be stored if it existed
238249
blob_file_location = CACHED_BLOBS_DIRECTORY / file_name
239250

240-
# use lock to avoid race conditions
241-
lock_file_path = blob_file_location.with_suffix(".lock")
242-
with FileLock(lock_file_path):
243-
# check whether blob exists
244-
assert blob_file_location.exists(), (
245-
f"Tried to load blob from file but {blob_file_location} does not exist"
246-
)
251+
# check whether blob exists
252+
assert blob_file_location.exists(), (
253+
f"Tried to load blob from file but {blob_file_location} does not exist"
254+
)
247255

248-
# read blob from file
249-
with open(blob_file_location, "r", encoding="utf-8") as f:
250-
json_str: str = f.read()
256+
# read blob from file
257+
with open(blob_file_location, "r", encoding="utf-8") as f:
258+
json_str: str = f.read()
251259

252260
# reconstruct and return blob object
253261
return Blob.model_validate_json(json_str)

src/ethereum_test_types/tests/test_blob_types.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Test suite for blobs."""
22

33
import copy
4+
import time
45

56
import pytest
7+
from filelock import FileLock
68

79
from ethereum_test_forks import (
810
Cancun,
@@ -18,6 +20,65 @@
1820
from ..blob_types import CACHED_BLOBS_DIRECTORY, Blob, clear_blob_cache
1921

2022

23+
def increment_counter(timeout: float = 10):
24+
"""
25+
Increment counter in file, creating if doesn't exist.
26+
27+
This is needed because we require the unit test 'test_transition_fork_blobs' to run
28+
at the end without having to include another dependency for ordering tests.
29+
That test has to run at the end because it assumes that no json blobs not created
30+
by itself are created while it is running.
31+
32+
The hardcoded counter value in the test above has to be updated if any new blob_related
33+
unit tests that create json blobs are added in the future.
34+
35+
"""
36+
file_path = CACHED_BLOBS_DIRECTORY / "blob_unit_test_counter.txt"
37+
lock_file = file_path.with_suffix(".lock")
38+
39+
with FileLock(lock_file, timeout=timeout):
40+
# Read current value or start at 0
41+
if file_path.exists():
42+
current_value = int(file_path.read_text().strip())
43+
else:
44+
current_value = 0
45+
46+
# Increment and write back
47+
new_value = current_value + 1
48+
file_path.write_text(str(new_value))
49+
50+
return new_value
51+
52+
53+
def wait_until_counter_reached(target: int, poll_interval: float = 0.1):
54+
"""Wait until blob unit test counter reaches target value."""
55+
file_path = CACHED_BLOBS_DIRECTORY / "blob_unit_test_counter.txt"
56+
lock_file = file_path.with_suffix(".lock") # Add lock file path
57+
58+
while True:
59+
# Use FileLock when reading!
60+
with FileLock(lock_file, timeout=10):
61+
if file_path.exists():
62+
try:
63+
current_value = int(file_path.read_text().strip())
64+
if current_value == target:
65+
# file_path.unlink() # get rid to effectively reset counter to 0
66+
return current_value
67+
elif current_value > target:
68+
pytest.fail(
69+
f"The blob_unit_test lock counter is too high! "
70+
f"Expected {target}, but got {current_value}. "
71+
f"It probably reused an existing file that was not cleared. "
72+
f"Delete {file_path} manually to fix this."
73+
)
74+
except Exception:
75+
current_value = 0
76+
else:
77+
current_value = 0
78+
79+
time.sleep(poll_interval)
80+
81+
2182
@pytest.mark.parametrize("seed", [0, 10, 100])
2283
@pytest.mark.parametrize("fork", [Cancun, Prague, Osaka])
2384
def test_blob_creation_and_writing_and_reading(
@@ -42,6 +103,8 @@ def test_blob_creation_and_writing_and_reading(
42103
# ensure file you read equals file you wrote
43104
assert b.model_dump() == restored.model_dump()
44105

106+
increment_counter()
107+
45108

46109
@pytest.mark.parametrize(
47110
"corruption_mode",
@@ -71,6 +134,8 @@ def test_blob_proof_corruption(
71134
"proof is unchanged!"
72135
)
73136

137+
increment_counter()
138+
74139

75140
@pytest.mark.parametrize("timestamp", [14999, 15000])
76141
@pytest.mark.parametrize(
@@ -81,6 +146,9 @@ def test_transition_fork_blobs(
81146
timestamp,
82147
):
83148
"""Generates blobs for transition forks (time 14999 is old fork, time 15000 is new fork)."""
149+
# line below guarantees that this test runs only after the other blob unit tests are done
150+
wait_until_counter_reached(21)
151+
84152
clear_blob_cache(CACHED_BLOBS_DIRECTORY)
85153

86154
print(f"Original fork: {fork}, Timestamp: {timestamp}")
@@ -109,3 +177,7 @@ def test_transition_fork_blobs(
109177
f"Transition fork failure! Fork {fork.name()} at timestamp: {timestamp} should have "
110178
f"transitioned to {post_transition_fork_at_15k.name()} but is still at {b.fork.name()}"
111179
)
180+
181+
# delete counter at last iteration (otherwise re-running all unit tests will fail)
182+
if timestamp == 15_000 and pre_transition_fork == Prague:
183+
(CACHED_BLOBS_DIRECTORY / "blob_unit_test_counter.txt").unlink()

0 commit comments

Comments
 (0)