Skip to content

Commit c69fff6

Browse files
committed
feat(test-commands): Allow pytest valid fork markers as params; add unit tests
1 parent 6110cff commit c69fff6

4 files changed

Lines changed: 311 additions & 5 deletions

File tree

packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,3 +1208,54 @@ def parametrize_fork(
12081208
metafunc.parametrize(
12091209
param_names, param_values, scope="function", indirect=indirect
12101210
)
1211+
1212+
1213+
def pytest_collection_modifyitems(
1214+
config: pytest.Config, items: List[pytest.Item]
1215+
) -> None:
1216+
"""
1217+
Filter tests based on param-level validity markers.
1218+
1219+
The pytest_generate_tests hook only considers function-level validity markers.
1220+
This hook runs after parametrization and can access all markers including
1221+
param-level ones, allowing us to properly filter tests based on param-level
1222+
valid_from/valid_until markers.
1223+
"""
1224+
items_to_remove = []
1225+
1226+
for i, item in enumerate(items):
1227+
# Get fork from params if available
1228+
params = None
1229+
if hasattr(item, "callspec"):
1230+
params = item.callspec.params
1231+
elif hasattr(item, "params"):
1232+
params = item.params
1233+
1234+
if not params or "fork" not in params or params["fork"] is None:
1235+
continue
1236+
1237+
fork: Fork = params["fork"]
1238+
1239+
# Get all markers including param-level ones
1240+
markers = item.iter_markers()
1241+
1242+
# Calculate valid fork set from all markers
1243+
# If this raises (e.g., duplicate markers from combining function-level
1244+
# and param-level), exit immediately with error
1245+
try:
1246+
valid_fork_set = ValidityMarker.get_test_fork_set_from_markers(
1247+
markers
1248+
)
1249+
except Exception as e:
1250+
pytest.exit(
1251+
f"Error in test '{item.name}': {e}",
1252+
returncode=pytest.ExitCode.USAGE_ERROR,
1253+
)
1254+
1255+
# If the fork is not in the valid set, mark for removal
1256+
if fork not in valid_fork_set:
1257+
items_to_remove.append(i)
1258+
1259+
# Remove items in reverse order to maintain indices
1260+
for i in reversed(items_to_remove):
1261+
del items[i]

packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_bad_validity_markers.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,73 @@ def test_invalid_validity_markers(
236236
errors=1,
237237
)
238238
assert error_string in "\n".join(result.stdout.lines)
239+
240+
241+
# --- Tests for param-level marker errors --- #
242+
243+
244+
param_level_marker_error_test_cases = (
245+
(
246+
"param_level_valid_from_with_function_level_valid_from",
247+
(
248+
"""
249+
import pytest
250+
@pytest.mark.parametrize(
251+
"value",
252+
[
253+
pytest.param(True, marks=pytest.mark.valid_from("Paris")),
254+
],
255+
)
256+
@pytest.mark.valid_from("Berlin")
257+
def test_case(state_test, value):
258+
assert 1
259+
""",
260+
"Too many 'valid_from' markers applied to test",
261+
),
262+
),
263+
(
264+
"param_level_valid_until_with_function_level_valid_until",
265+
(
266+
"""
267+
import pytest
268+
@pytest.mark.parametrize(
269+
"value",
270+
[
271+
pytest.param(True, marks=pytest.mark.valid_until("Cancun")),
272+
],
273+
)
274+
@pytest.mark.valid_until("Prague")
275+
def test_case(state_test, value):
276+
assert 1
277+
""",
278+
"Too many 'valid_until' markers applied to test",
279+
),
280+
),
281+
)
282+
283+
284+
@pytest.mark.parametrize(
285+
"test_function, error_string",
286+
[test_case for _, test_case in param_level_marker_error_test_cases],
287+
ids=[test_id for test_id, _ in param_level_marker_error_test_cases],
288+
)
289+
def test_param_level_marker_errors(
290+
pytester: pytest.Pytester, error_string: str, test_function: str
291+
) -> None:
292+
"""
293+
Test that combining function-level and param-level validity markers
294+
of the same type produces an error.
295+
296+
Unlike function-level errors (caught during test generation), param-level
297+
errors are caught during collection and cause pytest to exit immediately.
298+
"""
299+
pytester.makepyfile(test_function)
300+
pytester.copy_example(
301+
name="src/execution_testing/cli/pytest_commands/pytest_ini_files/pytest-fill.ini"
302+
)
303+
result = pytester.runpytest("-c", "pytest-fill.ini")
304+
305+
# pytest.exit() causes the run to terminate with no test outcomes
306+
assert result.ret != 0, "Expected non-zero exit code"
307+
stdout = "\n".join(result.stdout.lines)
308+
assert error_string in stdout, f"Expected '{error_string}' in output"

packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_markers.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,182 @@ def test_fork_markers(
222222
*pytest_args,
223223
)
224224
result.assert_outcomes(**outcomes)
225+
226+
227+
# --- Tests for param-level validity markers --- #
228+
229+
230+
def generate_param_level_marker_test() -> str:
231+
"""Generate a test function with param-level fork validity markers."""
232+
return """
233+
import pytest
234+
235+
@pytest.mark.parametrize(
236+
"value",
237+
[
238+
pytest.param(
239+
True,
240+
id="from_tangerine",
241+
marks=pytest.mark.valid_from("TangerineWhistle"),
242+
),
243+
pytest.param(
244+
False,
245+
id="from_paris",
246+
marks=pytest.mark.valid_from("Paris"),
247+
),
248+
],
249+
)
250+
@pytest.mark.state_test_only
251+
def test_param_level_valid_from(state_test, value):
252+
pass
253+
"""
254+
255+
256+
def generate_param_level_valid_until_test() -> str:
257+
"""Generate a test function with param-level valid_until markers."""
258+
return """
259+
import pytest
260+
261+
@pytest.mark.parametrize(
262+
"value",
263+
[
264+
pytest.param(
265+
True,
266+
id="until_cancun",
267+
marks=pytest.mark.valid_until("Cancun"),
268+
),
269+
pytest.param(
270+
False,
271+
id="until_paris",
272+
marks=pytest.mark.valid_until("Paris"),
273+
),
274+
],
275+
)
276+
@pytest.mark.state_test_only
277+
def test_param_level_valid_until(state_test, value):
278+
pass
279+
"""
280+
281+
282+
def generate_param_level_mixed_test() -> str:
283+
"""Generate a test with both function-level and param-level markers."""
284+
return """
285+
import pytest
286+
287+
@pytest.mark.parametrize(
288+
"value",
289+
[
290+
pytest.param(
291+
True,
292+
id="all_forks",
293+
marks=pytest.mark.valid_from("TangerineWhistle"),
294+
),
295+
pytest.param(
296+
False,
297+
id="paris_only",
298+
marks=pytest.mark.valid_from("Paris"),
299+
),
300+
],
301+
)
302+
@pytest.mark.valid_until("Cancun")
303+
@pytest.mark.state_test_only
304+
def test_mixed_function_and_param_markers(state_test, value):
305+
pass
306+
"""
307+
308+
309+
@pytest.mark.parametrize(
310+
"test_function,pytest_args,outcomes",
311+
[
312+
pytest.param(
313+
generate_param_level_marker_test(),
314+
["--from=Paris", "--until=Cancun"],
315+
# from_tangerine: Paris, Shanghai, Cancun = 3 forks
316+
# from_paris: Paris, Shanghai, Cancun = 3 forks
317+
# Total: 6 tests
318+
{"passed": 6, "failed": 0, "skipped": 0, "errors": 0},
319+
id="param_level_valid_from_paris_to_cancun",
320+
),
321+
pytest.param(
322+
generate_param_level_marker_test(),
323+
["--from=Berlin", "--until=Shanghai"],
324+
# from_tangerine: Berlin, London, Paris, Shanghai = 4 forks
325+
# from_paris: Paris, Shanghai = 2 forks
326+
# Total: 6 tests
327+
{"passed": 6, "failed": 0, "skipped": 0, "errors": 0},
328+
id="param_level_valid_from_berlin_to_shanghai",
329+
),
330+
pytest.param(
331+
generate_param_level_marker_test(),
332+
["--from=Berlin", "--until=London"],
333+
# from_tangerine: Berlin, London = 2 forks
334+
# from_paris: none (Paris > London)
335+
# Total: 2 tests
336+
{"passed": 2, "failed": 0, "skipped": 0, "errors": 0},
337+
id="param_level_valid_from_berlin_to_london",
338+
),
339+
pytest.param(
340+
generate_param_level_valid_until_test(),
341+
["--from=Paris", "--until=Prague"],
342+
# until_cancun: Paris, Shanghai, Cancun = 3 forks
343+
# until_paris: Paris = 1 fork
344+
# Total: 4 tests
345+
{"passed": 4, "failed": 0, "skipped": 0, "errors": 0},
346+
id="param_level_valid_until_paris_to_prague",
347+
),
348+
pytest.param(
349+
generate_param_level_valid_until_test(),
350+
["--from=Shanghai", "--until=Prague"],
351+
# until_cancun: Shanghai, Cancun = 2 forks
352+
# until_paris: none (Shanghai > Paris)
353+
# Total: 2 tests
354+
{"passed": 2, "failed": 0, "skipped": 0, "errors": 0},
355+
id="param_level_valid_until_shanghai_to_prague",
356+
),
357+
pytest.param(
358+
generate_param_level_mixed_test(),
359+
["--from=Berlin", "--until=Prague"],
360+
# Function marker: valid_until("Cancun") limits to <= Cancun
361+
# all_forks (TangerineWhistle): Berlin, London, Paris, Shanghai, Cancun = 5
362+
# paris_only: Paris, Shanghai, Cancun = 3
363+
# Total: 8 tests
364+
{"passed": 8, "failed": 0, "skipped": 0, "errors": 0},
365+
id="mixed_markers_berlin_to_prague",
366+
),
367+
pytest.param(
368+
generate_param_level_mixed_test(),
369+
["--from=Paris", "--until=Shanghai"],
370+
# Function marker: valid_until("Cancun") limits to <= Cancun
371+
# Command line: --until=Shanghai further limits to <= Shanghai
372+
# all_forks: Paris, Shanghai = 2 forks
373+
# paris_only: Paris, Shanghai = 2 forks
374+
# Total: 4 tests
375+
{"passed": 4, "failed": 0, "skipped": 0, "errors": 0},
376+
id="mixed_markers_paris_to_shanghai",
377+
),
378+
],
379+
)
380+
def test_param_level_validity_markers(
381+
pytester: pytest.Pytester,
382+
test_function: str,
383+
outcomes: dict,
384+
pytest_args: List[str],
385+
) -> None:
386+
"""
387+
Test param-level validity markers (valid_from, valid_until on pytest.param).
388+
389+
The pytest_collection_modifyitems hook filters tests based on param-level
390+
markers after parametrization, allowing different parameter values to have
391+
different fork validity ranges.
392+
"""
393+
pytester.makepyfile(test_function)
394+
pytester.copy_example(
395+
name="src/execution_testing/cli/pytest_commands/pytest_ini_files/pytest-fill.ini"
396+
)
397+
result = pytester.runpytest(
398+
"-c",
399+
"pytest-fill.ini",
400+
"-v",
401+
*pytest_args,
402+
)
403+
result.assert_outcomes(**outcomes)

tests/tangerine_whistle/eip150_operation_gas_costs/test_eip150_selfdestruct.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
from execution_testing import (
3030
Macros as Om,
3131
)
32-
from execution_testing.forks import Berlin, Cancun, SpuriousDragon
32+
from execution_testing.forks import (
33+
Berlin,
34+
Cancun,
35+
SpuriousDragon,
36+
)
3337
from execution_testing.forks.helpers import Fork
3438

3539
from .spec import ref_spec_150
@@ -306,7 +310,9 @@ def build_post_state(
306310
@pytest.mark.parametrize(
307311
"warm",
308312
[
309-
pytest.param(False, id="cold"),
313+
pytest.param(
314+
False, id="cold", marks=pytest.mark.valid_from("TangerineWhistle")
315+
),
310316
pytest.param(True, id="warm", marks=pytest.mark.valid_from("Berlin")),
311317
],
312318
)
@@ -323,7 +329,6 @@ def build_post_state(
323329
[0, 1],
324330
ids=["dead_beneficiary", "alive_beneficiary"],
325331
)
326-
@pytest.mark.valid_from("TangerineWhistle")
327332
def test_selfdestruct_to_account(
328333
pre: Alloc,
329334
blockchain_test: BlockchainTestFiller,
@@ -429,7 +434,9 @@ def test_selfdestruct_to_account(
429434
@pytest.mark.parametrize(
430435
"warm",
431436
[
432-
pytest.param(False, id="cold"),
437+
pytest.param(
438+
False, id="cold", marks=pytest.mark.valid_from("TangerineWhistle")
439+
),
433440
pytest.param(True, id="warm", marks=pytest.mark.valid_from("Berlin")),
434441
],
435442
)
@@ -446,7 +453,6 @@ def test_selfdestruct_to_account(
446453
[0, 1],
447454
ids=["dead_beneficiary", "alive_beneficiary"],
448455
)
449-
@pytest.mark.valid_from("TangerineWhistle")
450456
def test_selfdestruct_state_access_boundary(
451457
pre: Alloc,
452458
blockchain_test: BlockchainTestFiller,

0 commit comments

Comments
 (0)