Skip to content

Commit f6bd188

Browse files
refactor(fill): encapsulate fixture output options in FixtureOutput class (#1471)
* refactor(fill): encapsulate fixture output ops in `FixtureOutput` class * docs: update changelog * refactor(fill): convert `FixtureOutput` to pydantic model * refactor(fill): move `FixtureOutput` class to its own moudule * test(fill): fix unit test * docs: Move changelog * fix: changelog --------- Co-authored-by: Mario Vega <marioevz@gmail.com>
1 parent ea94dce commit f6bd188

File tree

5 files changed

+153
-70
lines changed

5 files changed

+153
-70
lines changed

docs/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ Test fixtures for use by clients are available for each release on the [Github r
88

99
### 💥 Breaking Change
1010

11+
### 🛠️ Framework
12+
13+
#### `fill`
14+
15+
- 🔀 Refactor: Encapsulate `fill`'s fixture output options (`--output`, `--flat-output`, `--single-fixture-per-file`) into a `FixtureOutput` class ([#1471](https://github.com/ethereum/execution-spec-tests/pull/1471)).
16+
17+
#### `consume`
18+
19+
### 📋 Misc
20+
21+
### 🧪 Test Cases
22+
23+
## [v4.5.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v4.5.0) - 2025-05-14
24+
25+
### 💥 Breaking Change
26+
1127
#### EOF removed from Osaka
1228

1329
Following ["Interop Testing Call 34"](https://github.com/ethereum/pm/issues/1499) and the procedural EIPs [PR](https://github.com/ethereum/EIPs/pull/9703) the decision to remove EOF from Osaka was made.

src/pytest_plugins/filler/__init__.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
"""
2-
A pytest plugin that provides fixtures that fill tests and generate
3-
fixtures.
4-
"""
1+
"""A pytest plugin to fill tests and generate JSON fixtures."""
2+
3+
from .fixture_output import FixtureOutput
4+
5+
__all__ = [
6+
"FixtureOutput",
7+
]

src/pytest_plugins/filler/filler.py

Lines changed: 35 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import configparser
1010
import datetime
1111
import os
12-
import tarfile
1312
import warnings
1413
from pathlib import Path
1514
from typing import Any, Dict, Generator, List, Type
@@ -35,6 +34,7 @@
3534

3635
from ..shared.helpers import get_spec_format_for_item, labeled_format_parameter_set
3736
from ..spec_version_checker.spec_version_checker import get_ref_spec_from_module
37+
from .fixture_output import FixtureOutput
3838

3939

4040
def default_output_directory() -> str:
@@ -53,18 +53,6 @@ def default_html_report_file_path() -> str:
5353
return ".meta/report_fill.html"
5454

5555

56-
def strip_output_tarball_suffix(output: Path) -> Path:
57-
"""Strip the '.tar.gz' suffix from the output path."""
58-
if str(output).endswith(".tar.gz"):
59-
return output.with_suffix("").with_suffix("")
60-
return output
61-
62-
63-
def is_output_stdout(output: Path) -> bool:
64-
"""Return True if the fixture output is configured to be stdout."""
65-
return strip_output_tarball_suffix(output).name == "stdout"
66-
67-
6856
def pytest_addoption(parser: pytest.Parser):
6957
"""Add command-line options to pytest."""
7058
evm_group = parser.getgroup("evm", "Arguments defining evm executable behavior")
@@ -229,12 +217,14 @@ def pytest_configure(config):
229217
EnvironmentDefaults.gas_limit = config.getoption("block_gas_limit")
230218
if config.option.collectonly:
231219
return
220+
221+
# Initialize fixture output configuration
222+
config.fixture_output = FixtureOutput.from_config(config)
223+
232224
if not config.getoption("disable_html") and config.getoption("htmlpath") is None:
233225
# generate an html report by default, unless explicitly disabled
234-
config.option.htmlpath = (
235-
strip_output_tarball_suffix(config.getoption("output"))
236-
/ default_html_report_file_path()
237-
)
226+
config.option.htmlpath = config.fixture_output.directory / default_html_report_file_path()
227+
238228
# Instantiate the transition tool here to check that the binary path/trace option is valid.
239229
# This ensures we only raise an error once, if appropriate, instead of for every test.
240230
t8n = TransitionTool.from_binary_path(
@@ -288,7 +278,7 @@ def pytest_report_teststatus(report, config: pytest.Config):
288278
...x...
289279
```
290280
"""
291-
if is_output_stdout(config.getoption("output")):
281+
if config.fixture_output.is_stdout: # type: ignore[attr-defined]
292282
return report.outcome, "", report.outcome.upper()
293283

294284

@@ -303,12 +293,12 @@ def pytest_terminal_summary(
303293
actually run the tests.
304294
"""
305295
yield
306-
if is_output_stdout(config.getoption("output")):
296+
if config.fixture_output.is_stdout: # type: ignore[attr-defined]
307297
return
308298
stats = terminalreporter.stats
309299
if "passed" in stats and stats["passed"]:
310300
# append / to indicate this is a directory
311-
output_dir = str(strip_output_tarball_suffix(config.getoption("output"))) + "/"
301+
output_dir = str(config.fixture_output.directory) + "/" # type: ignore[attr-defined]
312302
terminalreporter.write_sep(
313303
"=",
314304
(
@@ -505,45 +495,33 @@ def base_dump_dir(request: pytest.FixtureRequest) -> Path | None:
505495

506496

507497
@pytest.fixture(scope="session")
508-
def is_output_tarball(request: pytest.FixtureRequest) -> bool:
509-
"""Return True if the output directory is a tarball."""
510-
output: Path = request.config.getoption("output")
511-
if output.suffix == ".gz" and output.with_suffix("").suffix == ".tar":
512-
return True
513-
return False
498+
def fixture_output(request: pytest.FixtureRequest) -> FixtureOutput:
499+
"""Return the fixture output configuration."""
500+
return request.config.fixture_output # type: ignore[attr-defined]
514501

515502

516503
@pytest.fixture(scope="session")
517-
def output_dir(request: pytest.FixtureRequest, is_output_tarball: bool) -> Path:
518-
"""Return directory to store the generated test fixtures."""
519-
output = request.config.getoption("output")
520-
if is_output_tarball:
521-
return strip_output_tarball_suffix(output)
522-
return output
504+
def is_output_tarball(fixture_output: FixtureOutput) -> bool:
505+
"""Return True if the output directory is a tarball."""
506+
return fixture_output.is_tarball
523507

524508

525509
@pytest.fixture(scope="session")
526-
def output_metadata_dir(output_dir: Path) -> Path:
527-
"""Return metadata directory to store fixture meta files."""
528-
if is_output_stdout(output_dir):
529-
return output_dir
530-
return output_dir / ".meta"
510+
def output_dir(fixture_output: FixtureOutput) -> Path:
511+
"""Return directory to store the generated test fixtures."""
512+
return fixture_output.directory
531513

532514

533515
@pytest.fixture(scope="session", autouse=True)
534-
def create_properties_file(
535-
request: pytest.FixtureRequest, output_dir: Path, output_metadata_dir: Path
536-
) -> None:
516+
def create_properties_file(request: pytest.FixtureRequest, fixture_output: FixtureOutput) -> None:
537517
"""
538518
Create ini file with fixture build properties in the fixture output
539519
directory.
540520
"""
541-
if is_output_stdout(request.config.getoption("output")):
521+
if fixture_output.is_stdout:
542522
return
543-
if not output_dir.exists():
544-
output_dir.mkdir(parents=True)
545-
if not output_metadata_dir.exists():
546-
output_metadata_dir.mkdir(parents=True)
523+
524+
fixture_output.create_directories()
547525

548526
fixture_properties = {
549527
"timestamp": datetime.datetime.now().isoformat(),
@@ -574,7 +552,7 @@ def create_properties_file(
574552
)
575553
config["environment"] = environment_properties
576554

577-
ini_filename = output_metadata_dir / "fixtures.ini"
555+
ini_filename = fixture_output.metadata_dir / "fixtures.ini"
578556
with open(ini_filename, "w") as f:
579557
f.write("; This file describes fixture build properties\n\n")
580558
config.write(f)
@@ -610,9 +588,9 @@ def get_fixture_collection_scope(fixture_name, config):
610588
611589
See: https://docs.pytest.org/en/stable/how-to/fixtures.html#dynamic-scope
612590
"""
613-
if is_output_stdout(config.getoption("output")):
591+
if config.fixture_output.is_stdout:
614592
return "session"
615-
if config.getoption("single_fixture_per_file"):
593+
if config.fixture_output.single_fixture_per_file:
616594
return "function"
617595
return "module"
618596

@@ -636,17 +614,17 @@ def fixture_collector(
636614
evm_fixture_verification: FixtureConsumer,
637615
filler_path: Path,
638616
base_dump_dir: Path | None,
639-
output_dir: Path,
617+
fixture_output: FixtureOutput,
640618
) -> Generator[FixtureCollector, None, None]:
641619
"""
642620
Return configured fixture collector instance used for all tests
643621
in one test module.
644622
"""
645623
fixture_collector = FixtureCollector(
646-
output_dir=output_dir,
647-
flat_output=request.config.getoption("flat_output"),
624+
output_dir=fixture_output.directory,
625+
flat_output=fixture_output.flat_output,
648626
fill_static_tests=request.config.getoption("fill_static_tests_enabled"),
649-
single_fixture_per_file=request.config.getoption("single_fixture_per_file"),
627+
single_fixture_per_file=fixture_output.single_fixture_per_file,
650628
filler_path=filler_path,
651629
base_dump_dir=base_dump_dir,
652630
)
@@ -875,29 +853,20 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: int):
875853
if xdist.is_xdist_worker(session):
876854
return
877855

878-
output: Path = session.config.getoption("output")
856+
fixture_output = session.config.fixture_output # type: ignore[attr-defined]
879857
# When using --collect-only it should not matter whether fixtures folder exists or not
880-
if is_output_stdout(output) or session.config.option.collectonly:
858+
if fixture_output.is_stdout or session.config.option.collectonly:
881859
return
882860

883-
output_dir = strip_output_tarball_suffix(output)
884861
# Remove any lock files that may have been created.
885-
for file in output_dir.rglob("*.lock"):
862+
for file in fixture_output.directory.rglob("*.lock"):
886863
file.unlink()
887864

888865
# Generate index file for all produced fixtures.
889866
if session.config.getoption("generate_index"):
890867
generate_fixtures_index(
891-
output_dir, quiet_mode=True, force_flag=False, disable_infer_format=False
868+
fixture_output.directory, quiet_mode=True, force_flag=False, disable_infer_format=False
892869
)
893870

894871
# Create tarball of the output directory if the output is a tarball.
895-
is_output_tarball = output.suffix == ".gz" and output.with_suffix("").suffix == ".tar"
896-
if is_output_tarball:
897-
source_dir = output_dir
898-
tarball_filename = output
899-
with tarfile.open(tarball_filename, "w:gz") as tar:
900-
for file in source_dir.rglob("*"):
901-
if file.suffix in {".json", ".ini"}:
902-
arcname = Path("fixtures") / file.relative_to(source_dir)
903-
tar.add(file, arcname=arcname)
872+
fixture_output.create_tarball()
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Fixture output configuration for generated test fixtures."""
2+
3+
import tarfile
4+
from pathlib import Path
5+
6+
import pytest
7+
from pydantic import BaseModel, Field
8+
9+
10+
class FixtureOutput(BaseModel):
11+
"""Represents the output destination for generated test fixtures."""
12+
13+
output_path: Path = Field(description="Directory path to store the generated test fixtures")
14+
flat_output: bool = Field(
15+
default=False,
16+
description="Output each test case in the directory without the folder structure",
17+
)
18+
single_fixture_per_file: bool = Field(
19+
default=False,
20+
description=(
21+
"Don't group fixtures in JSON files by test function; "
22+
"write each fixture to its own file"
23+
),
24+
)
25+
26+
@property
27+
def directory(self) -> Path:
28+
"""Return the actual directory path where fixtures will be written."""
29+
return self.strip_tarball_suffix(self.output_path)
30+
31+
@property
32+
def metadata_dir(self) -> Path:
33+
"""Return metadata directory to store fixture meta files."""
34+
if self.is_stdout:
35+
return self.directory
36+
return self.directory / ".meta"
37+
38+
@property
39+
def is_tarball(self) -> bool:
40+
"""Return True if the output should be packaged as a tarball."""
41+
path = self.output_path
42+
return path.suffix == ".gz" and path.with_suffix("").suffix == ".tar"
43+
44+
@property
45+
def is_stdout(self) -> bool:
46+
"""Return True if the fixture output is configured to be stdout."""
47+
return self.directory.name == "stdout"
48+
49+
@staticmethod
50+
def strip_tarball_suffix(path: Path) -> Path:
51+
"""Strip the '.tar.gz' suffix from the output path."""
52+
if str(path).endswith(".tar.gz"):
53+
return path.with_suffix("").with_suffix("")
54+
return path
55+
56+
def create_directories(self) -> None:
57+
"""Create output and metadata directories if needed."""
58+
if self.is_stdout:
59+
return
60+
61+
self.directory.mkdir(parents=True, exist_ok=True)
62+
self.metadata_dir.mkdir(parents=True, exist_ok=True)
63+
64+
def create_tarball(self) -> None:
65+
"""Create tarball of the output directory if configured to do so."""
66+
if not self.is_tarball:
67+
return
68+
69+
with tarfile.open(self.output_path, "w:gz") as tar:
70+
for file in self.directory.rglob("*"):
71+
if file.suffix in {".json", ".ini"}:
72+
arcname = Path("fixtures") / file.relative_to(self.directory)
73+
tar.add(file, arcname=arcname)
74+
75+
@classmethod
76+
def from_options(
77+
cls, output_path: Path, flat_output: bool, single_fixture_per_file: bool
78+
) -> "FixtureOutput":
79+
"""Create a FixtureOutput instance from pytest options."""
80+
return cls(
81+
output_path=output_path,
82+
flat_output=flat_output,
83+
single_fixture_per_file=single_fixture_per_file,
84+
)
85+
86+
@classmethod
87+
def from_config(cls, config: pytest.Config) -> "FixtureOutput":
88+
"""Create a FixtureOutput instance from pytest configuration."""
89+
return cls(
90+
output_path=config.getoption("output"),
91+
flat_output=config.getoption("flat_output"),
92+
single_fixture_per_file=config.getoption("single_fixture_per_file"),
93+
)

src/pytest_plugins/filler/tests/test_filler.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ def test_fixture_output_based_on_command_line_args(
569569
expected_ini_file = "fixtures.ini"
570570
expected_index_file = "index.json"
571571
expected_resolver_file = None
572+
resolver_file = None
572573
if TransitionTool.default_tool == ExecutionSpecsTransitionTool:
573574
expected_resolver_file = "eels_resolutions.json"
574575

@@ -698,6 +699,7 @@ def test_fill_variables(
698699
expected_ini_file = "fixtures.ini"
699700
expected_index_file = "index.json"
700701
expected_resolver_file = None
702+
resolver_file = None
701703
if TransitionTool.default_tool == ExecutionSpecsTransitionTool:
702704
expected_resolver_file = "eels_resolutions.json"
703705

0 commit comments

Comments
 (0)