Skip to content

Commit 8d2ee88

Browse files
sipaismaelsadeeqryanofskyseditedSjors
committed
tests: add functional tests for IPC interface
Co-Authored-By: ismaelsadeeq <ask4ismailsadiq@gmail.com> Co-Authored-By: ryanofsky <ryan@ofsky.org> Co-Authored-By: TheCharlatan <seb.kung@gmail.com> Co-Authored-By: Sjors Provoost <sjors@sprovoost.nl>
1 parent 3cc9a06 commit 8d2ee88

File tree

4 files changed

+212
-0
lines changed

4 files changed

+212
-0
lines changed

test/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ The ZMQ functional test requires a python ZMQ library. To install it:
3434
- on Unix, run `sudo apt-get install python3-zmq`
3535
- on mac OS, run `pip3 install pyzmq`
3636

37+
The IPC functional test requires a python IPC library. `pip3 install pycapnp` may work, but if not, install it from source:
38+
39+
```sh
40+
git clone -b v2.1.0 https://github.com/capnproto/pycapnp
41+
pip3 install ./pycapnp
42+
```
43+
44+
If that does not work, try adding `-C force-bundled-libcapnp=True` to the `pip` command.
45+
Depending on the system, it may be necessary to install and run in a venv:
46+
47+
```sh
48+
python -m venv venv
49+
git clone -b v2.1.0 https://github.com/capnproto/pycapnp
50+
venv/bin/pip3 install ./pycapnp -C force-bundled-libcapnp=True
51+
venv/bin/python3 build/test/functional/interface_ipc.py
52+
```
3753

3854
On Windows the `PYTHONUTF8` environment variable must be set to 1:
3955

test/functional/interface_ipc.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test the IPC (multiprocess) interface."""
6+
import asyncio
7+
from io import BytesIO
8+
from pathlib import Path
9+
import shutil
10+
from test_framework.messages import (CBlock, CTransaction, ser_uint256)
11+
from test_framework.test_framework import (BitcoinTestFramework, assert_equal)
12+
from test_framework.wallet import MiniWallet
13+
14+
# Test may be skipped and not have capnp installed
15+
try:
16+
import capnp # type: ignore[import] # noqa: F401
17+
except ImportError:
18+
pass
19+
20+
21+
class IPCInterfaceTest(BitcoinTestFramework):
22+
23+
def skip_test_if_missing_module(self):
24+
self.skip_if_no_ipc()
25+
self.skip_if_no_py_capnp()
26+
27+
def load_capnp_modules(self):
28+
if capnp_bin := shutil.which("capnp"):
29+
# Add the system cap'nproto path so include/capnp/c++.capnp can be found.
30+
capnp_dir = Path(capnp_bin).parent.parent / "include"
31+
else:
32+
# If there is no system cap'nproto, the pycapnp module should have its own "bundled"
33+
# includes at this location. If pycapnp was installed with bundled capnp,
34+
# capnp/c++.capnp can be found here.
35+
capnp_dir = Path(capnp.__path__[0]).parent
36+
src_dir = Path(self.config['environment']['SRCDIR']) / "src"
37+
mp_dir = src_dir / "ipc" / "libmultiprocess" / "include"
38+
imports = [str(capnp_dir), str(src_dir), str(mp_dir)]
39+
return {
40+
"proxy": capnp.load(str(mp_dir / "mp" / "proxy.capnp"), imports=imports),
41+
"init": capnp.load(str(src_dir / "ipc" / "capnp" / "init.capnp"), imports=imports),
42+
"echo": capnp.load(str(src_dir / "ipc" / "capnp" / "echo.capnp"), imports=imports),
43+
"mining": capnp.load(str(src_dir / "ipc" / "capnp" / "mining.capnp"), imports=imports),
44+
}
45+
46+
def set_test_params(self):
47+
self.num_nodes = 1
48+
49+
def setup_nodes(self):
50+
self.extra_init = [{"ipcbind": True}]
51+
super().setup_nodes()
52+
# Use this function to also load the capnp modules (we cannot use set_test_params for this,
53+
# as it is being called before knowing whether capnp is available).
54+
self.capnp_modules = self.load_capnp_modules()
55+
56+
async def make_capnp_init_ctx(self):
57+
node = self.nodes[0]
58+
# Establish a connection, and create Init proxy object.
59+
connection = await capnp.AsyncIoStream.create_unix_connection(node.ipc_socket_path)
60+
client = capnp.TwoPartyClient(connection)
61+
init = client.bootstrap().cast_as(self.capnp_modules['init'].Init)
62+
# Create a remote thread on the server for the IPC calls to be executed in.
63+
threadmap = init.construct().threadMap
64+
thread = threadmap.makeThread("pythread").result
65+
ctx = self.capnp_modules['proxy'].Context()
66+
ctx.thread = thread
67+
# Return both.
68+
return ctx, init
69+
70+
async def parse_and_deserialize_block(self, block_template, ctx):
71+
block_data = BytesIO((await block_template.result.getBlock(ctx)).result)
72+
block = CBlock()
73+
block.deserialize(block_data)
74+
return block
75+
76+
def run_echo_test(self):
77+
self.log.info("Running echo test")
78+
async def async_routine():
79+
ctx, init = await self.make_capnp_init_ctx()
80+
self.log.debug("Create Echo proxy object")
81+
echo = init.makeEcho(ctx).result
82+
self.log.debug("Test a few invocations of echo")
83+
for s in ["hallo", "", "haha"]:
84+
result_eval = (await echo.echo(ctx, s)).result
85+
assert_equal(s, result_eval)
86+
self.log.debug("Destroy the Echo object")
87+
echo.destroy(ctx)
88+
asyncio.run(capnp.run(async_routine()))
89+
90+
def run_mining_test(self):
91+
self.log.info("Running mining test")
92+
block_hash_size = 32
93+
block_header_size = 80
94+
timeout = 1000.0 # 1000 milliseconds
95+
miniwallet = MiniWallet(self.nodes[0])
96+
97+
async def async_routine():
98+
ctx, init = await self.make_capnp_init_ctx()
99+
self.log.debug("Create Mining proxy object")
100+
mining = init.makeMining(ctx)
101+
self.log.debug("Test simple inspectors")
102+
assert (await mining.result.isTestChain(ctx))
103+
assert (await mining.result.isInitialBlockDownload(ctx))
104+
blockref = await mining.result.getTip(ctx)
105+
assert blockref.hasResult
106+
assert_equal(len(blockref.result.hash), block_hash_size)
107+
current_block_height = self.nodes[0].getchaintips()[0]["height"]
108+
assert blockref.result.height == current_block_height
109+
self.log.debug("Mine a block")
110+
wait = mining.result.waitTipChanged(ctx, blockref.result.hash, )
111+
self.generate(self.nodes[0], 1)
112+
newblockref = await wait
113+
assert_equal(len(newblockref.result.hash), block_hash_size)
114+
assert_equal(newblockref.result.height, current_block_height + 1)
115+
self.log.debug("Wait for timeout")
116+
wait = mining.result.waitTipChanged(ctx, newblockref.result.hash, timeout)
117+
oldblockref = await wait
118+
assert_equal(len(newblockref.result.hash), block_hash_size)
119+
assert_equal(oldblockref.result.hash, newblockref.result.hash)
120+
assert_equal(oldblockref.result.height, newblockref.result.height)
121+
122+
self.log.debug("Create a template")
123+
opts = self.capnp_modules['mining'].BlockCreateOptions()
124+
opts.useMempool = True
125+
opts.blockReservedWeight = 4000
126+
opts.coinbaseOutputMaxAdditionalSigops = 0
127+
template = mining.result.createNewBlock(opts)
128+
self.log.debug("Test some inspectors of Template")
129+
header = await template.result.getBlockHeader(ctx)
130+
assert_equal(len(header.result), block_header_size)
131+
block = await self.parse_and_deserialize_block(template, ctx)
132+
assert_equal(ser_uint256(block.hashPrevBlock), newblockref.result.hash)
133+
assert len(block.vtx) >= 1
134+
txfees = await template.result.getTxFees(ctx)
135+
assert_equal(len(txfees.result), 0)
136+
txsigops = await template.result.getTxSigops(ctx)
137+
assert_equal(len(txsigops.result), 0)
138+
coinbase_data = BytesIO((await template.result.getCoinbaseTx(ctx)).result)
139+
coinbase = CTransaction()
140+
coinbase.deserialize(coinbase_data)
141+
assert_equal(coinbase.vin[0].prevout.hash, 0)
142+
self.log.debug("Wait for a new template")
143+
waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
144+
waitoptions.timeout = timeout
145+
waitnext = template.result.waitNext(ctx, waitoptions)
146+
self.generate(self.nodes[0], 1)
147+
template2 = await waitnext
148+
block2 = await self.parse_and_deserialize_block(template2, ctx)
149+
assert_equal(len(block2.vtx), 1)
150+
self.log.debug("Wait for another, but time out")
151+
template3 = await template2.result.waitNext(ctx, waitoptions)
152+
assert_equal(template3.to_dict(), {})
153+
self.log.debug("Wait for another, get one after increase in fees in the mempool")
154+
waitnext = template2.result.waitNext(ctx, waitoptions)
155+
miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
156+
template4 = await waitnext
157+
block3 = await self.parse_and_deserialize_block(template4, ctx)
158+
assert_equal(len(block3.vtx), 2)
159+
self.log.debug("Wait again, this should return the same template, since the fee threshold is zero")
160+
template5 = await template4.result.waitNext(ctx, waitoptions)
161+
block4 = await self.parse_and_deserialize_block(template5, ctx)
162+
assert_equal(len(block4.vtx), 2)
163+
waitoptions.feeThreshold = 1
164+
self.log.debug("Wait for another, get one after increase in fees in the mempool")
165+
waitnext = template5.result.waitNext(ctx, waitoptions)
166+
miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
167+
template6 = await waitnext
168+
block4 = await self.parse_and_deserialize_block(template6, ctx)
169+
assert_equal(len(block4.vtx), 3)
170+
self.log.debug("Wait for another, but time out, since the fee threshold is set now")
171+
template7 = await template6.result.waitNext(ctx, waitoptions)
172+
assert_equal(template7.to_dict(), {})
173+
self.log.debug("Destroy template objects")
174+
template.result.destroy(ctx)
175+
template2.result.destroy(ctx)
176+
template3.result.destroy(ctx)
177+
template4.result.destroy(ctx)
178+
template5.result.destroy(ctx)
179+
template6.result.destroy(ctx)
180+
template7.result.destroy(ctx)
181+
asyncio.run(capnp.run(async_routine()))
182+
183+
def run_test(self):
184+
self.run_echo_test()
185+
self.run_mining_test()
186+
187+
if __name__ == '__main__':
188+
IPCInterfaceTest(__file__).main()

test/functional/test_framework/test_framework.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,13 @@ def skip_if_no_py_sqlite3(self):
964964
except ImportError:
965965
raise SkipTest("sqlite3 module not available.")
966966

967+
def skip_if_no_py_capnp(self):
968+
"""Attempt to import the capnp package and skip the test if the import fails."""
969+
try:
970+
import capnp # type: ignore[import] # noqa: F401
971+
except ImportError:
972+
raise SkipTest("capnp module not available.")
973+
967974
def skip_if_no_python_bcc(self):
968975
"""Attempt to import the bcc package and skip the tests if the import fails."""
969976
try:

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@
339339
'rpc_scantxoutset.py',
340340
'feature_unsupported_utxo_db.py',
341341
'feature_logging.py',
342+
'interface_ipc.py',
342343
'feature_anchors.py',
343344
'mempool_datacarrier.py',
344345
'feature_coinstatsindex.py',

0 commit comments

Comments
 (0)