Skip to content

Commit 49e519d

Browse files
committed
better pytest integration, still WIP
1 parent 4f42272 commit 49e519d

File tree

2 files changed

+141
-83
lines changed

2 files changed

+141
-83
lines changed

src/pytest_plugins/execute/eth_config/eth_config.py

Lines changed: 41 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
"""Pytest plugin to test the `eth_config` RPC endpoint in a node."""
22

3-
import json
43
import re
5-
from hashlib import sha256
64
from os.path import realpath
75
from pathlib import Path
8-
from typing import List, Tuple
6+
from typing import Dict, List, Tuple
97

108
import pytest
119
import requests
@@ -80,7 +78,10 @@ def pytest_configure(config: pytest.Config) -> None:
8078
network_name = config.getoption("network")
8179
network_configs_path = config.getoption("network_config_file")
8280
clients = config.getoption("clients")
83-
rpc_endpoint = config.getoption("rpc_endpoint")
81+
82+
# set flags for defining whether to run majority eth_config test or not, and how
83+
config.option.majority_eth_config_test_enabled = False
84+
config.option.majority_clients = [] # List[str]
8485

8586
# either load network file or activate majority test mode
8687
if network_configs_path is not None: # case 1: load provided networks file
@@ -105,9 +106,9 @@ def pytest_configure(config: pytest.Config) -> None:
105106
pytest.exit(f"Unsupported client was passed: {c}")
106107
print(f"Activating majority mode\nProvided client list: {clients}")
107108

108-
# request and store and compare all eth_config responses, then terminate
109-
majority_eth_config_test(exec_clients=clients, rpc_endpoint=rpc_endpoint)
110-
return # TODO: how to not run the other tests, exit(1)?
109+
# store majority mode configuration
110+
config.option.majority_eth_config_test_enabled = True
111+
config.option.majority_clients = clients # List[str]
111112

112113
if config.getoption("collectonly", default=False):
113114
return
@@ -138,18 +139,43 @@ def eth_rpc(rpc_endpoint: str) -> EthRPC:
138139
return EthRPC(rpc_endpoint)
139140

140141

141-
def majority_eth_config_test(exec_clients: List[str], rpc_endpoint: str): # noqa: D103
142+
def get_eth_config(url: str) -> Tuple[bool, str]: # success, response
143+
"""Request data from devnet node via JSON_RPC."""
144+
payload = {
145+
"jsonrpc": "2.0",
146+
"method": "eth_config",
147+
"params": [],
148+
"id": 1,
149+
}
150+
151+
headers = {"Content-Type": "application/json"}
152+
153+
try:
154+
# Make the request
155+
response = requests.post(url, json=payload, headers=headers, timeout=20)
156+
157+
# Return JSON response
158+
return True, response.json()
159+
160+
except Exception as e:
161+
return False, f"error: {e}"
162+
163+
164+
def get_rpc_url_combinations_el_cl(
165+
el_clients: List[str], rpc_endpoint: str
166+
) -> None | Dict[str, List[str]]:
167+
"""Get cl+el url combinations for json rpc."""
142168
# sanity checks
143169
assert ".ethpandaops.io" in rpc_endpoint
144-
assert len(exec_clients) > 0
145-
if "geth" in exec_clients and "devnet-3" in rpc_endpoint:
146-
print("Devnet-3 geth does not support eth_config")
147-
return
170+
assert len(el_clients) > 0
171+
if "geth" in el_clients and "fusaka-devnet-3" in rpc_endpoint:
172+
print("fusaka-devnet-3 geth does not support eth_config")
173+
return None
148174

149175
# generate client-specific URLs from provided rpc_endpoint (it does not matter which client the original rpc_endpoint specifies) # noqa: E501
150176
# we want all combinations of consensus and execution clients (sometimes an exec client is only reachable via a subset of consensus client combinations) # noqa: E501
151177
pattern = r"(.*?@rpc\.)([^-]+)-([^-]+)(-.*)"
152-
url_dict = {
178+
url_dict: Dict[str, List[str]] = {
153179
exec_client: [
154180
re.sub(
155181
pattern,
@@ -158,7 +184,7 @@ def majority_eth_config_test(exec_clients: List[str], rpc_endpoint: str): # noq
158184
)
159185
for consensus in CONSENSUS_CLIENTS
160186
]
161-
for exec_client in exec_clients
187+
for exec_client in el_clients
162188
}
163189
# url_dict looks like this:
164190
# {
@@ -167,70 +193,4 @@ def majority_eth_config_test(exec_clients: List[str], rpc_endpoint: str): # noq
167193
# ...
168194
# }
169195

170-
# print("Majority test might contact some of these URLs:")
171-
# pprint(url_dict)
172-
173-
# responses dict maps exec-client name to its response
174-
responses = dict() # noqa: C408
175-
for exec_client in url_dict.keys():
176-
# try only as many consensus+exec client combinations until you receive a response
177-
# if all combinations fail we panic
178-
for url in url_dict[exec_client]:
179-
success, response = get_eth_config(url)
180-
if not success:
181-
# safely split url to not leak rpc_endpoint in logs
182-
print(
183-
f"When trying to get eth_config from {url.split('@', 1)[-1] if '@' in url else ''} the following problem occurred: {response}" # noqa: E501
184-
)
185-
continue
186-
187-
responses[exec_client] = response
188-
print(f"Response of {exec_client}: {response}\n\n")
189-
break # no need to gather more responses for this client
190-
191-
assert len(responses.keys()) == len(exec_clients), "Failed to get an eth_config response "
192-
f" from each specified execution client. Full list of execution clients is {exec_clients} "
193-
f"but we were only able to gather eth_config responses from: {responses.keys()}\nWill try "
194-
"again with a different consensus-execution client combination for this execution client"
195-
196-
# determine hashes of client responses
197-
client_to_hash_dict = dict() # noqa: C408
198-
for client in responses.keys():
199-
response_bytes = json.dumps(responses[client], sort_keys=True).encode("utf-8")
200-
response_hash = sha256(response_bytes).digest().hex()
201-
print(f"Response hash of client {client}: {response_hash}")
202-
client_to_hash_dict[client] = response_hash
203-
204-
# if not all responses have the same hash there is a critical consensus issue
205-
expected_hash = ""
206-
for h in client_to_hash_dict.keys():
207-
if expected_hash == "":
208-
expected_hash = client_to_hash_dict[h]
209-
continue
210-
211-
if client_to_hash_dict[h] != expected_hash:
212-
pytest.exit("Critical consensus issue: Not all eth_config responses are the same!")
213-
214-
print("All clients returned the same eth_config response. Test has been passed!")
215-
216-
217-
def get_eth_config(url: str) -> Tuple[bool, str]: # success, response
218-
"""Request data from devnet node via JSON_RPC."""
219-
payload = {
220-
"jsonrpc": "2.0",
221-
"method": "eth_config",
222-
"params": [],
223-
"id": 1,
224-
}
225-
226-
headers = {"Content-Type": "application/json"}
227-
228-
try:
229-
# Make the request
230-
response = requests.post(url, json=payload, headers=headers, timeout=20)
231-
232-
# Return JSON response
233-
return True, response.json()
234-
235-
except Exception as e:
236-
return False, f"error: {e}"
196+
return url_dict

src/pytest_plugins/execute/eth_config/execute_eth_config.py

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
"""Pytest test to verify a client's configuration using `eth_config` RPC endpoint."""
22

3+
import json
34
import time
5+
from hashlib import sha256
6+
from typing import Dict, List
47

58
import pytest
69

710
from ethereum_test_rpc import EthConfigResponse, EthRPC
811

12+
from .eth_config import get_eth_config, get_rpc_url_combinations_el_cl
913
from .types import NetworkConfig
1014

1115

@@ -16,9 +20,10 @@ def eth_config_response(eth_rpc: EthRPC) -> EthConfigResponse | None:
1620

1721

1822
@pytest.fixture(scope="session")
19-
def network(request: pytest.FixtureRequest) -> NetworkConfig:
23+
def network(request) -> NetworkConfig:
2024
"""Get the network that will be used to verify all tests."""
21-
return request.config.network # type: ignore
25+
config = request.config
26+
return config.getoption("network")
2227

2328

2429
@pytest.fixture(scope="session")
@@ -36,8 +41,13 @@ def expected_eth_config(network: NetworkConfig, current_time: int) -> EthConfigR
3641
def test_eth_config_current(
3742
eth_config_response: EthConfigResponse | None,
3843
expected_eth_config: EthConfigResponse,
44+
request,
3945
) -> None:
4046
"""Validate `current` field of the `eth_config` RPC endpoint."""
47+
config = request.config
48+
if config.getoption("network_config_file") is None:
49+
pytest.skip("Skipping test because no 'network_config_file' was specified")
50+
4151
assert eth_config_response is not None, "Client did not return a valid `eth_config` response."
4252
assert eth_config_response.current is not None, (
4353
"Client did not return a valid `current` fork config."
@@ -53,8 +63,13 @@ def test_eth_config_current(
5363
def test_eth_config_current_fork_id(
5464
eth_config_response: EthConfigResponse | None,
5565
expected_eth_config: EthConfigResponse,
66+
request,
5667
) -> None:
5768
"""Validate `forkId` field within the `current` configuration object."""
69+
config = request.config
70+
if config.getoption("network_config_file") is None:
71+
pytest.skip("Skipping test because no 'network_config_file' was specified")
72+
5873
assert eth_config_response is not None, "Client did not return a valid `eth_config` response."
5974
assert eth_config_response.current is not None, (
6075
"Client did not return a valid `current` fork config."
@@ -72,8 +87,15 @@ def test_eth_config_current_fork_id(
7287
def test_eth_config_next(
7388
eth_config_response: EthConfigResponse | None,
7489
expected_eth_config: EthConfigResponse,
90+
request,
7591
) -> None:
7692
"""Validate `next` field of the `eth_config` RPC endpoint."""
93+
config = request.config
94+
if config.getoption("network_config_file") is None:
95+
pytest.skip("Skipping test because no 'network_config_file' was specified")
96+
else:
97+
print("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
98+
7799
assert eth_config_response is not None, "Client did not return a valid `eth_config` response."
78100
expected_next = expected_eth_config.next
79101
if expected_next is None:
@@ -94,8 +116,13 @@ def test_eth_config_next(
94116
def test_eth_config_next_fork_id(
95117
eth_config_response: EthConfigResponse | None,
96118
expected_eth_config: EthConfigResponse,
119+
request,
97120
) -> None:
98121
"""Validate `forkId` field within the `next` configuration object."""
122+
config = request.config
123+
if config.getoption("network_config_file") is None:
124+
pytest.skip("Skipping test because no 'network_config_file' was specified")
125+
99126
assert eth_config_response is not None, "Client did not return a valid `eth_config` response."
100127
expected_next = expected_eth_config.next
101128
if expected_next is None:
@@ -124,8 +151,12 @@ def test_eth_config_next_fork_id(
124151
def test_eth_config_last(
125152
eth_config_response: EthConfigResponse | None,
126153
expected_eth_config: EthConfigResponse,
154+
config: pytest.Config,
127155
) -> None:
128156
"""Validate `last` field of the `eth_config` RPC endpoint."""
157+
if config.getoption("network_config_file") is None:
158+
pytest.skip("Skipping test because no 'network_config_file' was specified")
159+
129160
expected_last = expected_eth_config.last
130161
assert eth_config_response is not None, "Client did not return a valid `eth_config` response."
131162
if expected_last is None:
@@ -146,8 +177,12 @@ def test_eth_config_last(
146177
def test_eth_config_last_fork_id(
147178
eth_config_response: EthConfigResponse | None,
148179
expected_eth_config: EthConfigResponse,
180+
config: pytest.Config,
149181
) -> None:
150182
"""Validate `forkId` field within the `last` configuration object."""
183+
if config.getoption("network_config_file") is None:
184+
pytest.skip("Skipping test because no 'network_config_file' was specified")
185+
151186
assert eth_config_response is not None, "Client did not return a valid `eth_config` response."
152187
expected_last = expected_eth_config.last
153188
if expected_last is None:
@@ -171,3 +206,66 @@ def test_eth_config_last_fork_id(
171206
f"{received_fork_id} != "
172207
f"{expected_last_fork_id}"
173208
)
209+
210+
211+
def test_eth_config_majority(
212+
request,
213+
) -> None:
214+
"""Queries devnet exec clients for their eth_config and fails if not all have the same response.""" # noqa: E501
215+
# decide whether to run this test
216+
config = request.config
217+
run_this_test_bool = config.getoption(name="majority_eth_config_test_enabled")
218+
if not run_this_test_bool:
219+
pytest.skip("Skipping eth_config majority test")
220+
221+
# retrieve required values for running this test
222+
rpc_endpoint = config.getoption("rpc_endpoint")
223+
el_clients: List[str] = config.getoption("majority_clients") # besu, erigon, ..
224+
225+
url_dict: None | Dict[str, List[str]] = get_rpc_url_combinations_el_cl(
226+
el_clients=el_clients, rpc_endpoint=rpc_endpoint
227+
)
228+
assert url_dict is not None
229+
responses = dict() # noqa: C408
230+
for exec_client in url_dict.keys():
231+
# try only as many consensus+exec client combinations until you receive a response
232+
# if all combinations fail we panic
233+
for url in url_dict[exec_client]:
234+
success, response = get_eth_config(url)
235+
if not success:
236+
# safely split url to not leak rpc_endpoint in logs
237+
print(
238+
f"When trying to get eth_config from {url.split('@', 1)[-1] if '@' in url else ''} the following problem occurred: {response}" # noqa: E501
239+
)
240+
continue
241+
242+
responses[exec_client] = response
243+
print(f"Response of {exec_client}: {response}\n\n")
244+
break # no need to gather more responses for this client
245+
246+
assert len(responses.keys()) == len(el_clients), "Failed to get an eth_config response "
247+
f" from each specified execution client. Full list of execution clients is {el_clients} "
248+
f"but we were only able to gather eth_config responses from: {responses.keys()}\nWill try "
249+
"again with a different consensus-execution client combination for this execution client"
250+
251+
# determine hashes of client responses
252+
client_to_hash_dict = dict() # noqa: C408
253+
for client in responses.keys():
254+
response_bytes = json.dumps(responses[client], sort_keys=True).encode("utf-8")
255+
response_hash = sha256(response_bytes).digest().hex()
256+
print(f"Response hash of client {client}: {response_hash}")
257+
client_to_hash_dict[client] = response_hash
258+
259+
# if not all responses have the same hash there is a critical consensus issue
260+
expected_hash = ""
261+
for h in client_to_hash_dict.keys():
262+
if expected_hash == "":
263+
expected_hash = client_to_hash_dict[h]
264+
continue
265+
266+
assert client_to_hash_dict[h] == expected_hash, (
267+
"Critical consensus issue: Not all eth_config responses are the same!"
268+
)
269+
assert expected_hash != ""
270+
271+
print("All clients returned the same eth_config response. Test has been passed!")

0 commit comments

Comments
 (0)