diff --git a/test/functional/blockchain.py b/test/functional/blockchain.py --- a/test/functional/blockchain.py +++ b/test/functional/blockchain.py @@ -167,13 +167,13 @@ assert_equal(self.nodes[0].getblockcount(), 206) self.log.debug('Node should not stop at this height') assert_raises(subprocess.TimeoutExpired, - lambda: self.bitcoind_processes[0].wait(timeout=3)) + lambda: self.nodes[0].process.wait(timeout=3)) try: self.nodes[0].generate(1) except (ConnectionError, http.client.BadStatusLine): pass # The node already shut down before response self.log.debug('Node should stop at this height...') - self.bitcoind_processes[0].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT) + self.nodes[0].process.wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT) self.nodes[0] = self.start_node(0, self.options.tmpdir) assert_equal(self.nodes[0].getblockcount(), 207) diff --git a/test/functional/dbcrash.py b/test/functional/dbcrash.py --- a/test/functional/dbcrash.py +++ b/test/functional/dbcrash.py @@ -67,7 +67,9 @@ self.node2_args, self.node3_args] def setup_network(self): - self.setup_nodes() + # Need a bit of extra time for the nodes to start up for this test + self.nodes = self.start_nodes( + self.num_nodes, self.options.tmpdir, self.extra_args, timewait=90) # Leave them unconnected, we'll use submitblock directly in this test def restart_node(self, node_index, expected_tip): @@ -77,11 +79,11 @@ after 60 seconds. Returns the utxo hash of the given node.""" time_start = time.time() - while time.time() - time_start < 60: + while time.time() - time_start < 120: try: # Any of these RPC calls could throw due to node crash self.nodes[node_index] = self.start_node( - node_index, self.options.tmpdir, self.extra_args[node_index]) + node_index, self.options.tmpdir, self.extra_args[node_index], timewait=90) self.nodes[node_index].waitforblock(expected_tip) utxo_hash = self.nodes[node_index].gettxoutsetinfo()[ 'hash_serialized'] diff --git a/test/functional/fundrawtransaction.py b/test/functional/fundrawtransaction.py --- a/test/functional/fundrawtransaction.py +++ b/test/functional/fundrawtransaction.py @@ -462,8 +462,7 @@ self.stop_node(0) self.stop_node(2) self.stop_node(3) - self.nodes[1].encryptwallet("test") - self.bitcoind_processes[1].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT) + self.nodes[1].node_encrypt_wallet("test") self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir) # This test is not meant to test fee estimation and we'd like diff --git a/test/functional/getblocktemplate_longpoll.py b/test/functional/getblocktemplate_longpoll.py --- a/test/functional/getblocktemplate_longpoll.py +++ b/test/functional/getblocktemplate_longpoll.py @@ -18,7 +18,8 @@ self.longpollid = templat['longpollid'] # create a new connection to the node, we can't use the same # connection from two threads - self.node = get_rpc_proxy(node.url, 1, timeout=600) + self.node = get_rpc_proxy( + node.url, 1, timeout=600, coveragedir=node.coverage_dir) def run(self): self.node.getblocktemplate({'longpollid': self.longpollid}) diff --git a/test/functional/keypool.py b/test/functional/keypool.py --- a/test/functional/keypool.py +++ b/test/functional/keypool.py @@ -21,8 +21,7 @@ 'hdmasterkeyid'] == wallet_info_old['hdmasterkeyid']) # Encrypt wallet and wait to terminate - nodes[0].encryptwallet('test') - self.bitcoind_processes[0].wait() + nodes[0].node_encrypt_wallet('test') # Restart node 0 nodes[0] = self.start_node(0, self.options.tmpdir) # Keep creating keys diff --git a/test/functional/multiwallet.py b/test/functional/multiwallet.py --- a/test/functional/multiwallet.py +++ b/test/functional/multiwallet.py @@ -41,12 +41,16 @@ self.nodes[0] = self.start_node( 0, self.options.tmpdir, self.extra_args[0]) - w1 = self.nodes[0] / "wallet/w1" + w1 = self.nodes[0].get_wallet_rpc("w1") + w2 = self.nodes[0].get_wallet_rpc("w2") + w3 = self.nodes[0].get_wallet_rpc("w3") + wallet_bad = self.nodes[0].get_wallet_rpc("bad") + w1.generate(1) # accessing invalid wallet fails assert_raises_jsonrpc(-18, "Requested wallet does not exist or is not loaded", - (self.nodes[0] / "wallet/bad").getwalletinfo) + wallet_bad.getwalletinfo) # accessing wallet RPC without using wallet endpoint fails assert_raises_jsonrpc(-19, "Wallet file not specified (must request wallet RPC through /wallet/ uri-path).", @@ -58,14 +62,12 @@ w1_name = w1_info['walletname'] assert_equal(w1_name, "w1") - # check w1 wallet balance - w2 = self.nodes[0] / "wallet/w2" + # check w2 wallet balance w2_info = w2.getwalletinfo() assert_equal(w2_info['immature_balance'], 0) w2_name = w2_info['walletname'] assert_equal(w2_name, "w2") - w3 = self.nodes[0] / "wallet/w3" w3_name = w3.getwalletinfo()['walletname'] assert_equal(w3_name, "w3") diff --git a/test/functional/rpcbind_test.py b/test/functional/rpcbind_test.py --- a/test/functional/rpcbind_test.py +++ b/test/functional/rpcbind_test.py @@ -36,7 +36,7 @@ binds = ['-rpcbind=' + addr for addr in addresses] self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir, [ base_args + binds], connect_to) - pid = self.bitcoind_processes[0].pid + pid = self.nodes[0].process.pid assert_equal(set(get_bind_addrs(pid)), set(expected)) self.stop_nodes() @@ -50,8 +50,8 @@ self.nodes = self.start_nodes( self.num_nodes, self.options.tmpdir, [base_args]) # connect to node through non-loopback interface - node = get_rpc_proxy(rpc_url(get_datadir_path( - self.options.tmpdir, 0), 0, "%s:%d" % (rpchost, rpcport)), 0) + node = get_rpc_proxy(rpc_url(get_datadir_path(self.options.tmpdir, 0), 0, "%s:%d" % ( + rpchost, rpcport)), 0, coveragedir=self.options.coveragedir) node.getnetworkinfo() self.stop_nodes() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -4,14 +4,11 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. from collections import deque -import errno from enum import Enum -import http.client import logging import optparse import os import shutil -import subprocess import sys import tempfile import time @@ -19,6 +16,7 @@ from .authproxy import JSONRPCException from . import coverage +from .test_node import TestNode from .util import ( MAX_NODES, PortSeed, @@ -26,12 +24,9 @@ check_json_precision, connect_nodes_bi, disconnect_nodes, - get_rpc_proxy, initialize_datadir, - get_datadir_path, log_filename, p2p_port, - rpc_url, set_node_times, sync_blocks, sync_mempools, @@ -71,7 +66,6 @@ self.num_nodes = 4 self.setup_clean_chain = False self.nodes = [] - self.bitcoind_processes = {} self.mocktime = 0 def add_options(self, parser): @@ -219,70 +213,65 @@ def start_node(self, i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, stderr=None): """Start a bitcoind and return RPC connection to it""" - datadir = os.path.join(dirname, "node" + str(i)) + if extra_args is None: + extra_args = [] if binary is None: binary = os.getenv("BITCOIND", "bitcoind") - args = [binary, "-datadir=" + datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-logtimemicros", "-debug", - "-debugexclude=libevent", "-debugexclude=leveldb", "-mocktime=" + str(self.mocktime), "-uacomment=testnode%d" % i] - if extra_args is not None: - args.extend(extra_args) - self.bitcoind_processes[i] = subprocess.Popen(args, stderr=stderr) - self.log.debug( - "initialize_chain: bitcoind started, waiting for RPC to come up") - self._wait_for_bitcoind_start( - self.bitcoind_processes[i], datadir, i, rpchost) - self.log.debug("initialize_chain: RPC successfully started") - proxy = get_rpc_proxy( - rpc_url(datadir, i, rpchost), i, timeout=timewait) - - if self.options.coveragedir: - coverage.write_all_rpc_commands(self.options.coveragedir, proxy) - - return proxy + node = TestNode(i, dirname, extra_args, rpchost, timewait, binary, + stderr, self.mocktime, coverage_dir=self.options.coveragedir) + node.start() + node.wait_for_rpc_connection() + + if self.options.coveragedir is not None: + coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc) + + return node def start_nodes(self, num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None): """Start multiple bitcoinds, return RPC connections to them""" if extra_args is None: - extra_args = [None] * num_nodes + extra_args = [[]] * num_nodes if binary is None: binary = [None] * num_nodes assert_equal(len(extra_args), num_nodes) assert_equal(len(binary), num_nodes) - rpcs = [] + nodes = [] try: for i in range(num_nodes): - rpcs.append(self.start_node( - i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i])) + nodes.append(TestNode(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i], + stderr=None, mocktime=self.mocktime, coverage_dir=self.options.coveragedir)) + nodes[i].start() + for node in nodes: + node.wait_for_rpc_connection() except: # If one node failed to start, stop the others - # TODO: abusing self.nodes in this way is a little hacky. - # Eventually we should do a better job of tracking nodes - self.nodes.extend(rpcs) self.stop_nodes() - self.nodes = [] raise - return rpcs + + if self.options.coveragedir is not None: + for node in nodes: + coverage.write_all_rpc_commands( + self.options.coveragedir, node.rpc) + + return nodes def stop_node(self, i): """Stop a bitcoind test node""" - - self.log.debug("Stopping node %d" % i) - try: - self.nodes[i].stop() - except http.client.CannotSendRequest as e: - self.log.exception("Unable to stop node") - return_code = self.bitcoind_processes[i].wait( - timeout=BITCOIND_PROC_WAIT_TIMEOUT) - del self.bitcoind_processes[i] - assert_equal(return_code, 0) + self.nodes[i].stop_node() + while not self.nodes[i].is_node_stopped(): + time.sleep(0.1) def stop_nodes(self): """Stop multiple bitcoind test nodes""" + for node in self.nodes: + # Issue RPC to stop nodes + node.stop_node() - for i in range(len(self.nodes)): - self.stop_node(i) - assert not self.bitcoind_processes.values() # All connections must be gone now + for node in self.nodes: + # Wait for nodes to stop + while not node.is_node_stopped(): + time.sleep(0.1) def assert_start_raises_init_error(self, i, dirname, extra_args=None, expected_msg=None): with tempfile.SpooledTemporaryFile(max_size=2**16) as log_stderr: @@ -291,6 +280,8 @@ self.stop_node(i) except Exception as e: assert 'bitcoind exited' in str(e) # node must have shutdown + self.nodes[i].running = False + self.nodes[i].process = None if expected_msg is not None: log_stderr.seek(0) stderr = log_stderr.read().decode('utf-8') @@ -305,7 +296,7 @@ raise AssertionError(assert_msg) def wait_for_node_exit(self, i, timeout): - self.bitcoind_processes[i].wait(timeout) + self.nodes[i].process.wait(timeout) def split_network(self): """ @@ -408,21 +399,14 @@ "-keypool=1", "-datadir=" + datadir, "-discover=0"] if i > 0: args.append("-connect=127.0.0.1:" + str(p2p_port(0))) - self.bitcoind_processes[i] = subprocess.Popen(args) - self.log.debug( - "initialize_chain: bitcoind started, waiting for RPC to come up") - self._wait_for_bitcoind_start( - self.bitcoind_processes[i], datadir, i) - self.log.debug("initialize_chain: RPC successfully started") + self.nodes.append(TestNode(i, cachedir, extra_args=[], rpchost=None, timewait=None, + binary=None, stderr=None, mocktime=self.mocktime, coverage_dir=None)) + self.nodes[i].args = args + self.nodes[i].start() - self.nodes = [] - for i in range(MAX_NODES): - try: - self.nodes.append(get_rpc_proxy( - rpc_url(get_datadir_path(cachedir, i), i), i)) - except: - self.log.exception("Error connecting to node %d" % i) - sys.exit(1) + # Wait for RPC connections to be ready + for node in self.nodes: + node.wait_for_rpc_connection() # Create a 200-block-long chain; each of the 4 first nodes # gets 25 mature blocks and 25 immature. @@ -467,32 +451,6 @@ for i in range(num_nodes): initialize_datadir(test_dir, i) - def _wait_for_bitcoind_start(self, process, datadir, i, rpchost=None): - """Wait for bitcoind to start. - - This means that RPC is accessible and fully initialized. - Raise an exception if bitcoind exits during initialization.""" - while True: - if process.poll() is not None: - raise Exception( - 'bitcoind exited with status %i during initialization' % process.returncode) - try: - # Check if .cookie file to be created - rpc = get_rpc_proxy( - rpc_url(datadir, i, rpchost), i, coveragedir=self.options.coveragedir) - rpc.getblockcount() - break # break out of loop on success - except IOError as e: - if e.errno != errno.ECONNREFUSED: # Port not yet open? - raise # unknown IO error - except JSONRPCException as e: # Initialization phase - if e.error['code'] != -28: # RPC in warmup? - raise # unknown JSON RPC exception - except ValueError as e: # cookie file not found and no rpcuser or rpcassword. bitcoind still starting - if "No RPC credentials" not in str(e): - raise - time.sleep(0.25) - class ComparisonTestFramework(BitcoinTestFramework): """Test framework for doing p2p comparison testing diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py new file mode 100755 --- /dev/null +++ b/test/functional/test_framework/test_node.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Class for bitcoind node under test""" + +import errno +import http.client +import logging +import os +import subprocess +import time + +from .util import ( + assert_equal, + get_rpc_proxy, + rpc_url, +) +from .authproxy import JSONRPCException + + +class TestNode(): + """A class for representing a bitcoind node under test. + + This class contains: + + - state about the node (whether it's running, etc) + - a Python subprocess.Popen object representing the running process + - an RPC connection to the node + + To make things easier for the test writer, a bit of magic is happening under the covers. + Any unrecognised messages will be dispatched to the RPC connection.""" + + def __init__(self, i, dirname, extra_args, rpchost, timewait, binary, stderr, mocktime, coverage_dir): + self.index = i + self.datadir = os.path.join(dirname, "node" + str(i)) + self.rpchost = rpchost + if timewait: + self.rpc_timeout = timewait + else: + # Wait for up to 60 seconds for the RPC server to respond + self.rpc_timeout = 60 + if binary is None: + self.binary = os.getenv("BITCOIND", "bitcoind") + else: + self.binary = binary + self.stderr = stderr + self.coverage_dir = coverage_dir + # Most callers will just need to add extra args to the standard list below. For those callers that need more flexibity, they can just set the args property directly. + self.extra_args = extra_args + self.args = [self.binary, "-datadir=" + self.datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-logtimemicros", + "-debug", "-debugexclude=libevent", "-debugexclude=leveldb", "-mocktime=" + str(mocktime), "-uacomment=testnode%d" % i] + + self.running = False + self.process = None + self.rpc_connected = False + self.rpc = None + self.url = None + self.log = logging.getLogger('TestFramework.node%d' % i) + + def __getattr__(self, *args, **kwargs): + """Dispatches any unrecognised messages to the RPC connection.""" + assert self.rpc_connected and self.rpc is not None, "Error: no RPC connection" + return self.rpc.__getattr__(*args, **kwargs) + + def start(self): + """Start the node.""" + self.process = subprocess.Popen( + self.args + self.extra_args, stderr=self.stderr) + self.running = True + self.log.debug("bitcoind started, waiting for RPC to come up") + + def wait_for_rpc_connection(self): + """Sets up an RPC connection to the bitcoind process. Returns False if unable to connect.""" + # Poll at a rate of four times per second + poll_per_s = 4 + for _ in range(poll_per_s * self.rpc_timeout): + assert self.process.poll( + ) is None, "bitcoind exited with status %i during initialization" % self.process.returncode + try: + self.rpc = get_rpc_proxy(rpc_url( + self.datadir, self.index, self.rpchost), self.index, coveragedir=self.coverage_dir) + self.rpc.getblockcount() + # If the call to getblockcount() succeeds then the RPC connection is up + self.rpc_connected = True + self.url = self.rpc.url + self.log.debug("RPC successfully started") + return + except IOError as e: + if e.errno != errno.ECONNREFUSED: # Port not yet open? + raise # unknown IO error + except JSONRPCException as e: # Initialization phase + if e.error['code'] != -28: # RPC in warmup? + raise # unknown JSON RPC exception + except ValueError as e: # cookie file not found and no rpcuser or rpcassword. bitcoind still starting + if "No RPC credentials" not in str(e): + raise + time.sleep(1.0 / poll_per_s) + raise AssertionError("Unable to connect to bitcoind") + + def get_wallet_rpc(self, wallet_name): + assert self.rpc_connected + assert self.rpc + wallet_path = "wallet/%s" % wallet_name + return self.rpc / wallet_path + + def stop_node(self): + """Stop the node.""" + if not self.running: + return + self.log.debug("Stopping node") + try: + self.stop() + except http.client.CannotSendRequest: + self.log.exception("Unable to stop node.") + + def is_node_stopped(self): + """Checks whether the node has stopped. + + Returns True if the node has stopped. False otherwise. + This method is responsible for freeing resources (self.process).""" + if not self.running: + return True + return_code = self.process.poll() + if return_code is not None: + # process has stopped. Assert that it didn't return an error code. + assert_equal(return_code, 0) + self.running = False + self.process = None + self.log.debug("Node stopped") + return True + return False + + def node_encrypt_wallet(self, passphrase): + """"Encrypts the wallet. + + This causes bitcoind to shutdown, so this method takes + care of cleaning up resources.""" + self.encryptwallet(passphrase) + while not self.is_node_stopped(): + time.sleep(0.1) + self.rpc = None + self.rpc_connected = False diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -9,7 +9,6 @@ import json import logging import os -import sys import random import re import time @@ -235,7 +234,7 @@ def rpc_url(datadir, i, rpchost=None): - rpc_u, rpc_p = get_auth_cookie(datadir, i) + rpc_u, rpc_p = get_auth_cookie(datadir) host = '127.0.0.1' port = rpc_port(i) if rpchost: @@ -267,7 +266,7 @@ return os.path.join(dirname, "node" + str(n)) -def get_auth_cookie(datadir, n): +def get_auth_cookie(datadir): user = None password = None if os.path.isfile(os.path.join(datadir, "bitcoin.conf")): @@ -642,6 +641,7 @@ # Try to work it based out on main module # We might expect the caller to be rpc-tests.py or a test script # itself. + import sys mainmod = sys.modules['__main__'] mainmod_path = getattr(mainmod, '__file__', '') if mainmod_path and mainmod_path.endswith('.py'): diff --git a/test/functional/wallet-dump.py b/test/functional/wallet-dump.py --- a/test/functional/wallet-dump.py +++ b/test/functional/wallet-dump.py @@ -94,8 +94,7 @@ assert_equal(found_addr_rsv, 90 * 2) # encrypt wallet, restart, unlock and dump - self.nodes[0].encryptwallet('test') - self.bitcoind_processes[0].wait() + self.nodes[0].node_encrypt_wallet('test') self.nodes[0] = self.start_node( 0, self.options.tmpdir, self.extra_args[0]) self.nodes[0].walletpassphrase('test', 10) diff --git a/test/functional/wallet-encryption.py b/test/functional/wallet-encryption.py --- a/test/functional/wallet-encryption.py +++ b/test/functional/wallet-encryption.py @@ -31,8 +31,7 @@ assert_equal(len(privkey), 52) # Encrypt the wallet - self.nodes[0].encryptwallet(passphrase) - self.bitcoind_processes[0].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT) + self.nodes[0].node_encrypt_wallet(passphrase) self.nodes[0] = self.start_node(0, self.options.tmpdir) # Test that the wallet is encrypted