Skip to content

Commit c14bdb7

Browse files
fix(consume): fix the test re-run hive commands displayed in hive ui (#1494)
* fix(consume): fix the test re-run commands displayed in hive ui * docs: update changelog * fix(consume): only show the client relevant to test in hive command * chore(consume): add `ClientFile` model; serialize to YAML * fix(consume): add forgotten client file to hive command-line * chore(consume): remove debugging log message * fix(consume): add forgotten pytest fixture to function sig --------- Co-authored-by: Mario Vega <[email protected]>
1 parent 14a7429 commit c14bdb7

File tree

6 files changed

+173
-74
lines changed

6 files changed

+173
-74
lines changed

docs/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Users can expect that all tests currently living in [ethereum/tests](https://git
2626

2727
#### `consume`
2828

29+
- 🐞 Fix the Hive commands used to reproduce test executions that are displayed in test descriptions in the Hive UI ([#1494](https://github.com/ethereum/execution-spec-tests/pull/1494)).
2930
- 🐞 Fix consume direct fails for geth blockchain tests ([#1502](https://github.com/ethereum/execution-spec-tests/pull/1502)).
3031

3132
### 📋 Misc

src/pytest_plugins/consume/hive_simulators/conftest.py

+129-56
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import io
44
import json
55
import logging
6+
import textwrap
7+
import urllib
8+
import warnings
69
from pathlib import Path
710
from typing import Dict, Generator, List, Literal, cast
811

@@ -22,7 +25,7 @@
2225
from ethereum_test_rpc import EthRPC
2326
from pytest_plugins.consume.consume import FixturesSource
2427
from pytest_plugins.consume.hive_simulators.ruleset import ruleset # TODO: generate dynamically
25-
from pytest_plugins.pytest_hive.hive_info import ClientInfo
28+
from pytest_plugins.pytest_hive.hive_info import ClientFile, HiveInfo
2629

2730
from .exceptions import EXCEPTION_MAPPERS
2831
from .timing import TimingData
@@ -61,91 +64,161 @@ def eth_rpc(client: Client) -> EthRPC:
6164

6265

6366
@pytest.fixture(scope="function")
64-
def hive_client_config_file_parameter(
65-
client_type: ClientType, client_file: List[ClientInfo]
66-
) -> List[str]:
67-
"""Return the hive client config file that is currently being used to configure tests."""
68-
for client in client_file:
69-
if client_type.name.startswith(client.client):
70-
return ["--client-file", f"<('{client.model_dump_json(exclude_none=True)}')"]
71-
return []
67+
def hive_clients_yaml_target_filename() -> str:
68+
"""Return the name of the target clients YAML file."""
69+
return "clients_eest.yaml"
7270

7371

7472
@pytest.fixture(scope="function")
75-
def hive_consume_command(
76-
test_suite_name: str,
73+
def hive_clients_yaml_generator_command(
7774
client_type: ClientType,
75+
client_file: ClientFile,
76+
hive_clients_yaml_target_filename: str,
77+
hive_info: HiveInfo,
78+
) -> str:
79+
"""Generate a shell command that creates a clients YAML file for the current client."""
80+
try:
81+
if not client_file:
82+
raise ValueError("No client information available - try updating hive")
83+
client_config = [c for c in client_file.root if c.client in client_type.name]
84+
if not client_config:
85+
raise ValueError(f"Client '{client_type.name}' not found in client file")
86+
try:
87+
yaml_content = ClientFile(root=[client_config[0]]).yaml().replace(" ", "&nbsp;")
88+
return f'echo "\\\n{yaml_content}" > {hive_clients_yaml_target_filename}'
89+
except Exception as e:
90+
raise ValueError(f"Failed to generate YAML: {str(e)}") from e
91+
except ValueError as e:
92+
error_message = str(e)
93+
warnings.warn(
94+
f"{error_message}. The Hive clients YAML generator command will not be available.",
95+
stacklevel=2,
96+
)
97+
98+
issue_title = f"Client {client_type.name} configuration issue"
99+
issue_body = f"Error: {error_message}\nHive version: {hive_info.commit}\n"
100+
issue_url = f"https://github.com/ethereum/execution-spec-tests/issues/new?title={urllib.parse.quote(issue_title)}&body={urllib.parse.quote(issue_body)}"
101+
102+
return (
103+
f"Error: {error_message}\n"
104+
f'Please <a href="{issue_url}">create an issue</a> to report this problem.'
105+
)
106+
107+
108+
@pytest.fixture(scope="function")
109+
def filtered_hive_options(hive_info: HiveInfo) -> List[str]:
110+
"""Filter Hive command options to remove unwanted options."""
111+
logger.info("Hive info: %s", hive_info.command)
112+
113+
unwanted_options = [
114+
"--client", # gets overwritten: we specify a single client; the one from the test case
115+
"--client-file", # gets overwritten: we'll write our own client file
116+
"--results-root", # use default value instead (or you have to pass it to ./hiveview)
117+
"--sim.limit", # gets overwritten: we only run the current test case id
118+
"--sim.parallelism", # skip; we'll only be running a single test
119+
]
120+
121+
command_parts = []
122+
skip_next = False
123+
for part in hive_info.command:
124+
if skip_next:
125+
skip_next = False
126+
continue
127+
128+
if part in unwanted_options:
129+
skip_next = True
130+
continue
131+
132+
if any(part.startswith(f"{option}=") for option in unwanted_options):
133+
continue
134+
135+
command_parts.append(part)
136+
137+
return command_parts
138+
139+
140+
@pytest.fixture(scope="function")
141+
def hive_client_config_file_parameter(hive_clients_yaml_target_filename: str) -> str:
142+
"""Return the hive client config file parameter."""
143+
return f"--client-file {hive_clients_yaml_target_filename}"
144+
145+
146+
@pytest.fixture(scope="function")
147+
def hive_consume_command(
78148
test_case: TestCaseIndexFile | TestCaseStream,
79-
hive_client_config_file_parameter: List[str],
80-
) -> List[str]:
149+
hive_client_config_file_parameter: str,
150+
filtered_hive_options: List[str],
151+
client_type: ClientType,
152+
) -> str:
81153
"""Command to run the test within hive."""
82-
command = ["./hive", "--sim", f"ethereum/{test_suite_name}"]
83-
if hive_client_config_file_parameter:
84-
command += hive_client_config_file_parameter
85-
command += ["--client", client_type.name, "--sim.limit", f'"id:{test_case.id}"']
86-
return command
154+
command_parts = filtered_hive_options.copy()
155+
command_parts.append(f"{hive_client_config_file_parameter}")
156+
command_parts.append(f"--client={client_type.name}")
157+
command_parts.append(f'--sim.limit="id:{test_case.id}"')
158+
159+
return " ".join(command_parts)
87160

88161

89162
@pytest.fixture(scope="function")
90163
def hive_dev_command(
91164
client_type: ClientType,
92-
hive_client_config_file_parameter: List[str],
93-
) -> List[str]:
165+
hive_client_config_file_parameter: str,
166+
) -> str:
94167
"""Return the command used to instantiate hive alongside the `consume` command."""
95-
hive_dev = ["./hive", "--dev"]
96-
if hive_client_config_file_parameter:
97-
hive_dev += hive_client_config_file_parameter
98-
hive_dev += ["--client", client_type.name]
99-
return hive_dev
168+
return f"./hive --dev {hive_client_config_file_parameter} --client {client_type.name}"
100169

101170

102171
@pytest.fixture(scope="function")
103172
def eest_consume_command(
104173
test_suite_name: str,
105174
test_case: TestCaseIndexFile | TestCaseStream,
106175
fixture_source_flags: List[str],
107-
) -> List[str]:
176+
) -> str:
108177
"""Commands to run the test within EEST using a hive dev back-end."""
178+
flags = " ".join(fixture_source_flags)
109179
return (
110-
["consume", test_suite_name.split("-")[-1], "-v"]
111-
+ fixture_source_flags
112-
+ [
113-
"-k",
114-
f'"{test_case.id}"',
115-
]
180+
f"uv run consume {test_suite_name.split('-')[-1]} "
181+
f'{flags} --sim.limit="id:{test_case.id}" -v -s'
116182
)
117183

118184

119185
@pytest.fixture(scope="function")
120186
def test_case_description(
121187
fixture: BaseFixture,
122188
test_case: TestCaseIndexFile | TestCaseStream,
123-
hive_consume_command: List[str],
124-
hive_dev_command: List[str],
125-
eest_consume_command: List[str],
189+
hive_clients_yaml_generator_command: str,
190+
hive_consume_command: str,
191+
hive_dev_command: str,
192+
eest_consume_command: str,
126193
) -> str:
127-
"""
128-
Create the description of the current blockchain fixture test case.
129-
Includes reproducible commands to re-run the test case against the target client.
130-
"""
131-
description = f"Test id: {test_case.id}"
132-
if "url" in fixture.info:
133-
description += f"\n\nTest source: {fixture.info['url']}"
134-
if "description" not in fixture.info:
135-
description += "\n\nNo description field provided in the fixture's 'info' section."
194+
"""Create the description of the current blockchain fixture test case."""
195+
test_url = fixture.info.get("url", "")
196+
197+
if "description" not in fixture.info or fixture.info["description"] is None:
198+
test_docstring = "No documentation available."
136199
else:
137-
description += f"\n\n{fixture.info['description']}"
138-
description += (
139-
f"\n\nCommand to reproduce entirely in hive:"
140-
f"\n<code>{' '.join(hive_consume_command)}</code>"
141-
)
142-
eest_commands = "\n".join(
143-
f"{i + 1}. <code>{' '.join(cmd)}</code>"
144-
for i, cmd in enumerate([hive_dev_command, eest_consume_command])
145-
)
146-
description += (
147-
f"\n\nCommands to reproduce within EEST using a hive dev back-end:\n{eest_commands}"
148-
)
200+
# this prefix was included in the fixture description field for fixtures <= v4.3.0
201+
test_docstring = fixture.info["description"].replace("Test function documentation:\n", "") # type: ignore
202+
203+
description = textwrap.dedent(f"""
204+
<b>Test Details</b>
205+
<code>{test_case.id}</code>
206+
{f'<a href="{test_url}">[source]</a>' if test_url else ""}
207+
208+
{test_docstring}
209+
210+
<b>Run This Test Locally:</b>
211+
To run this test in <a href="https://github.com/ethereum/hive">hive</a></i>:
212+
<code>{hive_clients_yaml_generator_command}
213+
{hive_consume_command}</code>
214+
215+
<b>Advanced: Run the test against a hive developer backend using EEST's <code>consume</code> command</b>
216+
Create the client YAML file, as above, then:
217+
1. Start hive in dev mode: <code>{hive_dev_command}</code>
218+
2. In the EEST repository root: <code>{eest_consume_command}</code>
219+
""") # noqa: E501
220+
221+
description = description.strip()
149222
description = description.replace("\n", "<br/>")
150223
return description
151224

+37-8
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,54 @@
11
"""Hive instance information structures."""
22

3-
from typing import List
3+
from typing import Dict, List, Optional
44

5-
from pydantic import BaseModel, Field
5+
import yaml
6+
from pydantic import BaseModel, Field, RootModel
67

78
from ethereum_test_base_types import CamelModel
89

910

10-
class ClientInfo(BaseModel):
11-
"""Client information."""
11+
class YAMLModel(BaseModel):
12+
"""A helper class for YAML serialization of pydantic models."""
13+
14+
def yaml(self, **kwargs):
15+
"""Return the YAML representation of the model."""
16+
return yaml.dump(self.model_dump(), **kwargs)
17+
18+
@classmethod
19+
def parse_yaml(cls, yaml_string):
20+
"""Parse a YAML string into a model instance."""
21+
data = yaml.safe_load(yaml_string)
22+
return cls(**data)
23+
24+
25+
class ClientConfig(YAMLModel):
26+
"""
27+
Client configuration for YAML serialization.
28+
29+
Represents a single client entry in the clients.yaml file.
30+
"""
1231

1332
client: str
14-
nametag: str | None = None
15-
dockerfile: str | None = None
16-
build_args: dict[str, str] | None = None
33+
nametag: Optional[str] = None
34+
dockerfile: Optional[str] = None
35+
build_args: Optional[Dict[str, str]] = Field(default_factory=lambda: {})
36+
37+
38+
class ClientFile(RootModel, YAMLModel):
39+
"""
40+
Represents the entire clients.yaml file structure.
41+
42+
The clients.yaml file is a list of client configurations.
43+
"""
44+
45+
root: List[ClientConfig]
1746

1847

1948
class HiveInfo(CamelModel):
2049
"""Hive instance information."""
2150

2251
command: List[str]
23-
client_file: List[ClientInfo] = Field(default_factory=list)
52+
client_file: ClientFile = Field(default_factory=lambda: ClientFile(root=[]))
2453
commit: str
2554
date: str

src/pytest_plugins/pytest_hive/pytest_hive.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import warnings
3535
from dataclasses import asdict
3636
from pathlib import Path
37-
from typing import List
3837

3938
import pytest
4039
from filelock import FileLock
@@ -43,7 +42,7 @@
4342
from hive.testing import HiveTest, HiveTestResult, HiveTestSuite
4443

4544
from ..logging import get_logger
46-
from .hive_info import ClientInfo, HiveInfo
45+
from .hive_info import ClientFile, HiveInfo
4746

4847
logger = get_logger(__name__)
4948

@@ -122,7 +121,7 @@ def pytest_report_header(config, start_path):
122121
f"hive commit: {hive_info.commit}",
123122
f"hive date: {hive_info.date}",
124123
]
125-
for client in hive_info.client_file:
124+
for client in hive_info.client_file.root:
126125
header_lines += [
127126
f"hive client ({client.client}): {client.model_dump_json(exclude_none=True)}",
128127
]
@@ -160,10 +159,10 @@ def hive_info(simulator: Simulation):
160159

161160

162161
@pytest.fixture(scope="session")
163-
def client_file(hive_info: HiveInfo | None) -> List[ClientInfo]:
162+
def client_file(hive_info: HiveInfo | None) -> ClientFile:
164163
"""Return the client file used when launching hive."""
165164
if hive_info is None:
166-
return []
165+
return ClientFile(root=[])
167166
return hive_info.client_file
168167

169168

src/pytest_plugins/shared/execute_fill.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,7 @@ def test_case_description(request: pytest.FixtureRequest) -> str:
158158
if hasattr(request.node, "cls"):
159159
test_class_doc = f"Test class documentation:\n{request.cls.__doc__}" if request.cls else ""
160160
if hasattr(request.node, "function"):
161-
test_function_doc = (
162-
f"Test function documentation:\n{request.function.__doc__}"
163-
if request.function.__doc__
164-
else ""
165-
)
161+
test_function_doc = f"{request.function.__doc__}" if request.function.__doc__ else ""
166162
if not test_class_doc and not test_function_doc:
167163
return description_unavailable
168164
combined_docstring = f"{test_class_doc}\n\n{test_function_doc}".strip()

whitelist.txt

+1
Original file line numberDiff line numberDiff line change
@@ -883,3 +883,4 @@ coeffs
883883
uncomp
884884
isogeny
885885
codomain
886+
nametag

0 commit comments

Comments
 (0)