Skip to content

Commit 3eddd28

Browse files
feat(consume): enable logging, propagate logs to hive results (ethereum#1361)
Co-authored-by: spencer <[email protected]>
1 parent 0fc6bd8 commit 3eddd28

File tree

15 files changed

+1003
-18
lines changed

15 files changed

+1003
-18
lines changed

docs/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ consume cache --help
3131
- ✨ Allow filtering of test cases by fork via pytest marks (e.g., `-m "Cancun or Prague"`) ([#1304](https://github.com/ethereum/execution-spec-tests/pull/1304), [#1318](https://github.com/ethereum/execution-spec-tests/pull/1318)).
3232
- ✨ Allow filtering of test cases by fixture format via pytest marks (e.g., `-m blockchain_test`) ([#1314](https://github.com/ethereum/execution-spec-tests/pull/1314)).
3333
- ✨ Add top-level entries `forks` and `fixture_formats` to the index file that list all the forks and fixture formats used in the indexed fixtures ([#1318](https://github.com/ethereum/execution-spec-tests/pull/1318)).
34-
- 🐞 Don't parametrize tests for unsupported fixture formats; improve `consume` test collection ([#1315](https://github.com/ethereum/execution-spec-tests/pull/1314)).
34+
- ✨ Enable logging from `consume` commands ([#1361](https://github.com/ethereum/execution-spec-tests/pull/1361)).
35+
- ✨ Propagate stdout and stderr (including logs) captured during test execution to the Hive test result ([#1361](https://github.com/ethereum/execution-spec-tests/pull/1361)).
36+
- 🐞 Don't parametrize tests for unsupported fixture formats; improve `consume` test collection ([#1315](https://github.com/ethereum/execution-spec-tests/pull/1315)).
3537
- 🐞 Fix the the hive command printed in test reports to reproduce tests in isolation by prefixing the `--sim.limit` flag value with `id:` ([#1333](https://github.com/ethereum/execution-spec-tests/pull/1333)).
3638
- 🐞 Improve index generation of [ethereum/tests](https://github.com/ethereum/tests) fixtures: Allow generation at any directory level and include `generatedTestHash` in the index file for the `fixture_hash` ([#1303](https://github.com/ethereum/execution-spec-tests/pull/1303)).
3739
- 🐞 Fix loading of [ethereum/tests](https://github.com/ethereum/tests) and [ethereum/legacytests](https://github.com/ethereum/legacytests) fixtures for the case of mixed `0x0` and `0x1` transaction types in multi-index (`data`, `gas`, `value`) state test fixtures ([#1330](https://github.com/ethereum/execution-spec-tests/pull/1330)).

docs/dev/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This documentation is aimed at maintainers of `execution-spec-tests` but may be
77
- [Generating documentation](./docs.md): Steps to create and build documentation for the project.
88
- [Documenting CLI commands](./documenting_clis.md): Instructions for documenting command line interfaces (CLIs).
99
- [Coding style](./coding_style.md): Standards and best practices for code formatting and to maintain consistency across the repository.
10+
- [Logging](./logging.md): Documentation on using the custom logging system with enhanced features.
1011
- [Enabling pre-commit checks](./precommit.md): A guide for setting up pre-commit hooks to enforce code quality before commits.
1112
- [Running github actions locally](./test_actions_locally.md): Instructions for testing GitHub Actions workflows on your local machine to streamline development and debugging.
1213
- [Porting tests](./porting_legacy_tests.md): A guide to porting legacy ethereum tests to EEST.

docs/dev/logging.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Logging
2+
3+
This document describes the logging system used in the Ethereum Execution Spec Tests project. Currently, logging is only supported for `consume` commands.
4+
5+
## Overview
6+
7+
The project uses Python's standard logging module with custom extensions to provide enhanced logging capabilities. Our logging system is implemented in the `src/pytest_plugins/logging.py` module and provides the following features:
8+
9+
- Custom log levels between the standard Python log levels
10+
- Timestamps with millisecond precision in UTC
11+
- Color-coded log output (when not running in Docker)
12+
- File logging with a consistent naming pattern
13+
- Integration with pytest's output capture
14+
- Support for distributed test execution with pytest-xdist
15+
16+
## Custom Log Levels
17+
18+
In addition to the standard Python log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL), we've added the following custom levels:
19+
20+
| Level | Numeric Value | Purpose |
21+
|-------|--------------|---------|
22+
| VERBOSE | 15 | For messages more detailed than INFO but less verbose than DEBUG |
23+
| FAIL | 35 | For test failures and related issues (between WARNING and ERROR) |
24+
25+
## Using the Logger
26+
27+
### Getting a Logger
28+
29+
To use the custom logger in your code, import the `get_logger` function from the logging module:
30+
31+
```python
32+
from pytest_plugins.logging import get_logger
33+
34+
# Create a logger with your module's name
35+
logger = get_logger(__name__)
36+
```
37+
38+
### Logging at Different Levels
39+
40+
You can use all standard Python log methods plus our custom methods:
41+
42+
```python
43+
# Standard log levels
44+
logger.debug("Detailed debug information")
45+
logger.info("General information")
46+
logger.warning("Warning message")
47+
logger.error("Error message")
48+
logger.critical("Critical failure")
49+
50+
# Custom log levels
51+
logger.verbose("More detailed than INFO, less than DEBUG")
52+
logger.fail("Test failure or similar issue")
53+
```
54+
55+
### When to Use Each Level
56+
57+
- **DEBUG (10)**: For very detailed diagnostic information useful for debugging
58+
- **VERBOSE (15)**: For information that's useful during development but more detailed than INFO
59+
- **INFO (20)**: For general information about program operation
60+
- **WARNING (30)**: For potential issues that don't prevent program execution
61+
- **FAIL (35)**: For test failures and related issues
62+
- **ERROR (40)**: For errors that prevent an operation from completing
63+
- **CRITICAL (50)**: For critical errors that may prevent the program from continuing
64+
65+
## Configuration
66+
67+
### Setting Log Level on the Command Line
68+
69+
You can adjust the log level when running pytest with the `--eest-log-level` option:
70+
71+
```bash
72+
consume engine --input=latest@stable --eest-log-level=VERBOSE -s --sim.limit=".*chainid.*"
73+
```
74+
75+
The argument accepts both log level names (e.g., "DEBUG", "VERBOSE", "INFO") and numeric values.
76+
77+
Adding pytest's `-s` flag writes the logging messages to the terminal; otherwise output will be written to the log file that is reported in the test session header at the end of the test session.
78+
79+
### Log File Output
80+
81+
Log files are automatically created in the `logs/` directory with a naming pattern that includes:
82+
83+
- The command name, e.g. `consume`,
84+
- An optional subcommand (e.g., `engine`),
85+
- A timestamp in UTC,
86+
- The worker ID (when using pytest-xdist).
87+
88+
Example: `consume-engine-20240101-123456-main.log`
89+
90+
The log file path is displayed in the pytest header and summary.
91+
92+
### Using the Standalone Configuration in Non-Pytest Projects
93+
94+
The logging module can also be used in non-pytest projects by using the `configure_logging` function:
95+
96+
```python
97+
from pytest_plugins.logging import configure_logging, get_logger
98+
99+
# Configure logging with custom settings
100+
configure_logging(
101+
log_level="VERBOSE",
102+
log_file="my_application.log",
103+
log_to_stdout=True,
104+
log_format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
105+
use_color=True
106+
)
107+
108+
# Get a logger and use it
109+
logger = get_logger(__name__)
110+
logger.verbose("Logging configured in standalone application!")
111+
```
112+
113+
The `configure_logging` function accepts the following parameters:
114+
115+
- `log_level`: A string or numeric log level (default: "INFO")
116+
- `log_file`: Path to a log file, or None to disable file logging (default: None)
117+
- `log_to_stdout`: Whether to log to stdout (default: True)
118+
- `log_format`: The format string for log messages (default: "%(asctime)s [%(levelname)s] %(name)s: %(message)s")
119+
- `use_color`: Whether to use colors in stdout output, or None to auto-detect (default: None)
120+
121+
## Implementation Details
122+
123+
### The EESTLogger Class
124+
125+
The `EESTLogger` class extends Python's `Logger` class to add the custom log methods. The main module logger is automatically configured to use this class.
126+
127+
### Formatters
128+
129+
Two formatter classes are available:
130+
131+
- `UTCFormatter`: Formats timestamps with millisecond precision in UTC
132+
- `ColorFormatter`: Extends `UTCFormatter` to add ANSI colors to log level names in terminal output
133+
134+
### Pytest and Hive Integration
135+
136+
The logging module includes several pytest hooks to:
137+
138+
- Configure logging at the start of a test session
139+
- Record logs during test execution
140+
- Display the log file path in the test report
141+
- Ensure logs are captured properly during fixture teardown
142+
143+
The `hive_pytest` plugin has been extended to propagate the logs to the `hiveview` UI via the test case's `details` ("description" in `hiveview`).
144+
145+
## Example Usage
146+
147+
A complete example of using the logging system in a `consume` test (or plugin):
148+
149+
```python
150+
from pytest_plugins.logging import get_logger
151+
152+
# Get a properly typed logger for your module
153+
logger = get_logger(__name__)
154+
155+
def test_something():
156+
# Use standard log levels
157+
logger.debug("Setting up test variables")
158+
logger.info("Starting test")
159+
160+
# Use custom log levels
161+
logger.verbose("Additional details about test execution")
162+
163+
# Log warnings or errors as needed
164+
if something_concerning:
165+
logger.warning("Something looks suspicious")
166+
167+
if something_failed:
168+
logger.fail("Test condition not met")
169+
assert False, "Test failed"
170+
171+
# Log successful completion
172+
logger.info("Test completed successfully")
173+
```
174+
175+
All log messages will be displayed according to the configured log level and captured in the log file.

docs/navigation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
* [Generating Documentation](dev/docs.md)
3939
* [Documenting CLI Commands](dev/documenting_clis.md)
4040
* [Coding Style](dev/coding_style.md)
41+
* [Logging](dev/logging.md)
4142
* [Enabling Precommit Checks](dev/precommit.md)
4243
* [Running Github Actions Locally](dev/test_actions_locally.md)
4344
* [Porting Legacy Tests](dev/porting_legacy_tests.md)

pytest-consume.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ addopts =
66
-rxXs
77
--tb short
88
-p pytest_plugins.concurrency
9+
# disable pytest built-in logging entirely `-p no:logging`
10+
-p no:logging
11+
-p pytest_plugins.logging.logging
912
-p pytest_plugins.consume.consume
1013
-p pytest_plugins.help.help

src/pytest_plugins/consume/hive_simulators/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io
44
import json
5+
import logging
56
from pathlib import Path
67
from typing import Dict, Generator, List, Literal, cast
78

@@ -24,6 +25,8 @@
2425

2526
from .timing import TimingData
2627

28+
logger = logging.getLogger(__name__)
29+
2730

2831
def pytest_addoption(parser):
2932
"""Hive simulator specific consume command line options."""
@@ -202,6 +205,7 @@ def client(
202205
total_timing_data: TimingData,
203206
) -> Generator[Client, None, None]:
204207
"""Initialize the client with the appropriate files and environment variables."""
208+
logger.info(f"Starting client ({client_type.name})...")
205209
with total_timing_data.time("Start client"):
206210
client = hive_test.start_client(
207211
client_type=client_type, environment=environment, files=client_files
@@ -211,9 +215,12 @@ def client(
211215
"setup. Check the client or Hive server logs for more information."
212216
)
213217
assert client is not None, error_message
218+
logger.info(f"Client ({client_type.name}) ready!")
214219
yield client
220+
logger.info(f"Stopping client ({client_type.name})...")
215221
with total_timing_data.time("Stop client"):
216222
client.stop()
223+
logger.info(f"Client ({client_type.name}) stopped!")
217224

218225

219226
@pytest.fixture(scope="function", autouse=True)

src/pytest_plugins/consume/hive_simulators/engine/test_via_engine.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
from ethereum_test_rpc import EngineRPC, EthRPC
1010
from ethereum_test_rpc.types import ForkchoiceState, JSONRPCError, PayloadStatusEnum
1111
from pytest_plugins.consume.hive_simulators.exceptions import GenesisBlockMismatchExceptionError
12+
from pytest_plugins.logging import get_logger
1213

1314
from ..timing import TimingData
1415

16+
logger = get_logger(__name__)
17+
1518

1619
def test_blockchain_via_engine(
1720
timing_data: TimingData,
@@ -27,48 +30,72 @@ def test_blockchain_via_engine(
2730
"""
2831
# Send a initial forkchoice update
2932
with timing_data.time("Initial forkchoice update"):
33+
logger.info("Sending initial forkchoice update to genesis block...")
3034
forkchoice_response = engine_rpc.forkchoice_updated(
3135
forkchoice_state=ForkchoiceState(
3236
head_block_hash=fixture.genesis.block_hash,
3337
),
3438
payload_attributes=None,
3539
version=fixture.payloads[0].forkchoice_updated_version,
3640
)
41+
status = forkchoice_response.payload_status.status
42+
logger.info(f"Initial forkchoice update response: {status}")
3743
assert forkchoice_response.payload_status.status == PayloadStatusEnum.VALID, (
3844
f"unexpected status on forkchoice updated to genesis: {forkchoice_response}"
3945
)
4046

4147
with timing_data.time("Get genesis block"):
48+
logger.info("Calling getBlockByNumber to get genesis block...")
4249
genesis_block = eth_rpc.get_block_by_number(0)
4350
if genesis_block["hash"] != str(fixture.genesis.block_hash):
51+
expected = fixture.genesis.block_hash
52+
got = genesis_block["hash"]
53+
logger.fail(f"Genesis block hash mismatch. Expected: {expected}, Got: {got}")
4454
raise GenesisBlockMismatchExceptionError(
4555
expected_header=fixture.genesis,
4656
got_genesis_block=genesis_block,
4757
)
4858

4959
with timing_data.time("Payloads execution") as total_payload_timing:
60+
logger.info(f"Starting execution of {len(fixture.payloads)} payloads...")
5061
for i, payload in enumerate(fixture.payloads):
62+
logger.info(f"Processing payload {i + 1}/{len(fixture.payloads)}...")
5163
with total_payload_timing.time(f"Payload {i + 1}") as payload_timing:
5264
with payload_timing.time(f"engine_newPayloadV{payload.new_payload_version}"):
65+
logger.info(f"Sending engine_newPayloadV{payload.new_payload_version}...")
66+
expected_validity = "VALID" if payload.valid() else "INVALID"
67+
logger.info(f"Expected payload validity: {expected_validity}")
5368
try:
5469
payload_response = engine_rpc.new_payload(
5570
*payload.params,
5671
version=payload.new_payload_version,
5772
)
73+
logger.info(f"Payload response status: {payload_response.status}")
5874
assert payload_response.status == (
5975
PayloadStatusEnum.VALID
6076
if payload.valid()
6177
else PayloadStatusEnum.INVALID
6278
), f"unexpected status: {payload_response}"
6379
if payload.error_code is not None:
80+
error_code = payload.error_code
81+
logger.fail(
82+
f"Client failed to raise expected Engine API error code: "
83+
f"{error_code}"
84+
)
6485
raise Exception(
6586
"Client failed to raise expected Engine API error code: "
6687
f"{payload.error_code}"
6788
)
6889
except JSONRPCError as e:
90+
logger.info(f"JSONRPC error encountered: {e.code} - {e.message}")
6991
if payload.error_code is None:
92+
logger.fail(f"Unexpected error: {e.code} - {e.message}")
7093
raise Exception(f"unexpected error: {e.code} - {e.message}") from e
7194
if e.code != payload.error_code:
95+
expected_code = payload.error_code
96+
logger.fail(
97+
f"Unexpected error code: {e.code}, expected: {expected_code}"
98+
)
7299
raise Exception(
73100
f"unexpected error code: {e.code}, expected: {payload.error_code}"
74101
) from e
@@ -78,13 +105,18 @@ def test_blockchain_via_engine(
78105
f"engine_forkchoiceUpdatedV{payload.forkchoice_updated_version}"
79106
):
80107
# Send a forkchoice update to the engine
108+
version = payload.forkchoice_updated_version
109+
logger.info(f"Sending engine_forkchoiceUpdatedV{version}...")
81110
forkchoice_response = engine_rpc.forkchoice_updated(
82111
forkchoice_state=ForkchoiceState(
83112
head_block_hash=payload.params[0].block_hash,
84113
),
85114
payload_attributes=None,
86115
version=payload.forkchoice_updated_version,
87116
)
117+
status = forkchoice_response.payload_status.status
118+
logger.info(f"Forkchoice update response: {status}")
88119
assert (
89120
forkchoice_response.payload_status.status == PayloadStatusEnum.VALID
90121
), f"unexpected status: {forkchoice_response}"
122+
logger.info("All payloads processed successfully.")

src/pytest_plugins/consume/hive_simulators/rlp/test_via_rlp.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
Clients consume the genesis and RLP-encoded blocks from input files upon start-up.
66
"""
77

8+
import logging
9+
810
from ethereum_test_fixtures import BlockchainFixture
911
from ethereum_test_rpc import EthRPC
1012
from pytest_plugins.consume.hive_simulators.exceptions import GenesisBlockMismatchExceptionError
1113

1214
from ..timing import TimingData
1315

16+
logger = logging.getLogger(__name__)
17+
1418

1519
def test_via_rlp(
1620
timing_data: TimingData,
@@ -22,12 +26,16 @@ def test_via_rlp(
2226
2. Check the client last block hash matches `fixture.last_block_hash`.
2327
"""
2428
with timing_data.time("Get genesis block"):
29+
logger.info("Calling getBlockByNumber to get genesis block...")
2530
genesis_block = eth_rpc.get_block_by_number(0)
31+
assert genesis_block, "`getBlockByNumber` didn't return a block."
2632
if genesis_block["hash"] != str(fixture.genesis.block_hash):
2733
raise GenesisBlockMismatchExceptionError(
2834
expected_header=fixture.genesis,
2935
got_genesis_block=genesis_block,
3036
)
3137
with timing_data.time("Get latest block"):
38+
logger.info("Calling getBlockByNumber to get latest block...")
3239
block = eth_rpc.get_block_by_number("latest")
40+
assert block, "`getBlockByNumber` didn't return a block."
3341
assert block["hash"] == str(fixture.last_block_hash), "hash mismatch in last block"

0 commit comments

Comments
 (0)