Skip to content

Commit 85aec87

Browse files
author
MarcoFalke
committed
Merge #10711: [tests] Introduce TestNode
7897338 [tests] Introduce TestNode (John Newbery) Pull request description: Continues #10082 TestNode is a class responsible for all state related to a bitcoind node under test. It stores local state, is responsible for tracking the bitcoind process and delegates unrecognised messages to the RPC connection. This commit changes start_nodes and stop_nodes to start and stop the bitcoind nodes in parallel, making test setup and teardown much faster. On my vm, this changeset reduces total test_runner runtime for the base set of tests (including building the cache) from 263s to 195s (a 25% speedup). Note that the time reported by test_runner does not include time spent building the cache: *with TestNode*: ``` → date +"%T" ; ./test_runner.py -q ; date +"%T" 12:48:04 .................................................................................................................................................................................................................................................................................................................................. TEST | STATUS | DURATION abandonconflict.py | ✓ Passed | 12 s bip68-112-113-p2p.py | ✓ Passed | 19 s blockchain.py | ✓ Passed | 8 s bumpfee.py | ✓ Passed | 13 s decodescript.py | ✓ Passed | 3 s disablewallet.py | ✓ Passed | 3 s disconnect_ban.py | ✓ Passed | 6 s fundrawtransaction.py | ✓ Passed | 37 s getchaintips.py | ✓ Passed | 4 s httpbasics.py | ✓ Passed | 3 s import-rescan.py | ✓ Passed | 4 s importmulti.py | ✓ Passed | 6 s importprunedfunds.py | ✓ Passed | 3 s invalidblockrequest.py | ✓ Passed | 4 s invalidtxrequest.py | ✓ Passed | 4 s keypool.py | ✓ Passed | 7 s listsinceblock.py | ✓ Passed | 4 s listtransactions.py | ✓ Passed | 33 s mempool_limit.py | ✓ Passed | 4 s mempool_persist.py | ✓ Passed | 15 s mempool_reorg.py | ✓ Passed | 4 s mempool_resurrect_test.py | ✓ Passed | 3 s mempool_spendcoinbase.py | ✓ Passed | 3 s merkle_blocks.py | ✓ Passed | 3 s multi_rpc.py | ✓ Passed | 4 s net.py | ✓ Passed | 3 s nulldummy.py | ✓ Passed | 3 s p2p-compactblocks.py | ✓ Passed | 28 s p2p-fullblocktest.py | ✓ Passed | 126 s p2p-leaktests.py | ✓ Passed | 8 s p2p-mempool.py | ✓ Passed | 3 s p2p-segwit.py | ✓ Passed | 59 s p2p-versionbits-warning.py | ✓ Passed | 8 s preciousblock.py | ✓ Passed | 3 s prioritise_transaction.py | ✓ Passed | 5 s proxy_test.py | ✓ Passed | 3 s rawtransactions.py | ✓ Passed | 9 s receivedby.py | ✓ Passed | 19 s reindex.py | ✓ Passed | 12 s rest.py | ✓ Passed | 9 s rpcnamedargs.py | ✓ Passed | 3 s segwit.py | ✓ Passed | 7 s sendheaders.py | ✓ Passed | 24 s signmessages.py | ✓ Passed | 3 s signrawtransactions.py | ✓ Passed | 3 s txn_clone.py | ✓ Passed | 4 s txn_doublespend.py --mineblock | ✓ Passed | 4 s uptime.py | ✓ Passed | 3 s wallet-accounts.py | ✓ Passed | 3 s wallet-dump.py | ✓ Passed | 7 s wallet-encryption.py | ✓ Passed | 8 s wallet-hd.py | ✓ Passed | 15 s wallet.py | ✓ Passed | 31 s walletbackup.py | ✓ Passed | 104 s zapwallettxes.py | ✓ Passed | 9 s zmq_test.py | ○ Skipped | 0 s ALL | ✓ Passed | 735 s (accumulated) Runtime: 189 s 12:51:19 ``` *master*: ``` → date +"%T" ; ./test_runner.py -q ; date +"%T" 12:40:13 .......................................................................................................................................................................................................................................................................................................................................................................................................................................... TEST | STATUS | DURATION abandonconflict.py | ✓ Passed | 15 s bip68-112-113-p2p.py | ✓ Passed | 19 s blockchain.py | ✓ Passed | 8 s bumpfee.py | ✓ Passed | 20 s decodescript.py | ✓ Passed | 3 s disablewallet.py | ✓ Passed | 3 s disconnect_ban.py | ✓ Passed | 8 s fundrawtransaction.py | ✓ Passed | 36 s getchaintips.py | ✓ Passed | 11 s httpbasics.py | ✓ Passed | 7 s import-rescan.py | ✓ Passed | 16 s importmulti.py | ✓ Passed | 10 s importprunedfunds.py | ✓ Passed | 5 s invalidblockrequest.py | ✓ Passed | 4 s invalidtxrequest.py | ✓ Passed | 3 s keypool.py | ✓ Passed | 7 s listsinceblock.py | ✓ Passed | 11 s listtransactions.py | ✓ Passed | 37 s mempool_limit.py | ✓ Passed | 4 s mempool_persist.py | ✓ Passed | 23 s mempool_reorg.py | ✓ Passed | 7 s mempool_resurrect_test.py | ✓ Passed | 3 s mempool_spendcoinbase.py | ✓ Passed | 3 s merkle_blocks.py | ✓ Passed | 10 s multi_rpc.py | ✓ Passed | 6 s net.py | ✓ Passed | 6 s nulldummy.py | ✓ Passed | 3 s p2p-compactblocks.py | ✓ Passed | 30 s p2p-fullblocktest.py | ✓ Passed | 126 s p2p-leaktests.py | ✓ Passed | 8 s p2p-mempool.py | ✓ Passed | 3 s p2p-segwit.py | ✓ Passed | 62 s p2p-versionbits-warning.py | ✓ Passed | 8 s preciousblock.py | ✓ Passed | 8 s prioritise_transaction.py | ✓ Passed | 7 s proxy_test.py | ✓ Passed | 10 s rawtransactions.py | ✓ Passed | 15 s receivedby.py | ✓ Passed | 28 s reindex.py | ✓ Passed | 12 s rest.py | ✓ Passed | 12 s rpcnamedargs.py | ✓ Passed | 3 s segwit.py | ✓ Passed | 12 s sendheaders.py | ✓ Passed | 26 s signmessages.py | ✓ Passed | 3 s signrawtransactions.py | ✓ Passed | 3 s txn_clone.py | ✓ Passed | 10 s txn_doublespend.py --mineblock | ✓ Passed | 10 s uptime.py | ✓ Passed | 3 s wallet-accounts.py | ✓ Passed | 3 s wallet-dump.py | ✓ Passed | 6 s wallet-encryption.py | ✓ Passed | 8 s wallet-hd.py | ✓ Passed | 18 s wallet.py | ✓ Passed | 69 s walletbackup.py | ✓ Passed | 130 s zapwallettxes.py | ✓ Passed | 15 s zmq_test.py | ○ Skipped | 0 s ALL | ✓ Passed | 936 s (accumulated) Runtime: 242 s 12:44:36 ``` Tree-SHA512: 6dfc4c11fd0caf7de6954c93679cf22c3df0acc6f432e616d1151062a61f456faa8ae2fe670b427868af55bb564802df84c8fd76e90b4b338750dbc23f46ad88
2 parents ae47724 + 7897338 commit 85aec87

File tree

12 files changed

+193
-97
lines changed

12 files changed

+193
-97
lines changed

test/functional/blockchain.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,13 @@ def _test_stopatheight(self):
139139
self.nodes[0].generate(6)
140140
assert_equal(self.nodes[0].getblockcount(), 206)
141141
self.log.debug('Node should not stop at this height')
142-
assert_raises(subprocess.TimeoutExpired, lambda: self.bitcoind_processes[0].wait(timeout=3))
142+
assert_raises(subprocess.TimeoutExpired, lambda: self.nodes[0].process.wait(timeout=3))
143143
try:
144144
self.nodes[0].generate(1)
145145
except (ConnectionError, http.client.BadStatusLine):
146146
pass # The node already shut down before response
147147
self.log.debug('Node should stop at this height...')
148-
self.bitcoind_processes[0].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT)
148+
self.nodes[0].process.wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT)
149149
self.nodes[0] = self.start_node(0, self.options.tmpdir)
150150
assert_equal(self.nodes[0].getblockcount(), 207)
151151

test/functional/bumpfee.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ def setup_network(self, split=False):
4141
self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir, extra_args)
4242

4343
# Encrypt wallet for test_locked_wallet_fails test
44-
self.nodes[1].encryptwallet(WALLET_PASSPHRASE)
45-
self.bitcoind_processes[1].wait()
44+
self.nodes[1].node_encrypt_wallet(WALLET_PASSPHRASE)
4645
self.nodes[1] = self.start_node(1, self.options.tmpdir, extra_args[1])
4746
self.nodes[1].walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
4847

test/functional/fundrawtransaction.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,8 +451,7 @@ def run_test(self):
451451
self.stop_node(0)
452452
self.stop_node(2)
453453
self.stop_node(3)
454-
self.nodes[1].encryptwallet("test")
455-
self.bitcoind_processes[1].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT)
454+
self.nodes[1].node_encrypt_wallet("test")
456455

457456
self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir)
458457
# This test is not meant to test fee estimation and we'd like

test/functional/getblocktemplate_longpoll.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def __init__(self, node):
1717
self.longpollid = templat['longpollid']
1818
# create a new connection to the node, we can't use the same
1919
# connection from two threads
20-
self.node = get_rpc_proxy(node.url, 1, timeout=600)
20+
self.node = get_rpc_proxy(node.url, 1, timeout=600, coveragedir=node.coverage_dir)
2121

2222
def run(self):
2323
self.node.getblocktemplate({'longpollid':self.longpollid})

test/functional/keypool.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ def run_test(self):
1717
assert(addr_before_encrypting_data['hdmasterkeyid'] == wallet_info_old['hdmasterkeyid'])
1818

1919
# Encrypt wallet and wait to terminate
20-
nodes[0].encryptwallet('test')
21-
self.bitcoind_processes[0].wait()
20+
nodes[0].node_encrypt_wallet('test')
2221
# Restart node 0
2322
nodes[0] = self.start_node(0, self.options.tmpdir)
2423
# Keep creating keys

test/functional/multiwallet.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,15 @@ def run_test(self):
3535

3636
self.nodes[0] = self.start_node(0, self.options.tmpdir, self.extra_args[0])
3737

38-
w1 = self.nodes[0] / "wallet/w1"
38+
w1 = self.nodes[0].get_wallet_rpc("w1")
39+
w2 = self.nodes[0].get_wallet_rpc("w2")
40+
w3 = self.nodes[0].get_wallet_rpc("w3")
41+
wallet_bad = self.nodes[0].get_wallet_rpc("bad")
42+
3943
w1.generate(1)
4044

4145
# accessing invalid wallet fails
42-
assert_raises_jsonrpc(-18, "Requested wallet does not exist or is not loaded", (self.nodes[0] / "wallet/bad").getwalletinfo)
46+
assert_raises_jsonrpc(-18, "Requested wallet does not exist or is not loaded", wallet_bad.getwalletinfo)
4347

4448
# accessing wallet RPC without using wallet endpoint fails
4549
assert_raises_jsonrpc(-19, "Wallet file not specified", self.nodes[0].getwalletinfo)
@@ -50,14 +54,12 @@ def run_test(self):
5054
w1_name = w1_info['walletname']
5155
assert_equal(w1_name, "w1")
5256

53-
# check w1 wallet balance
54-
w2 = self.nodes[0] / "wallet/w2"
57+
# check w2 wallet balance
5558
w2_info = w2.getwalletinfo()
5659
assert_equal(w2_info['immature_balance'], 0)
5760
w2_name = w2_info['walletname']
5861
assert_equal(w2_name, "w2")
5962

60-
w3 = self.nodes[0] / "wallet/w3"
6163
w3_name = w3.getwalletinfo()['walletname']
6264
assert_equal(w3_name, "w3")
6365

test/functional/rpcbind_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def run_bind_test(self, allow_ips, connect_to, addresses, expected):
3737
base_args += ['-rpcallowip=' + x for x in allow_ips]
3838
binds = ['-rpcbind='+addr for addr in addresses]
3939
self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir, [base_args + binds], connect_to)
40-
pid = self.bitcoind_processes[0].pid
40+
pid = self.nodes[0].process.pid
4141
assert_equal(set(get_bind_addrs(pid)), set(expected))
4242
self.stop_nodes()
4343

@@ -49,7 +49,7 @@ def run_allowip_test(self, allow_ips, rpchost, rpcport):
4949
base_args = ['-disablewallet', '-nolisten'] + ['-rpcallowip='+x for x in allow_ips]
5050
self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir, [base_args])
5151
# connect to node through non-loopback interface
52-
node = get_rpc_proxy(rpc_url(get_datadir_path(self.options.tmpdir, 0), 0, "%s:%d" % (rpchost, rpcport)), 0)
52+
node = get_rpc_proxy(rpc_url(get_datadir_path(self.options.tmpdir, 0), 0, "%s:%d" % (rpchost, rpcport)), 0, coveragedir=self.options.coveragedir)
5353
node.getnetworkinfo()
5454
self.stop_nodes()
5555

test/functional/test_framework/test_framework.py

Lines changed: 40 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,30 @@
55
"""Base class for RPC testing."""
66

77
from collections import deque
8-
import errno
98
from enum import Enum
10-
import http.client
119
import logging
1210
import optparse
1311
import os
1412
import pdb
1513
import shutil
16-
import subprocess
1714
import sys
1815
import tempfile
1916
import time
2017
import traceback
2118

2219
from .authproxy import JSONRPCException
2320
from . import coverage
21+
from .test_node import TestNode
2422
from .util import (
2523
MAX_NODES,
2624
PortSeed,
2725
assert_equal,
2826
check_json_precision,
2927
connect_nodes_bi,
3028
disconnect_nodes,
31-
get_rpc_proxy,
3229
initialize_datadir,
33-
get_datadir_path,
3430
log_filename,
3531
p2p_port,
36-
rpc_url,
3732
set_node_times,
3833
sync_blocks,
3934
sync_mempools,
@@ -70,7 +65,6 @@ def __init__(self):
7065
self.num_nodes = 4
7166
self.setup_clean_chain = False
7267
self.nodes = []
73-
self.bitcoind_processes = {}
7468
self.mocktime = 0
7569

7670
def add_options(self, parser):
@@ -213,64 +207,62 @@ def main(self):
213207
def start_node(self, i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, stderr=None):
214208
"""Start a bitcoind and return RPC connection to it"""
215209

216-
datadir = os.path.join(dirname, "node" + str(i))
210+
if extra_args is None:
211+
extra_args = []
217212
if binary is None:
218213
binary = os.getenv("BITCOIND", "bitcoind")
219-
args = [binary, "-datadir=" + datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-logtimemicros", "-debug", "-debugexclude=libevent", "-debugexclude=leveldb", "-mocktime=" + str(self.mocktime), "-uacomment=testnode%d" % i]
220-
if extra_args is not None:
221-
args.extend(extra_args)
222-
self.bitcoind_processes[i] = subprocess.Popen(args, stderr=stderr)
223-
self.log.debug("initialize_chain: bitcoind started, waiting for RPC to come up")
224-
self._wait_for_bitcoind_start(self.bitcoind_processes[i], datadir, i, rpchost)
225-
self.log.debug("initialize_chain: RPC successfully started")
226-
proxy = get_rpc_proxy(rpc_url(datadir, i, rpchost), i, timeout=timewait)
214+
node = TestNode(i, dirname, extra_args, rpchost, timewait, binary, stderr, self.mocktime, coverage_dir=self.options.coveragedir)
215+
node.start()
216+
node.wait_for_rpc_connection()
227217

228-
if self.options.coveragedir:
229-
coverage.write_all_rpc_commands(self.options.coveragedir, proxy)
218+
if self.options.coveragedir is not None:
219+
coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc)
230220

231-
return proxy
221+
return node
232222

233223
def start_nodes(self, num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None):
234224
"""Start multiple bitcoinds, return RPC connections to them"""
235225

236226
if extra_args is None:
237-
extra_args = [None] * num_nodes
227+
extra_args = [[]] * num_nodes
238228
if binary is None:
239229
binary = [None] * num_nodes
240230
assert_equal(len(extra_args), num_nodes)
241231
assert_equal(len(binary), num_nodes)
242-
rpcs = []
232+
nodes = []
243233
try:
244234
for i in range(num_nodes):
245-
rpcs.append(self.start_node(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i]))
235+
nodes.append(TestNode(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i], stderr=None, mocktime=self.mocktime, coverage_dir=self.options.coveragedir))
236+
nodes[i].start()
237+
for node in nodes:
238+
node.wait_for_rpc_connection()
246239
except:
247240
# If one node failed to start, stop the others
248-
# TODO: abusing self.nodes in this way is a little hacky.
249-
# Eventually we should do a better job of tracking nodes
250-
self.nodes.extend(rpcs)
251241
self.stop_nodes()
252-
self.nodes = []
253242
raise
254-
return rpcs
243+
244+
if self.options.coveragedir is not None:
245+
for node in nodes:
246+
coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc)
247+
248+
return nodes
255249

256250
def stop_node(self, i):
257251
"""Stop a bitcoind test node"""
258-
259-
self.log.debug("Stopping node %d" % i)
260-
try:
261-
self.nodes[i].stop()
262-
except http.client.CannotSendRequest as e:
263-
self.log.exception("Unable to stop node")
264-
return_code = self.bitcoind_processes[i].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT)
265-
del self.bitcoind_processes[i]
266-
assert_equal(return_code, 0)
252+
self.nodes[i].stop_node()
253+
while not self.nodes[i].is_node_stopped():
254+
time.sleep(0.1)
267255

268256
def stop_nodes(self):
269257
"""Stop multiple bitcoind test nodes"""
258+
for node in self.nodes:
259+
# Issue RPC to stop nodes
260+
node.stop_node()
270261

271-
for i in range(len(self.nodes)):
272-
self.stop_node(i)
273-
assert not self.bitcoind_processes.values() # All connections must be gone now
262+
for node in self.nodes:
263+
# Wait for nodes to stop
264+
while not node.is_node_stopped():
265+
time.sleep(0.1)
274266

275267
def assert_start_raises_init_error(self, i, dirname, extra_args=None, expected_msg=None):
276268
with tempfile.SpooledTemporaryFile(max_size=2**16) as log_stderr:
@@ -279,6 +271,8 @@ def assert_start_raises_init_error(self, i, dirname, extra_args=None, expected_m
279271
self.stop_node(i)
280272
except Exception as e:
281273
assert 'bitcoind exited' in str(e) # node must have shutdown
274+
self.nodes[i].running = False
275+
self.nodes[i].process = None
282276
if expected_msg is not None:
283277
log_stderr.seek(0)
284278
stderr = log_stderr.read().decode('utf-8')
@@ -292,7 +286,7 @@ def assert_start_raises_init_error(self, i, dirname, extra_args=None, expected_m
292286
raise AssertionError(assert_msg)
293287

294288
def wait_for_node_exit(self, i, timeout):
295-
self.bitcoind_processes[i].wait(timeout)
289+
self.nodes[i].process.wait(timeout)
296290

297291
def split_network(self):
298292
"""
@@ -389,18 +383,13 @@ def _initialize_chain(self, test_dir, num_nodes, cachedir):
389383
args = [os.getenv("BITCOIND", "bitcoind"), "-server", "-keypool=1", "-datadir=" + datadir, "-discover=0"]
390384
if i > 0:
391385
args.append("-connect=127.0.0.1:" + str(p2p_port(0)))
392-
self.bitcoind_processes[i] = subprocess.Popen(args)
393-
self.log.debug("initialize_chain: bitcoind started, waiting for RPC to come up")
394-
self._wait_for_bitcoind_start(self.bitcoind_processes[i], datadir, i)
395-
self.log.debug("initialize_chain: RPC successfully started")
386+
self.nodes.append(TestNode(i, cachedir, extra_args=[], rpchost=None, timewait=None, binary=None, stderr=None, mocktime=self.mocktime, coverage_dir=None))
387+
self.nodes[i].args = args
388+
self.nodes[i].start()
396389

397-
self.nodes = []
398-
for i in range(MAX_NODES):
399-
try:
400-
self.nodes.append(get_rpc_proxy(rpc_url(get_datadir_path(cachedir, i), i), i))
401-
except:
402-
self.log.exception("Error connecting to node %d" % i)
403-
sys.exit(1)
390+
# Wait for RPC connections to be ready
391+
for node in self.nodes:
392+
node.wait_for_rpc_connection()
404393

405394
# Create a 200-block-long chain; each of the 4 first nodes
406395
# gets 25 mature blocks and 25 immature.
@@ -444,30 +433,6 @@ def _initialize_chain_clean(self, test_dir, num_nodes):
444433
for i in range(num_nodes):
445434
initialize_datadir(test_dir, i)
446435

447-
def _wait_for_bitcoind_start(self, process, datadir, i, rpchost=None):
448-
"""Wait for bitcoind to start.
449-
450-
This means that RPC is accessible and fully initialized.
451-
Raise an exception if bitcoind exits during initialization."""
452-
while True:
453-
if process.poll() is not None:
454-
raise Exception('bitcoind exited with status %i during initialization' % process.returncode)
455-
try:
456-
# Check if .cookie file to be created
457-
rpc = get_rpc_proxy(rpc_url(datadir, i, rpchost), i, coveragedir=self.options.coveragedir)
458-
rpc.getblockcount()
459-
break # break out of loop on success
460-
except IOError as e:
461-
if e.errno != errno.ECONNREFUSED: # Port not yet open?
462-
raise # unknown IO error
463-
except JSONRPCException as e: # Initialization phase
464-
if e.error['code'] != -28: # RPC in warmup?
465-
raise # unknown JSON RPC exception
466-
except ValueError as e: # cookie file not found and no rpcuser or rpcassword. bitcoind still starting
467-
if "No RPC credentials" not in str(e):
468-
raise
469-
time.sleep(0.25)
470-
471436
class ComparisonTestFramework(BitcoinTestFramework):
472437
"""Test framework for doing p2p comparison testing
473438

0 commit comments

Comments
 (0)