|
3 | 3 | import io
|
4 | 4 | import json
|
5 | 5 | import logging
|
| 6 | +import textwrap |
| 7 | +import urllib |
| 8 | +import warnings |
6 | 9 | from pathlib import Path
|
7 | 10 | from typing import Dict, Generator, List, Literal, cast
|
8 | 11 |
|
|
22 | 25 | from ethereum_test_rpc import EthRPC
|
23 | 26 | from pytest_plugins.consume.consume import FixturesSource
|
24 | 27 | 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 |
26 | 29 |
|
27 | 30 | from .exceptions import EXCEPTION_MAPPERS
|
28 | 31 | from .timing import TimingData
|
@@ -61,91 +64,161 @@ def eth_rpc(client: Client) -> EthRPC:
|
61 | 64 |
|
62 | 65 |
|
63 | 66 | @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" |
72 | 70 |
|
73 | 71 |
|
74 | 72 | @pytest.fixture(scope="function")
|
75 |
| -def hive_consume_command( |
76 |
| - test_suite_name: str, |
| 73 | +def hive_clients_yaml_generator_command( |
77 | 74 | 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(" ", " ") |
| 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( |
78 | 148 | 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: |
81 | 153 | """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) |
87 | 160 |
|
88 | 161 |
|
89 | 162 | @pytest.fixture(scope="function")
|
90 | 163 | def hive_dev_command(
|
91 | 164 | client_type: ClientType,
|
92 |
| - hive_client_config_file_parameter: List[str], |
93 |
| -) -> List[str]: |
| 165 | + hive_client_config_file_parameter: str, |
| 166 | +) -> str: |
94 | 167 | """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}" |
100 | 169 |
|
101 | 170 |
|
102 | 171 | @pytest.fixture(scope="function")
|
103 | 172 | def eest_consume_command(
|
104 | 173 | test_suite_name: str,
|
105 | 174 | test_case: TestCaseIndexFile | TestCaseStream,
|
106 | 175 | fixture_source_flags: List[str],
|
107 |
| -) -> List[str]: |
| 176 | +) -> str: |
108 | 177 | """Commands to run the test within EEST using a hive dev back-end."""
|
| 178 | + flags = " ".join(fixture_source_flags) |
109 | 179 | 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' |
116 | 182 | )
|
117 | 183 |
|
118 | 184 |
|
119 | 185 | @pytest.fixture(scope="function")
|
120 | 186 | def test_case_description(
|
121 | 187 | fixture: BaseFixture,
|
122 | 188 | 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, |
126 | 193 | ) -> 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." |
136 | 199 | 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() |
149 | 222 | description = description.replace("\n", "<br/>")
|
150 | 223 | return description
|
151 | 224 |
|
|
0 commit comments