diff options
Diffstat (limited to 'test/functional/test_framework/test_framework.py')
-rwxr-xr-x | test/functional/test_framework/test_framework.py | 482 |
1 files changed, 482 insertions, 0 deletions
diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py new file mode 100755 index 0000000000..381513ab9e --- /dev/null +++ b/test/functional/test_framework/test_framework.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Base class for RPC testing.""" + +from collections import deque +from enum import Enum +import logging +import optparse +import os +import pdb +import shutil +import sys +import tempfile +import time +import traceback + +from .authproxy import JSONRPCException +from . import coverage +from .test_node import TestNode +from .util import ( + MAX_NODES, + PortSeed, + assert_equal, + check_json_precision, + connect_nodes_bi, + disconnect_nodes, + initialize_datadir, + log_filename, + p2p_port, + set_node_times, + sync_blocks, + sync_mempools, +) + +class TestStatus(Enum): + PASSED = 1 + FAILED = 2 + SKIPPED = 3 + +TEST_EXIT_PASSED = 0 +TEST_EXIT_FAILED = 1 +TEST_EXIT_SKIPPED = 77 + +class BitcoinTestFramework(object): + """Base class for a bitcoin test script. + + 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: + + - add_options() + - setup_chain() + - setup_network() + - setup_nodes() + + The __init__() and main() methods should not be overridden. + + This class also contains various public and private helper methods.""" + + def __init__(self): + """Sets test framework defaults. Do not override this method. Instead, override the set_test_params() method""" + self.setup_clean_chain = False + self.nodes = [] + self.mocktime = 0 + self.set_test_params() + + 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", + help="Leave bitcoinds and test.* datadir on exit or error") + parser.add_option("--noshutdown", dest="noshutdown", default=False, action="store_true", + help="Don't stop bitcoinds after the test execution") + parser.add_option("--srcdir", dest="srcdir", default=os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../../../src"), + help="Source directory containing bitcoind/bitcoin-cli (default: %default)") + parser.add_option("--cachedir", dest="cachedir", default=os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../../cache"), + help="Directory for caching pregenerated datadirs") + parser.add_option("--tmpdir", dest="tmpdir", help="Root directory for datadirs") + parser.add_option("-l", "--loglevel", dest="loglevel", default="INFO", + help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console. Note that logs at all levels are always written to the test_framework.log file in the temporary test directory.") + parser.add_option("--tracerpc", dest="trace_rpc", default=False, action="store_true", + help="Print out all RPC calls as they are made") + parser.add_option("--portseed", dest="port_seed", default=os.getpid(), type='int', + help="The seed to use for assigning port numbers (default: current process id)") + parser.add_option("--coveragedir", dest="coveragedir", + help="Write tested RPC commands into this directory") + parser.add_option("--configfile", dest="configfile", + help="Location of the test framework config file") + parser.add_option("--pdbonfailure", dest="pdbonfailure", default=False, action="store_true", + help="Attach a python debugger if test fails") + self.add_options(parser) + (self.options, self.args) = parser.parse_args() + + PortSeed.n = self.options.port_seed + + os.environ['PATH'] = self.options.srcdir + ":" + self.options.srcdir + "/qt:" + os.environ['PATH'] + + check_json_precision() + + # Set up temp directory and start logging + if self.options.tmpdir: + os.makedirs(self.options.tmpdir, exist_ok=False) + else: + self.options.tmpdir = tempfile.mkdtemp(prefix="test") + self._start_logging() + + success = TestStatus.FAILED + + try: + self.setup_chain() + self.setup_network() + self.run_test() + success = TestStatus.PASSED + except JSONRPCException as e: + self.log.exception("JSONRPC error") + except SkipTest as e: + self.log.warning("Test Skipped: %s" % e.message) + success = TestStatus.SKIPPED + except AssertionError as e: + self.log.exception("Assertion failed") + except KeyError as e: + self.log.exception("Key error") + except Exception as e: + self.log.exception("Unexpected exception caught during testing") + except KeyboardInterrupt as e: + self.log.warning("Exiting after keyboard interrupt") + + if success == TestStatus.FAILED and self.options.pdbonfailure: + print("Testcase failed. Attaching python debugger. Enter ? for help") + pdb.set_trace() + + if not self.options.noshutdown: + self.log.info("Stopping nodes") + if self.nodes: + self.stop_nodes() + else: + self.log.info("Note: bitcoinds were not stopped and may still be running") + + if not self.options.nocleanup and not self.options.noshutdown and success != TestStatus.FAILED: + self.log.info("Cleaning up") + shutil.rmtree(self.options.tmpdir) + else: + self.log.warning("Not cleaning up dir %s" % self.options.tmpdir) + if os.getenv("PYTHON_DEBUG", ""): + # Dump the end of the debug logs, to aid in debugging rare + # travis failures. + import glob + filenames = [self.options.tmpdir + "/test_framework.log"] + filenames += glob.glob(self.options.tmpdir + "/node*/regtest/debug.log") + MAX_LINES_TO_PRINT = 1000 + for fn in filenames: + try: + with open(fn, 'r') as f: + print("From", fn, ":") + print("".join(deque(f, MAX_LINES_TO_PRINT))) + except OSError: + print("Opening file %s failed." % fn) + traceback.print_exc() + + if success == TestStatus.PASSED: + self.log.info("Tests successful") + sys.exit(TEST_EXIT_PASSED) + elif success == TestStatus.SKIPPED: + self.log.info("Test skipped") + sys.exit(TEST_EXIT_SKIPPED) + else: + self.log.error("Test failed. Test logging available at %s/test_framework.log", self.options.tmpdir) + logging.shutdown() + sys.exit(TEST_EXIT_FAILED) + + # 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 add_options(self, parser): + """Override this method to add command-line options to the test""" + pass + + 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() + + def setup_network(self): + """Override this method to customize test network topology""" + 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): + """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 = [[]] * num_nodes + if binary is None: + binary = [None] * num_nodes + assert_equal(len(extra_args), num_nodes) + assert_equal(len(binary), num_nodes) + 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, 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 + self.stop_nodes() + raise + + 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.nodes[i].stop_node() + self.nodes[i].wait_until_stopped() + + def stop_nodes(self): + """Stop multiple bitcoind test nodes""" + for node in self.nodes: + # Issue RPC to stop nodes + node.stop_node() + + for node in self.nodes: + # Wait for nodes to stop + node.wait_until_stopped() + + def restart_node(self, i, extra_args=None): + """Stop and start a test node""" + self.stop_node(i) + self.start_node(i, extra_args) + + 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, 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') + if expected_msg not in stderr: + raise AssertionError("Expected error \"" + expected_msg + "\" not found in:\n" + stderr) + else: + if expected_msg is None: + assert_msg = "bitcoind should have exited with an error" + else: + assert_msg = "bitcoind should have exited with expected error " + expected_msg + raise AssertionError(assert_msg) + + def wait_for_node_exit(self, i, timeout): + self.nodes[i].process.wait(timeout) + + def split_network(self): + """ + Split the network of four nodes into nodes 0/1 and 2/3. + """ + disconnect_nodes(self.nodes[1], 2) + disconnect_nodes(self.nodes[2], 1) + self.sync_all([self.nodes[:2], self.nodes[2:]]) + + def join_network(self): + """ + Join the (previously split) network halves together. + """ + connect_nodes_bi(self.nodes, 1, 2) + self.sync_all() + + def sync_all(self, node_groups=None): + if not node_groups: + node_groups = [self.nodes] + + for group in node_groups: + sync_blocks(group) + sync_mempools(group) + + def enable_mocktime(self): + """Enable mocktime for the script. + + mocktime may be needed for scripts that use the cached version of the + blockchain. If the cached version of the blockchain is used without + mocktime then the mempools will not sync due to IBD. + + For backwared compatibility of the python scripts with previous + versions of the cache, this helper function sets mocktime to Jan 1, + 2014 + (201 * 10 * 60)""" + self.mocktime = 1388534400 + (201 * 10 * 60) + + def disable_mocktime(self): + self.mocktime = 0 + + # Private helper methods. These should not be accessed by the subclass test scripts. + + def _start_logging(self): + # Add logger and logging handlers + self.log = logging.getLogger('TestFramework') + self.log.setLevel(logging.DEBUG) + # Create file handler to log all messages + fh = logging.FileHandler(self.options.tmpdir + '/test_framework.log') + fh.setLevel(logging.DEBUG) + # Create console handler to log messages to stderr. By default this logs only error messages, but can be configured with --loglevel. + ch = logging.StreamHandler(sys.stdout) + # User can provide log level as a number or string (eg DEBUG). loglevel was caught as a string, so try to convert it to an int + ll = int(self.options.loglevel) if self.options.loglevel.isdigit() else self.options.loglevel.upper() + ch.setLevel(ll) + # Format logs the same as bitcoind's debug.log with microprecision (so log files can be concatenated and sorted) + formatter = logging.Formatter(fmt='%(asctime)s.%(msecs)03d000 %(name)s (%(levelname)s): %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + formatter.converter = time.gmtime + fh.setFormatter(formatter) + ch.setFormatter(formatter) + # add the handlers to the logger + self.log.addHandler(fh) + self.log.addHandler(ch) + + if self.options.trace_rpc: + rpc_logger = logging.getLogger("BitcoinRPC") + rpc_logger.setLevel(logging.DEBUG) + rpc_handler = logging.StreamHandler(sys.stdout) + rpc_handler.setLevel(logging.DEBUG) + rpc_logger.addHandler(rpc_handler) + + 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 self.num_nodes <= MAX_NODES + create_cache = False + for i in range(MAX_NODES): + if not os.path.isdir(os.path.join(self.options.cachedir, 'node' + str(i))): + create_cache = True + break + + if create_cache: + self.log.debug("Creating data directories from cached datadir") + + # find and delete old cache directories if any exist + for i in range(MAX_NODES): + 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(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.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) + + # 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. + # Note: To preserve compatibility with older versions of + # initialize_chain, only 4 nodes will generate coins. + # + # blocks are created with timestamps 10 minutes apart + # starting from 2010 minutes in the past + self.enable_mocktime() + block_time = self.mocktime - (201 * 10 * 60) + for i in range(2): + for peer in range(4): + for j in range(25): + set_node_times(self.nodes, block_time) + self.nodes[peer].generate(1) + block_time += 10 * 60 + # Must sync before next peer starts generating blocks + sync_blocks(self.nodes) + + # Shut them down, and clean up cache directories: + self.stop_nodes() + self.nodes = [] + self.disable_mocktime() + for i in range(MAX_NODES): + 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(self.options.tmpdir, i) # Overwrite port/rpcport in bitcoin.conf + + 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(self.num_nodes): + initialize_datadir(self.options.tmpdir, i) + +class ComparisonTestFramework(BitcoinTestFramework): + """Test framework for doing p2p comparison testing + + Sets up some bitcoind binaries: + - 1 binary: test binary + - 2 binaries: 1 test binary, 1 ref binary + - n>2 binaries: 1 test binary, n-1 ref binaries""" + + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + + def add_options(self, parser): + parser.add_option("--testbinary", dest="testbinary", + default=os.getenv("BITCOIND", "bitcoind"), + help="bitcoind binary to test") + parser.add_option("--refbinary", dest="refbinary", + default=os.getenv("BITCOIND", "bitcoind"), + help="bitcoind binary to use for reference nodes (if any)") + + def setup_network(self): + extra_args = [['-whitelist=127.0.0.1']] * self.num_nodes + if hasattr(self, "extra_args"): + extra_args = self.extra_args + 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""" + def __init__(self, message): + self.message = message |