Skip to content

Commit 07d5627

Browse files
Merge pull request #42 from runtimeverification/refactor/test-suite-dedup
refactor(tests): dedupe integration harness, drop dead scaffolding
2 parents 74c51a7 + 52913f5 commit 07d5627

6 files changed

Lines changed: 309 additions & 423 deletions

File tree

src/komet_node/hello.py

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/tests/integration/conftest.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""Shared fixtures and helpers for the integration tests.
2+
3+
The integration tests drive a live ``StellarRpcServer`` over HTTP. This module centralises
4+
everything they build on: the server fixture, the JSON-RPC / HTTP plumbing, wat compilation,
5+
the JSON-shape predicates used to assert the stellar-rpc wire format, and the transaction
6+
submit / deploy helpers. Keeping these in one place stops each feature's test file from
7+
carrying its own drifting copy.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import json
13+
import re
14+
import socket
15+
import subprocess
16+
import threading
17+
import time
18+
import urllib.request
19+
from typing import TYPE_CHECKING, Any, NamedTuple
20+
21+
import pytest
22+
from stellar_sdk import Account, Keypair, Network, TransactionBuilder
23+
from stellar_sdk.utils import sha256
24+
25+
from komet_node.server import StellarRpcServer
26+
27+
if TYPE_CHECKING:
28+
from collections.abc import Callable, Iterator
29+
from pathlib import Path
30+
31+
from stellar_sdk import xdr
32+
33+
PASSPHRASE = Network.TESTNET_NETWORK_PASSPHRASE
34+
35+
36+
# ---------------------------------------------------------------------------
37+
# HTTP / JSON-RPC plumbing
38+
# ---------------------------------------------------------------------------
39+
40+
41+
def wat_to_wasm(wat_path: Path) -> bytes:
42+
proc_res = subprocess.run(['wat2wasm', str(wat_path), '--output=/dev/stdout'], check=True, capture_output=True)
43+
return proc_res.stdout
44+
45+
46+
def _find_free_port() -> int:
47+
with socket.socket() as s:
48+
s.bind(('', 0))
49+
return s.getsockname()[1]
50+
51+
52+
def _wait_for_server(host: str, port: int, timeout: float = 10.0) -> None:
53+
deadline = time.time() + timeout
54+
while time.time() < deadline:
55+
try:
56+
with socket.create_connection((host, port), timeout=0.1):
57+
return
58+
except OSError:
59+
time.sleep(0.05)
60+
raise TimeoutError(f'Server did not start on {host}:{port}')
61+
62+
63+
def _rpc(port: int, method: str, params: dict[str, Any]) -> dict[str, Any]:
64+
body = json.dumps({'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params}).encode()
65+
return _post(port, body)
66+
67+
68+
def _post(port: int, body: bytes) -> Any:
69+
return json.loads(_post_raw(port, body))
70+
71+
72+
def _post_raw(port: int, body: bytes) -> bytes:
73+
req = urllib.request.Request(
74+
f'http://localhost:{port}',
75+
data=body,
76+
headers={'Content-Type': 'application/json'},
77+
)
78+
with urllib.request.urlopen(req) as resp:
79+
return resp.read()
80+
81+
82+
# ---------------------------------------------------------------------------
83+
# Spec-shape predicates
84+
#
85+
# The official serialization rules come from the Go structs in stellar/go-stellar-sdk
86+
# protocols/rpc (what real stellar-rpc emits): ledger sequence numbers and protocolVersion
87+
# are JSON numbers; the close-time fields on the singular methods are int64 with Go's
88+
# `,string` encoding, i.e. JSON strings holding a decimal integer; hashes are 64 lowercase
89+
# hex characters.
90+
# ---------------------------------------------------------------------------
91+
92+
_HEX64_RE = re.compile(r'[0-9a-f]{64}')
93+
_INT_STRING_RE = re.compile(r'-?[0-9]+')
94+
95+
96+
def _is_number(value: Any) -> bool:
97+
"""True for a JSON number decoded to int (bool is a distinct JSON type)."""
98+
return isinstance(value, int) and not isinstance(value, bool)
99+
100+
101+
def _is_int_string(value: Any) -> bool:
102+
"""True for a JSON string holding a decimal integer (Go int64 `,string` encoding)."""
103+
return isinstance(value, str) and _INT_STRING_RE.fullmatch(value) is not None
104+
105+
106+
def _is_hex64(value: Any) -> bool:
107+
return isinstance(value, str) and _HEX64_RE.fullmatch(value) is not None
108+
109+
110+
# ---------------------------------------------------------------------------
111+
# Server fixture
112+
# ---------------------------------------------------------------------------
113+
114+
115+
@pytest.fixture
116+
def server(tmp_path: Path) -> Iterator[StellarRpcServer]:
117+
port = _find_free_port()
118+
srv = StellarRpcServer(
119+
host='localhost',
120+
port=port,
121+
io_dir=tmp_path,
122+
network_passphrase=PASSPHRASE,
123+
)
124+
thread = threading.Thread(target=srv.serve, daemon=True)
125+
thread.start()
126+
_wait_for_server('localhost', port)
127+
yield srv
128+
srv.shutdown()
129+
130+
131+
# ---------------------------------------------------------------------------
132+
# Transaction submit / deploy helpers
133+
#
134+
# Every lifecycle test builds on the same primitives: submit a signed transaction and
135+
# confirm it reaches SUCCESS, fund a fresh account, deploy a contract from a .wat file, and
136+
# invoke functions on it. They compose — ``deploy_and_get_invoker`` is just the three-step
137+
# account -> upload -> deploy setup wrapped around ``make_invoker``.
138+
# ---------------------------------------------------------------------------
139+
140+
141+
class Deployed(NamedTuple):
142+
"""A deployed contract instance: its C-strkey address, wasm hash, and wasm bytecode."""
143+
144+
address: str
145+
wasm_hash: bytes
146+
wasm_bytecode: bytes
147+
148+
149+
def send_tx(server: StellarRpcServer, keypair: Keypair, tb: TransactionBuilder) -> tuple[str, dict[str, Any]]:
150+
"""Sign, submit, and confirm a transaction.
151+
152+
Asserts the submission is accepted (PENDING) and the transaction executes to SUCCESS.
153+
Returns ``(tx_hash, getTransaction_result)``.
154+
"""
155+
env = tb.set_timeout(30).build()
156+
env.sign(keypair)
157+
res = _rpc(server.port(), 'sendTransaction', {'transaction': env.to_xdr()})
158+
assert res['result']['status'] == 'PENDING'
159+
tx_hash = res['result']['hash']
160+
get_res = _rpc(server.port(), 'getTransaction', {'hash': tx_hash})['result']
161+
assert get_res['status'] == 'SUCCESS', f'Transaction failed: {get_res}'
162+
return tx_hash, get_res
163+
164+
165+
def fund_account(server: StellarRpcServer) -> tuple[Keypair, Account]:
166+
"""Create a fresh, funded account; return its keypair and sequence-tracking ``Account``."""
167+
keypair = Keypair.random()
168+
account = Account(keypair.public_key, sequence=0)
169+
tb = TransactionBuilder(account, PASSPHRASE).append_create_account_op(keypair.public_key, '1000')
170+
send_tx(server, keypair, tb)
171+
return keypair, account
172+
173+
174+
def deploy_contract(server: StellarRpcServer, keypair: Keypair, account: Account, wat_path: Path) -> Deployed:
175+
"""Upload ``wat_path`` and deploy an instance from it, signed by ``keypair``/``account``.
176+
177+
The account must already exist. Submits two transactions (upload + create) and returns
178+
the deployed contract's address, wasm hash, and wasm bytecode.
179+
"""
180+
181+
def builder() -> TransactionBuilder:
182+
return TransactionBuilder(account, PASSPHRASE)
183+
184+
wasm_bytecode = wat_to_wasm(wat_path)
185+
send_tx(server, keypair, builder().append_upload_contract_wasm_op(wasm_bytecode))
186+
wasm_hash = sha256(wasm_bytecode)
187+
salt = b'\x00' * 32
188+
send_tx(server, keypair, builder().append_create_contract_op(wasm_hash, keypair.public_key, None, salt))
189+
address = server.encoder.contract_address_from_deployer_address(keypair.public_key, salt)
190+
return Deployed(address=address, wasm_hash=wasm_hash, wasm_bytecode=wasm_bytecode)
191+
192+
193+
def make_invoker(
194+
server: StellarRpcServer, keypair: Keypair, account: Account, contract_address: str
195+
) -> Callable[..., str]:
196+
"""Return an ``invoke(func, args=None) -> tx_hash`` callable that asserts each call SUCCEEDs."""
197+
198+
def invoke(func: str, args: list[xdr.SCVal] | None = None) -> str:
199+
tb = TransactionBuilder(account, PASSPHRASE).append_invoke_contract_function_op(
200+
contract_address, func, args or []
201+
)
202+
tx_hash, _ = send_tx(server, keypair, tb)
203+
return tx_hash
204+
205+
return invoke
206+
207+
208+
def deploy_and_get_invoker(server: StellarRpcServer, wat_path: Path) -> Callable[..., str]:
209+
"""Fund an account, upload ``wat_path``, and deploy a contract instance from it.
210+
211+
Returns an ``invoke(func, args=None)`` callable that runs a contract function and returns
212+
the executed transaction's hash, asserting the whole setup and each call reaches SUCCESS.
213+
"""
214+
keypair, account = fund_account(server)
215+
deployed = deploy_contract(server, keypair, account, wat_path)
216+
return make_invoker(server, keypair, account, deployed.address)

src/tests/integration/test_get_events.py

Lines changed: 16 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,20 @@
1313

1414
from __future__ import annotations
1515

16-
import json
1716
import re
18-
import socket
19-
import subprocess
20-
import threading
21-
import time
22-
import urllib.request
2317
from datetime import datetime
2418
from pathlib import Path
2519
from typing import TYPE_CHECKING, Any
2620

27-
import pytest
28-
from stellar_sdk import Account, Keypair, Network, StrKey, TransactionBuilder, xdr
29-
from stellar_sdk.utils import sha256
21+
from stellar_sdk import StrKey, TransactionBuilder, xdr
3022
from stellar_sdk.xdr.sc_val_type import SCValType
3123

32-
from komet_node.server import StellarRpcServer
24+
from .conftest import PASSPHRASE, _is_number, _rpc, deploy_contract, fund_account, send_tx
3325

3426
if TYPE_CHECKING:
35-
from collections.abc import Iterator
27+
from stellar_sdk import Account, Keypair
28+
29+
from komet_node.server import StellarRpcServer
3630

3731
EVENTS_CONTRACT_WAT = (Path(__file__).parent / 'data' / 'wasm' / 'events.wat').resolve(strict=True)
3832

@@ -47,105 +41,26 @@
4741

4842

4943
# ---------------------------------------------------------------------------
50-
# Helpers (mirrored from test_server.py so this module stays self-contained)
44+
# Helpers (the server fixture, RPC plumbing, and deploy helpers live in conftest.py)
5145
# ---------------------------------------------------------------------------
5246

5347

54-
def wat_to_wasm(wat_path: Path) -> bytes:
55-
proc_res = subprocess.run(['wat2wasm', str(wat_path), '--output=/dev/stdout'], check=True, capture_output=True)
56-
return proc_res.stdout
57-
58-
59-
def _find_free_port() -> int:
60-
with socket.socket() as s:
61-
s.bind(('', 0))
62-
return s.getsockname()[1]
63-
64-
65-
def _wait_for_server(host: str, port: int, timeout: float = 10.0) -> None:
66-
deadline = time.time() + timeout
67-
while time.time() < deadline:
68-
try:
69-
with socket.create_connection((host, port), timeout=0.1):
70-
return
71-
except OSError:
72-
time.sleep(0.05)
73-
raise TimeoutError(f'Server did not start on {host}:{port}')
74-
75-
76-
def _rpc(port: int, method: str, params: dict[str, Any]) -> dict[str, Any]:
77-
body = json.dumps({'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params}).encode()
78-
req = urllib.request.Request(
79-
f'http://localhost:{port}',
80-
data=body,
81-
headers={'Content-Type': 'application/json'},
82-
)
83-
with urllib.request.urlopen(req) as resp:
84-
return json.loads(resp.read())
85-
86-
87-
@pytest.fixture
88-
def server(tmp_path: Path) -> Iterator[StellarRpcServer]:
89-
port = _find_free_port()
90-
srv = StellarRpcServer(
91-
host='localhost',
92-
port=port,
93-
io_dir=tmp_path,
94-
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE,
95-
)
96-
thread = threading.Thread(target=srv.serve, daemon=True)
97-
thread.start()
98-
_wait_for_server('localhost', port)
99-
yield srv
100-
srv.shutdown()
101-
102-
103-
def _send(server: StellarRpcServer, keypair: Keypair, tb: TransactionBuilder) -> str:
104-
"""Sign, submit, and confirm a transaction; return its hash."""
105-
env = tb.set_timeout(30).build()
106-
env.sign(keypair)
107-
res = _rpc(server.port(), 'sendTransaction', {'transaction': env.to_xdr()})
108-
assert res['result']['status'] == 'PENDING'
109-
tx_hash = res['result']['hash']
110-
get_res = _rpc(server.port(), 'getTransaction', {'hash': tx_hash})['result']
111-
assert get_res['status'] == 'SUCCESS', f'Transaction failed: {get_res}'
112-
return tx_hash
113-
114-
11548
def _deploy_events_contract(server: StellarRpcServer) -> tuple[Keypair, Account, str]:
11649
"""Create an account, upload events.wat, and deploy it (ledgers 1-3).
11750
11851
Returns the funding keypair, its (mutated) sequence-tracking account, and the C-strkey
11952
address of the deployed contract.
12053
"""
121-
keypair = Keypair.random()
122-
account = Account(keypair.public_key, sequence=0)
123-
124-
def builder() -> TransactionBuilder:
125-
return TransactionBuilder(account, Network.TESTNET_NETWORK_PASSPHRASE)
126-
127-
_send(server, keypair, builder().append_create_account_op(keypair.public_key, '1000'))
128-
129-
wasm_bytecode = wat_to_wasm(EVENTS_CONTRACT_WAT)
130-
_send(server, keypair, builder().append_upload_contract_wasm_op(wasm_bytecode))
131-
132-
wasm_hash = sha256(wasm_bytecode)
133-
salt = b'\x00' * 32
134-
_send(server, keypair, builder().append_create_contract_op(wasm_hash, keypair.public_key, None, salt))
135-
136-
contract_address = server.encoder.contract_address_from_deployer_address(keypair.public_key, salt)
137-
return keypair, account, contract_address
54+
keypair, account = fund_account(server)
55+
deployed = deploy_contract(server, keypair, account, EVENTS_CONTRACT_WAT)
56+
return keypair, account, deployed.address
13857

13958

14059
def _emit_event(server: StellarRpcServer, keypair: Keypair, account: Account, contract_address: str) -> str:
14160
"""Invoke the contract's `emit` function; return the transaction hash."""
142-
tb = TransactionBuilder(account, Network.TESTNET_NETWORK_PASSPHRASE)
143-
return _send(server, keypair, tb.append_invoke_contract_function_op(contract_address, 'emit', []))
144-
145-
146-
def _is_json_number(value: Any) -> bool:
147-
"""True for a JSON number decoded to int (bool is an int subclass, so exclude it)."""
148-
return isinstance(value, int) and not isinstance(value, bool)
61+
tb = TransactionBuilder(account, PASSPHRASE).append_invoke_contract_function_op(contract_address, 'emit', [])
62+
tx_hash, _ = send_tx(server, keypair, tb)
63+
return tx_hash
14964

15065

15166
def _assert_request_error(response: dict[str, Any]) -> None:
@@ -218,14 +133,11 @@ def test_get_events_rejects_invalid_xdr_format(server: StellarRpcServer) -> None
218133

219134
def test_get_events_no_events_returns_empty_array(server: StellarRpcServer) -> None:
220135
"""A range that contains no contract events yields an empty events array, not null."""
221-
keypair = Keypair.random()
222-
account = Account(keypair.public_key, sequence=0)
223-
tb = TransactionBuilder(account, Network.TESTNET_NETWORK_PASSPHRASE)
224-
_send(server, keypair, tb.append_create_account_op(keypair.public_key, '1000'))
136+
fund_account(server)
225137

226138
result = _rpc(server.port(), 'getEvents', {'startLedger': 1})['result']
227139
assert result['events'] == []
228-
assert _is_json_number(result['latestLedger'])
140+
assert _is_number(result['latestLedger'])
229141
assert result['latestLedger'] == 1
230142
# Real stellar-rpc always populates the cursor (the position to resume scanning from).
231143
assert isinstance(result['cursor'], str)
@@ -237,7 +149,7 @@ def test_get_events_returns_emitted_contract_event(server: StellarRpcServer) ->
237149
tx_hash = _emit_event(server, keypair, account, contract_address) # ledger 4
238150

239151
result = _rpc(server.port(), 'getEvents', {'startLedger': 1})['result']
240-
assert _is_json_number(result['latestLedger'])
152+
assert _is_number(result['latestLedger'])
241153
assert result['latestLedger'] == 4
242154
assert isinstance(result['cursor'], str)
243155

@@ -246,7 +158,7 @@ def test_get_events_returns_emitted_contract_event(server: StellarRpcServer) ->
246158
event = result['events'][0]
247159

248160
assert event['type'] == 'contract'
249-
assert _is_json_number(event['ledger'])
161+
assert _is_number(event['ledger'])
250162
assert event['ledger'] == 4
251163
# ledgerClosedAt is an ISO-8601 timestamp string.
252164
assert isinstance(event['ledgerClosedAt'], str)

0 commit comments

Comments
 (0)