Skip to content

Commit 7897338

Browse files
committed
[tests] Introduce TestNode
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.
1 parent e526ca6 commit 7897338

12 files changed

+193
-97
lines changed

test/functional/blockchain.py

+2-2
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

+1-2
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

+1-2
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

+1-1
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

+1-2
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

+7-5
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

+2-2
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

+40-75
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,29 @@
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 shutil
15-
import subprocess
1613
import sys
1714
import tempfile
1815
import time
1916
import traceback
2017

2118
from .authproxy import JSONRPCException
2219
from . import coverage
20+
from .test_node import TestNode
2321
from .util import (
2422
MAX_NODES,
2523
PortSeed,
2624
assert_equal,
2725
check_json_precision,
2826
connect_nodes_bi,
2927
disconnect_nodes,
30-
get_rpc_proxy,
3128
initialize_datadir,
32-
get_datadir_path,
3329
log_filename,
3430
p2p_port,
35-
rpc_url,
3631
set_node_times,
3732
sync_blocks,
3833
sync_mempools,
@@ -69,7 +64,6 @@ def __init__(self):
6964
self.num_nodes = 4
7065
self.setup_clean_chain = False
7166
self.nodes = []
72-
self.bitcoind_processes = {}
7367
self.mocktime = 0
7468

7569
def add_options(self, parser):
@@ -206,64 +200,62 @@ def main(self):
206200
def start_node(self, i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, stderr=None):
207201
"""Start a bitcoind and return RPC connection to it"""
208202

209-
datadir = os.path.join(dirname, "node" + str(i))
203+
if extra_args is None:
204+
extra_args = []
210205
if binary is None:
211206
binary = os.getenv("BITCOIND", "bitcoind")
212-
args = [binary, "-datadir=" + datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-logtimemicros", "-debug", "-debugexclude=libevent", "-debugexclude=leveldb", "-mocktime=" + str(self.mocktime), "-uacomment=testnode%d" % i]
213-
if extra_args is not None:
214-
args.extend(extra_args)
215-
self.bitcoind_processes[i] = subprocess.Popen(args, stderr=stderr)
216-
self.log.debug("initialize_chain: bitcoind started, waiting for RPC to come up")
217-
self._wait_for_bitcoind_start(self.bitcoind_processes[i], datadir, i, rpchost)
218-
self.log.debug("initialize_chain: RPC successfully started")
219-
proxy = get_rpc_proxy(rpc_url(datadir, i, rpchost), i, timeout=timewait)
207+
node = TestNode(i, dirname, extra_args, rpchost, timewait, binary, stderr, self.mocktime, coverage_dir=self.options.coveragedir)
208+
node.start()
209+
node.wait_for_rpc_connection()
220210

221-
if self.options.coveragedir:
222-
coverage.write_all_rpc_commands(self.options.coveragedir, proxy)
211+
if self.options.coveragedir is not None:
212+
coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc)
223213

224-
return proxy
214+
return node
225215

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

229219
if extra_args is None:
230-
extra_args = [None] * num_nodes
220+
extra_args = [[]] * num_nodes
231221
if binary is None:
232222
binary = [None] * num_nodes
233223
assert_equal(len(extra_args), num_nodes)
234224
assert_equal(len(binary), num_nodes)
235-
rpcs = []
225+
nodes = []
236226
try:
237227
for i in range(num_nodes):
238-
rpcs.append(self.start_node(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i]))
228+
nodes.append(TestNode(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i], stderr=None, mocktime=self.mocktime, coverage_dir=self.options.coveragedir))
229+
nodes[i].start()
230+
for node in nodes:
231+
node.wait_for_rpc_connection()
239232
except:
240233
# If one node failed to start, stop the others
241-
# TODO: abusing self.nodes in this way is a little hacky.
242-
# Eventually we should do a better job of tracking nodes
243-
self.nodes.extend(rpcs)
244234
self.stop_nodes()
245-
self.nodes = []
246235
raise
247-
return rpcs
236+
237+
if self.options.coveragedir is not None:
238+
for node in nodes:
239+
coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc)
240+
241+
return nodes
248242

249243
def stop_node(self, i):
250244
"""Stop a bitcoind test node"""
251-
252-
self.log.debug("Stopping node %d" % i)
253-
try:
254-
self.nodes[i].stop()
255-
except http.client.CannotSendRequest as e:
256-
self.log.exception("Unable to stop node")
257-
return_code = self.bitcoind_processes[i].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT)
258-
del self.bitcoind_processes[i]
259-
assert_equal(return_code, 0)
245+
self.nodes[i].stop_node()
246+
while not self.nodes[i].is_node_stopped():
247+
time.sleep(0.1)
260248

261249
def stop_nodes(self):
262250
"""Stop multiple bitcoind test nodes"""
251+
for node in self.nodes:
252+
# Issue RPC to stop nodes
253+
node.stop_node()
263254

264-
for i in range(len(self.nodes)):
265-
self.stop_node(i)
266-
assert not self.bitcoind_processes.values() # All connections must be gone now
255+
for node in self.nodes:
256+
# Wait for nodes to stop
257+
while not node.is_node_stopped():
258+
time.sleep(0.1)
267259

268260
def assert_start_raises_init_error(self, i, dirname, extra_args=None, expected_msg=None):
269261
with tempfile.SpooledTemporaryFile(max_size=2**16) as log_stderr:
@@ -272,6 +264,8 @@ def assert_start_raises_init_error(self, i, dirname, extra_args=None, expected_m
272264
self.stop_node(i)
273265
except Exception as e:
274266
assert 'bitcoind exited' in str(e) # node must have shutdown
267+
self.nodes[i].running = False
268+
self.nodes[i].process = None
275269
if expected_msg is not None:
276270
log_stderr.seek(0)
277271
stderr = log_stderr.read().decode('utf-8')
@@ -285,7 +279,7 @@ def assert_start_raises_init_error(self, i, dirname, extra_args=None, expected_m
285279
raise AssertionError(assert_msg)
286280

287281
def wait_for_node_exit(self, i, timeout):
288-
self.bitcoind_processes[i].wait(timeout)
282+
self.nodes[i].process.wait(timeout)
289283

290284
def split_network(self):
291285
"""
@@ -382,18 +376,13 @@ def _initialize_chain(self, test_dir, num_nodes, cachedir):
382376
args = [os.getenv("BITCOIND", "bitcoind"), "-server", "-keypool=1", "-datadir=" + datadir, "-discover=0"]
383377
if i > 0:
384378
args.append("-connect=127.0.0.1:" + str(p2p_port(0)))
385-
self.bitcoind_processes[i] = subprocess.Popen(args)
386-
self.log.debug("initialize_chain: bitcoind started, waiting for RPC to come up")
387-
self._wait_for_bitcoind_start(self.bitcoind_processes[i], datadir, i)
388-
self.log.debug("initialize_chain: RPC successfully started")
379+
self.nodes.append(TestNode(i, cachedir, extra_args=[], rpchost=None, timewait=None, binary=None, stderr=None, mocktime=self.mocktime, coverage_dir=None))
380+
self.nodes[i].args = args
381+
self.nodes[i].start()
389382

390-
self.nodes = []
391-
for i in range(MAX_NODES):
392-
try:
393-
self.nodes.append(get_rpc_proxy(rpc_url(get_datadir_path(cachedir, i), i), i))
394-
except:
395-
self.log.exception("Error connecting to node %d" % i)
396-
sys.exit(1)
383+
# Wait for RPC connections to be ready
384+
for node in self.nodes:
385+
node.wait_for_rpc_connection()
397386

398387
# Create a 200-block-long chain; each of the 4 first nodes
399388
# gets 25 mature blocks and 25 immature.
@@ -437,30 +426,6 @@ def _initialize_chain_clean(self, test_dir, num_nodes):
437426
for i in range(num_nodes):
438427
initialize_datadir(test_dir, i)
439428

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

0 commit comments

Comments
 (0)