Skip to content

Commit

Permalink
refactor(fixtures): Remove formats.py, embed properties as class va…
Browse files Browse the repository at this point in the history
…rs (#826)

* refactor(fixtures): Embed FixtureFormat methods in instance

* refactor(fw): remove `src/ethereum_test_fixtures/formats.py`

* refactor(fw): rename

* fix(fw): nit

* fix(fw): pydantic fixture format

* fix(fixtures): remove unused properties

* fix(fixtures): remove unused properties

* fix(fixtures): remove unused properties

* fix(specs): tox
  • Loading branch information
marioevz committed Sep 30, 2024
1 parent 8fe1c23 commit 6e4c3a1
Show file tree
Hide file tree
Showing 27 changed files with 274 additions and 292 deletions.
21 changes: 8 additions & 13 deletions src/cli/gen_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)

from ethereum_test_base_types import HexNumber
from ethereum_test_fixtures import FixtureFormats
from ethereum_test_fixtures import FIXTURE_FORMATS, BlockchainFixture, FixtureFormat
from ethereum_test_fixtures.consume import IndexFile, TestCaseIndexFile
from ethereum_test_fixtures.file import Fixtures

Expand Down Expand Up @@ -48,21 +48,16 @@ def count_json_files_exclude_index(start_path: Path) -> int:
return json_file_count


def infer_fixture_format_from_path(file: Path) -> FixtureFormats:
def infer_fixture_format_from_path(file: Path) -> FixtureFormat | None:
"""
Attempt to infer the fixture format from the file path.
"""
if "blockchain_tests_engine" in file.parts:
return FixtureFormats.BLOCKCHAIN_TEST_ENGINE
if "blockchain_tests" in file.parts:
return FixtureFormats.BLOCKCHAIN_TEST
for fixture_type in FIXTURE_FORMATS.values():
if fixture_type.output_base_dir_name() in file.parts:
return fixture_type
if "BlockchainTests" in file.parts: # ethereum/tests
return FixtureFormats.BLOCKCHAIN_TEST
if "state_tests" in file.parts:
return FixtureFormats.STATE_TEST
if "eof_tests" in file.parts:
return FixtureFormats.EOF_TEST
return FixtureFormats.UNSET_TEST_FORMAT
return BlockchainFixture
return None


@click.command(
Expand Down Expand Up @@ -200,7 +195,7 @@ def generate_fixtures_index(
json_path=relative_file_path,
fixture_hash=fixture.info.get("hash", None),
fork=fixture.get_fork(),
format=fixture.format,
format=fixture.__class__,
)
)

Expand Down
24 changes: 13 additions & 11 deletions src/ethereum_test_fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,35 @@
Ethereum test fixture format definitions.
"""

from typing import List, Type
from typing import Dict

from .base import BaseFixture
from .base import BaseFixture, FixtureFormat
from .blockchain import EngineFixture as BlockchainEngineFixture
from .blockchain import Fixture as BlockchainFixture
from .blockchain import FixtureCommon as BlockchainFixtureCommon
from .collector import FixtureCollector, TestInfo
from .eof import Fixture as EOFFixture
from .formats import FixtureFormats
from .state import Fixture as StateFixture
from .verify import FixtureVerifier

FIXTURE_TYPES: List[Type[BaseFixture]] = [
BlockchainFixture,
BlockchainEngineFixture,
EOFFixture,
StateFixture,
]
FIXTURE_FORMATS: Dict[str, FixtureFormat] = {
f.fixture_format_name: f # type: ignore
for f in [
BlockchainFixture,
BlockchainEngineFixture,
EOFFixture,
StateFixture,
]
}
__all__ = [
"FIXTURE_TYPES",
"FIXTURE_FORMATS",
"BaseFixture",
"BlockchainFixture",
"BlockchainFixtureCommon",
"BlockchainEngineFixture",
"EOFFixture",
"FixtureCollector",
"FixtureFormats",
"FixtureFormat",
"FixtureVerifier",
"StateFixture",
"TestInfo",
Expand Down
31 changes: 27 additions & 4 deletions src/ethereum_test_fixtures/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,30 @@
import hashlib
import json
from functools import cached_property
from typing import Any, ClassVar, Dict
from typing import Any, ClassVar, Dict, Type

from pydantic import Field

from ethereum_test_base_types import CamelModel, ReferenceSpec

from .formats import FixtureFormats
from ethereum_test_forks import Fork


class BaseFixture(CamelModel):
"""Represents a base Ethereum test fixture of any type."""

info: Dict[str, str] = Field(default_factory=dict, alias="_info")
format: ClassVar[FixtureFormats] = FixtureFormats.UNSET_TEST_FORMAT

# Fixture format properties
fixture_format_name: ClassVar[str] = "unset"
output_file_extension: ClassVar[str] = ".json"
description: ClassVar[str] = "Unknown fixture format; it has not been set."

@classmethod
def output_base_dir_name(cls) -> str:
"""
Returns the name of the subdirectory where this type of fixture should be dumped to.
"""
return cls.fixture_format_name.replace("test", "tests")

@cached_property
def json_dict(self) -> Dict[str, Any]:
Expand Down Expand Up @@ -69,3 +79,16 @@ def get_fork(self) -> str | None:
Returns the fork of the fixture as a string.
"""
raise NotImplementedError

@classmethod
def supports_fork(cls, fork: Fork) -> bool:
"""
Returns whether the fixture can be generated for the given fork.
By default, all fixtures support all forks.
"""
return True


# Type alias for a base fixture class
FixtureFormat = Type[BaseFixture]
22 changes: 17 additions & 5 deletions src/ethereum_test_fixtures/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
ZeroPaddedHexNumber,
)
from ethereum_test_exceptions import EngineAPIError, ExceptionInstanceOrList
from ethereum_test_forks import Fork
from ethereum_test_forks import Fork, Paris
from ethereum_test_types.types import (
AuthorizationTupleGeneric,
ConsolidationRequest,
Expand All @@ -41,7 +41,6 @@
)

from .base import BaseFixture
from .formats import FixtureFormats


class HeaderForkRequirement(str):
Expand Down Expand Up @@ -499,19 +498,32 @@ class Fixture(FixtureCommon):
Cross-client specific blockchain test model use in JSON fixtures.
"""

fixture_format_name: ClassVar[str] = "blockchain_test"
description: ClassVar[str] = "Tests that generate a blockchain test fixture."

genesis_rlp: Bytes = Field(..., alias="genesisRLP")
blocks: List[FixtureBlock | InvalidFixtureBlock]
seal_engine: Literal["NoProof"] = Field("NoProof")

format: ClassVar[FixtureFormats] = FixtureFormats.BLOCKCHAIN_TEST


class EngineFixture(FixtureCommon):
"""
Engine specific test fixture information.
"""

fixture_format_name: ClassVar[str] = "blockchain_test_engine"
description: ClassVar[
str
] = "Tests that generate a blockchain test fixture in Engine API format."

payloads: List[FixtureEngineNewPayload] = Field(..., alias="engineNewPayloads")
sync_payload: FixtureEngineNewPayload | None = None

format: ClassVar[FixtureFormats] = FixtureFormats.BLOCKCHAIN_TEST_ENGINE
@classmethod
def supports_fork(cls, fork: Fork) -> bool:
"""
Returns whether the fixture can be generated for the given fork.
The Engine API is available only on Paris and afterwards.
"""
return fork >= Paris
11 changes: 5 additions & 6 deletions src/ethereum_test_fixtures/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

from .base import BaseFixture
from .file import Fixtures
from .formats import FixtureFormats
from .verify import FixtureVerifier


Expand Down Expand Up @@ -139,8 +138,8 @@ def add_fixture(self, info: TestInfo, fixture: BaseFixture) -> Path:

fixture_path = (
self.output_dir
/ fixture.format.output_base_dir_name
/ fixture_basename.with_suffix(fixture.format.output_file_extension)
/ fixture.output_base_dir_name()
/ fixture_basename.with_suffix(fixture.output_file_extension)
)
if fixture_path not in self.all_fixtures.keys(): # relevant when we group by test function
self.all_fixtures[fixture_path] = Fixtures(root={})
Expand All @@ -163,7 +162,7 @@ def dump_fixtures(self) -> None:
os.makedirs(self.output_dir, exist_ok=True)
for fixture_path, fixtures in self.all_fixtures.items():
os.makedirs(fixture_path.parent, exist_ok=True)
if len({fixture.format for fixture in fixtures.values()}) != 1:
if len({fixture.__class__ for fixture in fixtures.values()}) != 1:
raise TypeError("All fixtures in a single file must have the same format.")
fixtures.collect_into_file(fixture_path)

Expand All @@ -173,11 +172,11 @@ def verify_fixture_files(self, evm_fixture_verification: FixtureVerifier) -> Non
"""
for fixture_path, name_fixture_dict in self.all_fixtures.items():
for fixture_name, fixture in name_fixture_dict.items():
if FixtureFormats.is_verifiable(fixture.format):
if evm_fixture_verification.is_verifiable(fixture.__class__):
info = self.json_path_to_test_item[fixture_path]
verify_fixtures_dump_dir = self._get_verify_fixtures_dump_dir(info)
evm_fixture_verification.verify_fixture(
fixture.format,
fixture.__class__,
fixture_path,
fixture_name=None,
debug_output_path=verify_fixtures_dump_dir,
Expand Down
17 changes: 11 additions & 6 deletions src/ethereum_test_fixtures/consume.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
import datetime
import json
from pathlib import Path
from typing import List, TextIO
from typing import Annotated, List, TextIO

from pydantic import BaseModel, RootModel
from pydantic import BaseModel, PlainSerializer, PlainValidator, RootModel

from ethereum_test_base_types import HexNumber
from ethereum_test_fixtures import FIXTURE_FORMATS, FixtureFormat

from .blockchain import EngineFixture as BlockchainEngineFixture
from .blockchain import Fixture as BlockchainFixture
from .file import Fixtures
from .formats import FixtureFormats
from .state import Fixture as StateFixture


Expand All @@ -25,7 +26,11 @@ class TestCaseBase(BaseModel):
id: str
fixture_hash: HexNumber | None
fork: str | None
format: FixtureFormats
format: Annotated[
FixtureFormat,
PlainSerializer(lambda f: f.fixture_format_name),
PlainValidator(lambda f: FIXTURE_FORMATS[f] if f in FIXTURE_FORMATS else f),
]
__test__ = False # stop pytest from collecting this class as a test


Expand Down Expand Up @@ -129,14 +134,14 @@ def from_stream(cls, fd: TextIO) -> "TestCases":
fixtures = Fixtures.from_json_data(json.load(fd))
test_cases = []
for fixture_name, fixture in fixtures.items():
if fixture.format == FixtureFormats.BLOCKCHAIN_TEST_ENGINE:
if fixture == BlockchainEngineFixture:
print("Skipping engine fixture", fixture_name)
test_cases.append(
TestCaseStream(
id=fixture_name,
fixture_hash=fixture.hash,
fork=fixture.get_fork(),
format=fixture.format,
format=fixture.__class__,
fixture=fixture,
)
)
Expand Down
6 changes: 3 additions & 3 deletions src/ethereum_test_fixtures/eof.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from ethereum_test_types.eof.v1 import ContainerKind

from .base import BaseFixture
from .formats import FixtureFormats


class Result(CamelModel):
Expand Down Expand Up @@ -50,9 +49,10 @@ class Fixture(BaseFixture):
Fixture for a single EOFTest.
"""

vectors: Mapping[Number, Vector]
fixture_format_name: ClassVar[str] = "eof_test"
description: ClassVar[str] = "Tests that generate an EOF test fixture."

format: ClassVar[FixtureFormats] = FixtureFormats.EOF_TEST
vectors: Mapping[Number, Vector]

def get_fork(self) -> str | None:
"""
Expand Down
26 changes: 9 additions & 17 deletions src/ethereum_test_fixtures/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@
"""
import json
from pathlib import Path
from typing import Any, Dict, Literal, Optional
from typing import Any, Dict, Optional

from pydantic import RootModel

from .base import FixtureFormat
from .blockchain import EngineFixture as BlockchainEngineFixture
from .blockchain import Fixture as BlockchainFixture
from .eof import Fixture as EOFFixture
from .formats import FixtureFormats
from .state import Fixture as StateFixture

FixtureFormatsValues = Literal[
"blockchain_test_engine", "blockchain_test", "state_test", "eof_test", "unset_test_format"
]

FixtureModel = BlockchainFixture | BlockchainEngineFixture | StateFixture | EOFFixture


Expand Down Expand Up @@ -75,7 +71,7 @@ def collect_into_file(self, file_path: Path):
def from_file(
cls,
file_path: Path,
fixture_format: Optional[FixtureFormats | FixtureFormatsValues] = None,
fixture_format: Optional[FixtureFormat] = None,
) -> "BaseFixturesRootModel":
"""
Dynamically create a fixture model from the specified json file and,
Expand All @@ -89,7 +85,7 @@ def from_file(
def from_json_data(
cls,
json_data: Dict[str, Any],
fixture_format: Optional[FixtureFormats | FixtureFormatsValues] = None,
fixture_format: Optional[FixtureFormat] = None,
) -> "BaseFixturesRootModel":
"""
Dynamically create a fixture model from the specified json data and,
Expand All @@ -101,17 +97,13 @@ def from_json_data(
fixture_format will provide a speed-up.
"""
model_mapping = {
FixtureFormats.BLOCKCHAIN_TEST: BlockchainFixtures,
FixtureFormats.BLOCKCHAIN_TEST_ENGINE: BlockchainEngineFixtures,
FixtureFormats.STATE_TEST: StateFixtures,
FixtureFormats.EOF_TEST: EOFFixtures,
FixtureFormats.BLOCKCHAIN_TEST.value: BlockchainFixtures,
FixtureFormats.BLOCKCHAIN_TEST_ENGINE.value: BlockchainEngineFixtures,
FixtureFormats.STATE_TEST.value: StateFixtures,
FixtureFormats.EOF_TEST.value: EOFFixtures,
BlockchainFixture: BlockchainFixtures,
BlockchainEngineFixture: BlockchainEngineFixtures,
StateFixture: StateFixtures,
EOFFixture: EOFFixtures,
}

if fixture_format not in [None, "unset_test_format", FixtureFormats.UNSET_TEST_FORMAT]:
if fixture_format is not None:
if fixture_format not in model_mapping:
raise TypeError(f"Unsupported fixture format: {fixture_format}")
model_class = model_mapping[fixture_format]
Expand Down
Loading

0 comments on commit 6e4c3a1

Please sign in to comment.