Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(ethereum_clis): move generic code from TransitionTool to a new generic base class EthereumCLI #894

Merged
merged 7 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ extend-ignore = E203, D107, D200, D203, D205,
# Ignore N806: Variable names with all caps (ALL_CAPS)
max-line-length = 99
per-file-ignore =
tests/evm_transition_tool/test_evaluate.py:E501
tests/ethereum_clis/test_evaluate.py:E501

extend-exclude =
setup.py
src/evm_transition_tool/tests/
src/ethereum_clis/tests/
src/ethereum_test_tools/tests/
src/ethereum_test_forks/tests/

Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Test fixtures for use by clients are available for each release on the [Github r
### 🔧 EVM Tools

- ✨ Fill test fixtures using EELS by default. EEST now uses the [`ethereum-specs-evm-resolver`](https://github.com/petertdavies/ethereum-spec-evm-resolver) with the EELS daemon ([#792](https://github.com/ethereum/execution-spec-tests/pull/792)).
- 🔀 Move the `evm_transition_tool` package to `ethereum_clis` and derive the transition tool CL interfaces from a shared `EthereumCLI` class that can be reused for other sub-commands ([#894](https://github.com/ethereum/execution-spec-tests/pull/894)).

### 📋 Misc

Expand Down
3 changes: 3 additions & 0 deletions docs/library/ethereum_clis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ethereum CLIs Package

::: ethereum_clis
3 changes: 0 additions & 3 deletions docs/library/evm_transition_tool.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/library/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ Execution spec tests consists of several packages that implement helper classes
- [`ethereum_test_tools`](./ethereum_test_tools.md) - provides primitives and helpers to test Ethereum execution clients.
- [`ethereum_test_types`](./ethereum_test_types.md) - provides Ethereum types built on top of the base types which are used to define test cases and interact with other libraries.
- [`ethereum_test_vm`](./ethereum_test_vm.md) - provides definitions for the Ethereum Virtual Machine (EVM) as used to define bytecode in test cases.
- [`evm_transition_tool`](./evm_transition_tool.md) - a wrapper for the transition (`t8n`) tool.
- [`ethereum_clis`](./ethereum_clis.md) - a wrapper for the transition (`t8n`) tool.
- [`pytest_plugins`](./pytest_plugins/index.md) - contains pytest customizations that provide additional functionality for generating test fixtures.
4 changes: 2 additions & 2 deletions docs/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
* [Running Github Actions Locally](dev/test_actions_locally.md)
* [Changelog](CHANGELOG.md)
* [Library Reference](library/index.md)
* [Miscellaneous CLI Tools](library/cli/index.md)
* [EEST CLI Tools](library/cli/index.md)
* [Ethereum Test Base Types Package](library/ethereum_test_base_types.md)
* [Ethereum Test Exceptions Package](library/ethereum_test_exceptions.md)
* [Ethereum Test Fixtures Package](library/ethereum_test_fixtures.md)
Expand All @@ -45,5 +45,5 @@
* [Ethereum Test Tools Package](library/ethereum_test_tools.md)
* [Ethereum Test Types Package](library/ethereum_test_types.md)
* [Ethereum Test VM Package](library/ethereum_test_vm.md)
* [EVM Transition Tool Package](library/evm_transition_tool.md)
* [Ethereum CLIs Package](library/ethereum_clis.md)
* [Pytest Plugins](library/pytest_plugins/index.md)
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
Library of Python wrappers for the different implementations of transition tools.
"""

from .besu import BesuTransitionTool
from .ethereumjs import EthereumJSTransitionTool
from .evmone import EvmOneTransitionTool
from .execution_specs import ExecutionSpecsTransitionTool
from .geth import GethTransitionTool
from .nimbus import NimbusTransitionTool
from .transition_tool import TransitionTool, TransitionToolNotFoundInPath, UnknownTransitionTool
from .clis.besu import BesuTransitionTool
from .clis.ethereumjs import EthereumJSTransitionTool
from .clis.evmone import EvmOneTransitionTool
from .clis.execution_specs import ExecutionSpecsTransitionTool
from .clis.geth import GethTransitionTool
from .clis.nimbus import NimbusTransitionTool
from .ethereum_cli import CLINotFoundInPath, UnknownCLI
from .transition_tool import TransitionTool
from .types import Result, TransitionToolOutput

TransitionTool.set_default_tool(ExecutionSpecsTransitionTool)
Expand All @@ -23,6 +24,6 @@
"Result",
"TransitionTool",
"TransitionToolOutput",
"TransitionToolNotFoundInPath",
"UnknownTransitionTool",
"CLINotFoundInPath",
"UnknownCLI",
)
3 changes: 3 additions & 0 deletions src/ethereum_clis/clis/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Package containing concrete implementations of Ethereum CL interfaces.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
from ethereum_test_forks import Fork
from ethereum_test_types import Alloc, Environment, Transaction

from .transition_tool import TransitionTool, dump_files_to_directory, model_dump_config
from .types import TransitionToolInput, TransitionToolOutput
from ..transition_tool import TransitionTool, dump_files_to_directory, model_dump_config
from ..types import TransitionToolInput, TransitionToolOutput


class BesuTransitionTool(TransitionTool):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""
EthereumJS Transition tool interface.
"""

from pathlib import Path
from re import compile
from typing import Optional

from ethereum_test_forks import Fork

from .transition_tool import TransitionTool
from ..transition_tool import TransitionTool


class EthereumJSTransitionTool(TransitionTool):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""
Evmone Transition tool interface.
"""

from pathlib import Path
from re import compile
from typing import Optional

from ethereum_test_forks import Fork

from .transition_tool import TransitionTool
from ..transition_tool import TransitionTool


class EvmOneTransitionTool(TransitionTool):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from ethereum_test_forks import Fork

from .transition_tool import TransitionTool
from ..transition_tool import TransitionTool

DAEMON_STARTUP_TIMEOUT_SECONDS = 5

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from ethereum_test_fixtures import BlockchainFixture, StateFixture
from ethereum_test_forks import Fork

from .transition_tool import FixtureFormat, TransitionTool, dump_files_to_directory
from ..transition_tool import FixtureFormat, TransitionTool, dump_files_to_directory


class GethTransitionTool(TransitionTool):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from ethereum_test_forks import Fork

from .transition_tool import TransitionTool
from ..transition_tool import TransitionTool


class NimbusTransitionTool(TransitionTool):
Expand Down
154 changes: 154 additions & 0 deletions src/ethereum_clis/ethereum_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""
Base class for interacting with Ethereum CLIs.
"""

import os
import shutil
import subprocess
from abc import ABC, abstractmethod
from itertools import groupby
from pathlib import Path
from re import Pattern
from typing import Any, List, Optional, Type


class UnknownCLI(Exception):
"""Exception raised if an unknown t8n is encountered"""

pass


class CLINotFoundInPath(Exception):
"""Exception raised if the specified t8n tool is not found in the path"""

def __init__(self, message="The transition tool was not found in the path", binary=None):
if binary:
message = f"{message} ({binary})"
super().__init__(message)


class EthereumCLI(ABC):
"""
Abstract base class to help create an Ethereum CLI.
"""

registered_tools: List[Type[Any]] = []
default_tool: Optional[Type[Any]] = None
default_binary: Path
detect_binary_pattern: Pattern
version_flag: str = "-v"
cached_version: Optional[str] = None

# Abstract methods that each tool must implement

@abstractmethod
def __init__(
self,
*,
binary: Optional[Path] = None,
trace: bool = False,
):
"""
Abstract initialization method that all subclasses must implement.
"""
if binary is None:
binary = self.default_binary
else:
# improve behavior of which by resolving the path: ~/relative paths don't work
resolved_path = Path(os.path.expanduser(binary)).resolve()
if resolved_path.exists():
binary = resolved_path
binary = shutil.which(binary) # type: ignore
if not binary:
raise CLINotFoundInPath(binary=binary)
self.binary = Path(binary)
self.trace = trace

@classmethod
def register_tool(cls, tool_subclass: Type[Any]):
"""
Registers a given subclass as tool option.
"""
cls.registered_tools.append(tool_subclass) # raise NotImplementedError

@classmethod
def set_default_tool(cls, tool_subclass: Type[Any]):
"""
Registers the default tool subclass.
"""
cls.default_tool = tool_subclass

@classmethod
def from_binary_path(cls, *, binary_path: Optional[Path], **kwargs) -> Any:
"""
Instantiates the appropriate TransitionTool subclass derived from the
tool's binary path.
"""
assert cls.default_tool is not None, "default transition tool was never set"

if binary_path is None:
return cls.default_tool(binary=binary_path, **kwargs)

resolved_path = Path(os.path.expanduser(binary_path)).resolve()
if resolved_path.exists():
binary_path = resolved_path
binary = shutil.which(binary_path) # type: ignore

if not binary:
raise CLINotFoundInPath(binary=binary)

binary = Path(binary)

# Group the tools by version flag, so we only have to call the tool once for all the
# classes that share the same version flag
for version_flag, subclasses in groupby(
cls.registered_tools, key=lambda x: x.version_flag
):
try:
result = subprocess.run(
[binary, version_flag], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
if result.returncode != 0:
raise Exception(f"Non-zero return code: {result.returncode}")

if result.stderr:
raise Exception(f"Tool wrote to stderr: {result.stderr.decode()}")

binary_output = ""
if result.stdout:
binary_output = result.stdout.decode().strip()
except Exception:
# If the tool doesn't support the version flag,
# we'll get an non-zero exit code.
continue
for subclass in subclasses:
if subclass.detect_binary(binary_output):
return subclass(binary=binary, **kwargs)

raise UnknownCLI(f"Unknown CLI: {binary_path}")

@classmethod
def detect_binary(cls, binary_output: str) -> bool:
"""
Returns True if the binary matches the tool
"""
assert cls.detect_binary_pattern is not None

return cls.detect_binary_pattern.match(binary_output) is not None

def version(self) -> str:
"""
Return name and version of tool used to state transition
"""
if self.cached_version is None:
result = subprocess.run(
[str(self.binary), self.version_flag],
stdout=subprocess.PIPE,
)

if result.returncode != 0:
raise Exception("failed to evaluate: " + result.stderr.decode())

self.cached_version = result.stdout.decode().strip()

return self.cached_version
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
import pytest
from pydantic import TypeAdapter

from ethereum_clis import ExecutionSpecsTransitionTool, TransitionTool
from ethereum_test_base_types import to_json
from ethereum_test_forks import Berlin, Fork, Istanbul, London
from ethereum_test_types import Alloc, Environment, Transaction
from evm_transition_tool import ExecutionSpecsTransitionTool, TransitionTool

FIXTURES_ROOT = Path(os.path.join("src", "evm_transition_tool", "tests", "fixtures"))
FIXTURES_ROOT = Path(os.path.join("src", "ethereum_clis", "tests", "fixtures"))
DEFAULT_EVM_T8N_BINARY_NAME = "ethereum-spec-evm-resolver"


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@

import pytest

from evm_transition_tool import (
from ethereum_clis import (
CLINotFoundInPath,
EvmOneTransitionTool,
ExecutionSpecsTransitionTool,
GethTransitionTool,
NimbusTransitionTool,
TransitionTool,
TransitionToolNotFoundInPath,
)


Expand Down Expand Up @@ -86,8 +86,8 @@ def mock_run(args, **kwargs):

def test_unknown_binary_path():
"""
Test that `from_binary_path` raises `UnknownTransitionTool` for unknown
Test that `from_binary_path` raises `UnknownCLI` for unknown
binary paths.
"""
with pytest.raises(TransitionToolNotFoundInPath):
with pytest.raises(CLINotFoundInPath):
TransitionTool.from_binary_path(binary_path=Path("unknown_binary_path"))
Loading