aboutsummaryrefslogtreecommitdiff
path: root/test/functional/test_framework/test_framework.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/test_framework/test_framework.py')
-rwxr-xr-xtest/functional/test_framework/test_framework.py269
1 files changed, 123 insertions, 146 deletions
diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py
index e562d11938..103651f175 100755
--- a/test/functional/test_framework/test_framework.py
+++ b/test/functional/test_framework/test_framework.py
@@ -5,15 +5,12 @@
"""Base class for RPC testing."""
from collections import deque
-import errno
from enum import Enum
-import http.client
import logging
import optparse
import os
import pdb
import shutil
-import subprocess
import sys
import tempfile
import time
@@ -21,6 +18,7 @@ import traceback
from .authproxy import JSONRPCException
from . import coverage
+from .test_node import TestNode
from .util import (
MAX_NODES,
PortSeed,
@@ -28,12 +26,9 @@ from .util import (
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,
@@ -53,58 +48,30 @@ BITCOIND_PROC_WAIT_TIMEOUT = 60
class BitcoinTestFramework(object):
"""Base class for a bitcoin test script.
- Individual bitcoin test scripts should subclass this class and override the following methods:
+ Individual bitcoin test scripts should subclass this class and override the set_test_params() and run_test() methods.
+
+ Individual tests can also override the following methods to customize the test setup:
- - __init__()
- add_options()
- setup_chain()
- setup_network()
- - run_test()
+ - setup_nodes()
- The main() method should not be overridden.
+ The __init__() and main() methods should not be overridden.
This class also contains various public and private helper methods."""
- # Methods to override in subclass test scripts.
def __init__(self):
- self.num_nodes = 4
+ """Sets test framework defaults. Do not override this method. Instead, override the set_test_params() method"""
self.setup_clean_chain = False
self.nodes = []
- self.bitcoind_processes = {}
self.mocktime = 0
+ self.set_test_params()
- def add_options(self, parser):
- pass
-
- def setup_chain(self):
- self.log.info("Initializing test directory " + self.options.tmpdir)
- if self.setup_clean_chain:
- self._initialize_chain_clean(self.options.tmpdir, self.num_nodes)
- else:
- self._initialize_chain(self.options.tmpdir, self.num_nodes, self.options.cachedir)
-
- def setup_network(self):
- self.setup_nodes()
-
- # Connect the nodes as a "chain". This allows us
- # to split the network between nodes 1 and 2 to get
- # two halves that can work on competing chains.
- for i in range(self.num_nodes - 1):
- connect_nodes_bi(self.nodes, i, i + 1)
- self.sync_all()
-
- def setup_nodes(self):
- extra_args = None
- if hasattr(self, "extra_args"):
- extra_args = self.extra_args
- self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir, extra_args)
-
- def run_test(self):
- raise NotImplementedError
-
- # Main function. This should not be overridden by the subclass test scripts.
+ assert hasattr(self, "num_nodes"), "Test must set self.num_nodes in set_test_params()"
def main(self):
+ """Main function. This should not be overridden by the subclass test scripts."""
parser = optparse.OptionParser(usage="%prog [options]")
parser.add_option("--nocleanup", dest="nocleanup", default=False, action="store_true",
@@ -208,77 +175,117 @@ class BitcoinTestFramework(object):
logging.shutdown()
sys.exit(TEST_EXIT_FAILED)
- # Public helper methods. These can be accessed by the subclass test scripts.
+ # Methods to override in subclass test scripts.
+ def set_test_params(self):
+ """Tests must this method to change default values for number of nodes, topology, etc"""
+ raise NotImplementedError
- 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"""
+ def add_options(self, parser):
+ """Override this method to add command-line options to the test"""
+ pass
- datadir = os.path.join(dirname, "node" + str(i))
- 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)
+ def setup_chain(self):
+ """Override this method to customize blockchain setup"""
+ self.log.info("Initializing test directory " + self.options.tmpdir)
+ if self.setup_clean_chain:
+ self._initialize_chain_clean()
+ else:
+ self._initialize_chain()
- if self.options.coveragedir:
- coverage.write_all_rpc_commands(self.options.coveragedir, proxy)
+ def setup_network(self):
+ """Override this method to customize test network topology"""
+ self.setup_nodes()
- return proxy
+ # Connect the nodes as a "chain". This allows us
+ # to split the network between nodes 1 and 2 to get
+ # two halves that can work on competing chains.
+ for i in range(self.num_nodes - 1):
+ connect_nodes_bi(self.nodes, i, i + 1)
+ self.sync_all()
- def start_nodes(self, num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None):
- """Start multiple bitcoinds, return RPC connections to them"""
+ def setup_nodes(self):
+ """Override this method to customize test node setup"""
+ extra_args = None
+ if hasattr(self, "extra_args"):
+ extra_args = self.extra_args
+ self.add_nodes(self.num_nodes, extra_args)
+ self.start_nodes()
+
+ def run_test(self):
+ """Tests must override this method to define test logic"""
+ raise NotImplementedError
+
+ # Public helper methods. These can be accessed by the subclass test scripts.
+
+ def add_nodes(self, num_nodes, extra_args=None, rpchost=None, timewait=None, binary=None):
+ """Instantiate TestNode objects"""
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 = []
+ for i in range(num_nodes):
+ self.nodes.append(TestNode(i, self.options.tmpdir, extra_args[i], rpchost, timewait=timewait, binary=binary[i], stderr=None, mocktime=self.mocktime, coverage_dir=self.options.coveragedir))
+
+ def start_node(self, i, extra_args=None, stderr=None):
+ """Start a bitcoind"""
+
+ node = self.nodes[i]
+
+ node.start(extra_args, stderr)
+ node.wait_for_rpc_connection()
+
+ if self.options.coveragedir is not None:
+ coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc)
+
+ def start_nodes(self, extra_args=None):
+ """Start multiple bitcoinds"""
+
+ if extra_args is None:
+ extra_args = [None] * self.num_nodes
+ assert_equal(len(extra_args), self.num_nodes)
try:
- for i in range(num_nodes):
- rpcs.append(self.start_node(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i]))
+ for i, node in enumerate(self.nodes):
+ node.start(extra_args[i])
+ for node in self.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 self.nodes:
+ coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc)
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):
+ def assert_start_raises_init_error(self, i, extra_args=None, expected_msg=None):
with tempfile.SpooledTemporaryFile(max_size=2**16) as log_stderr:
try:
- self.start_node(i, dirname, extra_args, stderr=log_stderr)
+ self.start_node(i, extra_args, stderr=log_stderr)
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')
@@ -292,7 +299,7 @@ class BitcoinTestFramework(object):
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):
"""
@@ -362,16 +369,16 @@ class BitcoinTestFramework(object):
rpc_handler.setLevel(logging.DEBUG)
rpc_logger.addHandler(rpc_handler)
- def _initialize_chain(self, test_dir, num_nodes, cachedir):
+ def _initialize_chain(self):
"""Initialize a pre-mined blockchain for use by the test.
Create a cache of a 200-block-long chain (with wallet) for MAX_NODES
Afterward, create num_nodes copies from the cache."""
- assert num_nodes <= MAX_NODES
+ assert self.num_nodes <= MAX_NODES
create_cache = False
for i in range(MAX_NODES):
- if not os.path.isdir(os.path.join(cachedir, 'node' + str(i))):
+ if not os.path.isdir(os.path.join(self.options.cachedir, 'node' + str(i))):
create_cache = True
break
@@ -380,27 +387,22 @@ class BitcoinTestFramework(object):
# find and delete old cache directories if any exist
for i in range(MAX_NODES):
- if os.path.isdir(os.path.join(cachedir, "node" + str(i))):
- shutil.rmtree(os.path.join(cachedir, "node" + str(i)))
+ if os.path.isdir(os.path.join(self.options.cachedir, "node" + str(i))):
+ shutil.rmtree(os.path.join(self.options.cachedir, "node" + str(i)))
# Create cache directories, run bitcoinds:
for i in range(MAX_NODES):
- datadir = initialize_datadir(cachedir, i)
+ datadir = initialize_datadir(self.options.cachedir, i)
args = [os.getenv("BITCOIND", "bitcoind"), "-server", "-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, self.options.cachedir, extra_args=[], rpchost=None, timewait=None, binary=None, stderr=None, mocktime=self.mocktime, coverage_dir=None))
+ self.nodes[i].args = args
+ self.start_node(i)
- 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.
@@ -425,48 +427,24 @@ class BitcoinTestFramework(object):
self.nodes = []
self.disable_mocktime()
for i in range(MAX_NODES):
- os.remove(log_filename(cachedir, i, "debug.log"))
- os.remove(log_filename(cachedir, i, "db.log"))
- os.remove(log_filename(cachedir, i, "peers.dat"))
- os.remove(log_filename(cachedir, i, "fee_estimates.dat"))
-
- for i in range(num_nodes):
- from_dir = os.path.join(cachedir, "node" + str(i))
- to_dir = os.path.join(test_dir, "node" + str(i))
+ os.remove(log_filename(self.options.cachedir, i, "debug.log"))
+ os.remove(log_filename(self.options.cachedir, i, "db.log"))
+ os.remove(log_filename(self.options.cachedir, i, "peers.dat"))
+ os.remove(log_filename(self.options.cachedir, i, "fee_estimates.dat"))
+
+ for i in range(self.num_nodes):
+ from_dir = os.path.join(self.options.cachedir, "node" + str(i))
+ to_dir = os.path.join(self.options.tmpdir, "node" + str(i))
shutil.copytree(from_dir, to_dir)
- initialize_datadir(test_dir, i) # Overwrite port/rpcport in bitcoin.conf
+ initialize_datadir(self.options.tmpdir, i) # Overwrite port/rpcport in bitcoin.conf
- def _initialize_chain_clean(self, test_dir, num_nodes):
+ def _initialize_chain_clean(self):
"""Initialize empty blockchain for use by the test.
Create an empty blockchain and num_nodes wallets.
Useful if a test case wants complete control over initialization."""
- 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)
+ for i in range(self.num_nodes):
+ initialize_datadir(self.options.tmpdir, i)
class ComparisonTestFramework(BitcoinTestFramework):
"""Test framework for doing p2p comparison testing
@@ -476,8 +454,7 @@ class ComparisonTestFramework(BitcoinTestFramework):
- 2 binaries: 1 test binary, 1 ref binary
- n>2 binaries: 1 test binary, n-1 ref binaries"""
- def __init__(self):
- super().__init__()
+ def set_test_params(self):
self.num_nodes = 2
self.setup_clean_chain = True
@@ -490,13 +467,13 @@ class ComparisonTestFramework(BitcoinTestFramework):
help="bitcoind binary to use for reference nodes (if any)")
def setup_network(self):
- extra_args = [['-whitelist=127.0.0.1']]*self.num_nodes
+ extra_args = [['-whitelist=127.0.0.1']] * self.num_nodes
if hasattr(self, "extra_args"):
extra_args = self.extra_args
- self.nodes = self.start_nodes(
- self.num_nodes, self.options.tmpdir, extra_args,
- binary=[self.options.testbinary] +
- [self.options.refbinary] * (self.num_nodes - 1))
+ self.add_nodes(self.num_nodes, extra_args,
+ binary=[self.options.testbinary] +
+ [self.options.refbinary] * (self.num_nodes - 1))
+ self.start_nodes()
class SkipTest(Exception):
"""This exception is raised to skip a test"""