Changeset View
Changeset View
Standalone View
Standalone View
test/functional/test_framework/test_framework.py
Show First 20 Lines • Show All 45 Lines • ▼ Show 20 Lines | |||||
TEST_EXIT_FAILED = 1 | TEST_EXIT_FAILED = 1 | ||||
TEST_EXIT_SKIPPED = 77 | TEST_EXIT_SKIPPED = 77 | ||||
# Timestamp is 01.01.2019 | # Timestamp is 01.01.2019 | ||||
TIMESTAMP_IN_THE_PAST = 1546300800 | TIMESTAMP_IN_THE_PAST = 1546300800 | ||||
class BitcoinTestFramework(): | class BitcoinTestFramework(): | ||||
"""Base class for a bitcoin test script. | """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 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: | Individual tests can also override the following methods to customize the test setup: | ||||
- add_options() | - add_options() | ||||
- setup_chain() | - setup_chain() | ||||
Show All 10 Lines | def __init__(self): | ||||
self.nodes = [] | self.nodes = [] | ||||
self.mocktime = 0 | self.mocktime = 0 | ||||
self.supports_cli = False | self.supports_cli = False | ||||
def main(self): | def main(self): | ||||
"""Main function. This should not be overridden by the subclass test scripts.""" | """Main function. This should not be overridden by the subclass test scripts.""" | ||||
parser = argparse.ArgumentParser(usage="%(prog)s [options]") | parser = argparse.ArgumentParser(usage="%(prog)s [options]") | ||||
parser.add_argument("--nocleanup", dest="nocleanup", default=False, action="store_true", | parser.add_argument( | ||||
"--nocleanup", dest="nocleanup", default=False, action="store_true", | |||||
help="Leave bitcoinds and test.* datadir on exit or error") | help="Leave bitcoinds and test.* datadir on exit or error") | ||||
parser.add_argument("--noshutdown", dest="noshutdown", default=False, action="store_true", | parser.add_argument( | ||||
"--noshutdown", dest="noshutdown", default=False, action="store_true", | |||||
help="Don't stop bitcoinds after the test execution") | help="Don't stop bitcoinds after the test execution") | ||||
parser.add_argument("--srcdir", dest="srcdir", default=os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../../../src"), | parser.add_argument( | ||||
"--srcdir", dest="srcdir", default=os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../../../src"), | |||||
help="Source directory containing bitcoind/bitcoin-cli (default: %(default)s)") | help="Source directory containing bitcoind/bitcoin-cli (default: %(default)s)") | ||||
parser.add_argument("--cachedir", dest="cachedir", default=os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../../cache"), | parser.add_argument( | ||||
"--cachedir", dest="cachedir", default=os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../../cache"), | |||||
help="Directory for caching pregenerated datadirs") | help="Directory for caching pregenerated datadirs") | ||||
parser.add_argument("--tmpdir", dest="tmpdir", | parser.add_argument("--tmpdir", dest="tmpdir", | ||||
help="Root directory for datadirs") | help="Root directory for datadirs") | ||||
parser.add_argument("-l", "--loglevel", dest="loglevel", default="INFO", | parser.add_argument( | ||||
"-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.") | 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_argument("--tracerpc", dest="trace_rpc", default=False, action="store_true", | parser.add_argument( | ||||
"--tracerpc", dest="trace_rpc", default=False, action="store_true", | |||||
help="Print out all RPC calls as they are made") | help="Print out all RPC calls as they are made") | ||||
parser.add_argument("--portseed", dest="port_seed", default=os.getpid(), type=int, | parser.add_argument( | ||||
"--portseed", dest="port_seed", default=os.getpid(), type=int, | |||||
help="The seed to use for assigning port numbers (default: current process id)") | help="The seed to use for assigning port numbers (default: current process id)") | ||||
parser.add_argument("--coveragedir", dest="coveragedir", | parser.add_argument("--coveragedir", dest="coveragedir", | ||||
help="Write tested RPC commands into this directory") | help="Write tested RPC commands into this directory") | ||||
parser.add_argument("--configfile", dest="configfile", | parser.add_argument("--configfile", dest="configfile", | ||||
help="Location of the test framework config file") | help="Location of the test framework config file") | ||||
parser.add_argument("--pdbonfailure", dest="pdbonfailure", default=False, action="store_true", | parser.add_argument( | ||||
"--pdbonfailure", dest="pdbonfailure", default=False, action="store_true", | |||||
help="Attach a python debugger if test fails") | help="Attach a python debugger if test fails") | ||||
parser.add_argument("--usecli", dest="usecli", default=False, action="store_true", | parser.add_argument( | ||||
"--usecli", dest="usecli", default=False, action="store_true", | |||||
help="use bitcoin-cli instead of RPC for all commands") | help="use bitcoin-cli instead of RPC for all commands") | ||||
parser.add_argument("--with-greatwallactivation", dest="greatwallactivation", default=False, action="store_true", | parser.add_argument( | ||||
"--with-greatwallactivation", dest="greatwallactivation", default=False, action="store_true", | |||||
help="Activate great wall update on timestamp {}".format(TIMESTAMP_IN_THE_PAST)) | help="Activate great wall update on timestamp {}".format(TIMESTAMP_IN_THE_PAST)) | ||||
self.add_options(parser) | self.add_options(parser) | ||||
self.options = parser.parse_args() | self.options = parser.parse_args() | ||||
self.set_test_params() | self.set_test_params() | ||||
assert hasattr( | assert hasattr( | ||||
self, "num_nodes"), "Test must set self.num_nodes in set_test_params()" | self, "num_nodes"), "Test must set self.num_nodes in set_test_params()" | ||||
Show All 34 Lines | def main(self): | ||||
except KeyError as e: | except KeyError as e: | ||||
self.log.exception("Key error") | self.log.exception("Key error") | ||||
except Exception as e: | except Exception as e: | ||||
self.log.exception("Unexpected exception caught during testing") | self.log.exception("Unexpected exception caught during testing") | ||||
except KeyboardInterrupt as e: | except KeyboardInterrupt as e: | ||||
self.log.warning("Exiting after keyboard interrupt") | self.log.warning("Exiting after keyboard interrupt") | ||||
if success == TestStatus.FAILED and self.options.pdbonfailure: | if success == TestStatus.FAILED and self.options.pdbonfailure: | ||||
print("Testcase failed. Attaching python debugger. Enter ? for help") | print( | ||||
"Testcase failed. Attaching python debugger. Enter ? for help") | |||||
pdb.set_trace() | pdb.set_trace() | ||||
if not self.options.noshutdown: | if not self.options.noshutdown: | ||||
self.log.info("Stopping nodes") | self.log.info("Stopping nodes") | ||||
if self.nodes: | if self.nodes: | ||||
self.stop_nodes() | self.stop_nodes() | ||||
else: | else: | ||||
for node in self.nodes: | |||||
node.cleanup_on_exit = False | |||||
self.log.info( | self.log.info( | ||||
"Note: bitcoinds were not stopped and may still be running") | "Note: bitcoinds were not stopped and may still be running") | ||||
if not self.options.nocleanup and not self.options.noshutdown and success != TestStatus.FAILED: | if not self.options.nocleanup and not self.options.noshutdown and success != TestStatus.FAILED: | ||||
self.log.info("Cleaning up") | self.log.info("Cleaning up") | ||||
shutil.rmtree(self.options.tmpdir) | shutil.rmtree(self.options.tmpdir) | ||||
else: | else: | ||||
self.log.warning( | self.log.warning( | ||||
▲ Show 20 Lines • Show All 62 Lines • ▼ Show 20 Lines | def setup_nodes(self): | ||||
extra_args = self.extra_args | extra_args = self.extra_args | ||||
self.add_nodes(self.num_nodes, extra_args) | self.add_nodes(self.num_nodes, extra_args) | ||||
self.start_nodes() | self.start_nodes() | ||||
def run_test(self): | def run_test(self): | ||||
"""Tests must override this method to define test logic""" | """Tests must override this method to define test logic""" | ||||
raise NotImplementedError | raise NotImplementedError | ||||
# Public helper methods. These can be accessed by the subclass test scripts. | # 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): | def add_nodes(self, num_nodes, extra_args=None, rpchost=None, timewait=None, binary=None): | ||||
"""Instantiate TestNode objects""" | """Instantiate TestNode objects""" | ||||
if extra_args is None: | if extra_args is None: | ||||
extra_args = [[]] * num_nodes | extra_args = [[]] * num_nodes | ||||
if binary is None: | if binary is None: | ||||
binary = [None] * num_nodes | binary = [None] * num_nodes | ||||
assert_equal(len(extra_args), num_nodes) | assert_equal(len(extra_args), num_nodes) | ||||
assert_equal(len(binary), num_nodes) | assert_equal(len(binary), num_nodes) | ||||
for i in range(num_nodes): | for i in range(num_nodes): | ||||
self.nodes.append(TestNode(i, self.options.tmpdir, extra_args[i], rpchost, rpc_port=rpc_port(i), p2p_port=p2p_port(i), | self.nodes.append( | ||||
TestNode( | |||||
i, self.options.tmpdir, extra_args[i], rpchost, rpc_port=rpc_port(i), p2p_port=p2p_port(i), | |||||
timewait=timewait, binary=binary[i], stderr=None, mocktime=self.mocktime, coverage_dir=self.options.coveragedir, use_cli=self.options.usecli)) | timewait=timewait, binary=binary[i], stderr=None, mocktime=self.mocktime, coverage_dir=self.options.coveragedir, use_cli=self.options.usecli)) | ||||
if self.options.greatwallactivation: | if self.options.greatwallactivation: | ||||
self.nodes[i].extend_default_args( | self.nodes[i].extend_default_args( | ||||
["-greatwallactivationtime={}".format(TIMESTAMP_IN_THE_PAST)]) | ["-greatwallactivationtime={}".format(TIMESTAMP_IN_THE_PAST)]) | ||||
def start_node(self, i, *args, **kwargs): | def start_node(self, i, *args, **kwargs): | ||||
"""Start a bitcoind""" | """Start a bitcoind""" | ||||
node = self.nodes[i] | node = self.nodes[i] | ||||
▲ Show 20 Lines • Show All 60 Lines • ▼ Show 20 Lines | def assert_start_raises_init_error(self, i, extra_args=None, expected_msg=None, *args, **kwargs): | ||||
stderr = log_stderr.read().decode('utf-8') | stderr = log_stderr.read().decode('utf-8') | ||||
if expected_msg not in stderr: | if expected_msg not in stderr: | ||||
raise AssertionError( | raise AssertionError( | ||||
"Expected error \"" + expected_msg + "\" not found in:\n" + stderr) | "Expected error \"" + expected_msg + "\" not found in:\n" + stderr) | ||||
else: | else: | ||||
if expected_msg is None: | if expected_msg is None: | ||||
assert_msg = "bitcoind should have exited with an error" | assert_msg = "bitcoind should have exited with an error" | ||||
else: | else: | ||||
assert_msg = "bitcoind should have exited with expected error " + expected_msg | assert_msg = "bitcoind should have exited with expected error " + \ | ||||
expected_msg | |||||
raise AssertionError(assert_msg) | raise AssertionError(assert_msg) | ||||
def wait_for_node_exit(self, i, timeout): | def wait_for_node_exit(self, i, timeout): | ||||
self.nodes[i].process.wait(timeout) | self.nodes[i].process.wait(timeout) | ||||
def split_network(self): | def split_network(self): | ||||
""" | """ | ||||
Split the network of four nodes into nodes 0/1 and 2/3. | Split the network of four nodes into nodes 0/1 and 2/3. | ||||
Show All 12 Lines | class BitcoinTestFramework(): | ||||
def sync_all(self, node_groups=None): | def sync_all(self, node_groups=None): | ||||
if not node_groups: | if not node_groups: | ||||
node_groups = [self.nodes] | node_groups = [self.nodes] | ||||
for group in node_groups: | for group in node_groups: | ||||
sync_blocks(group) | sync_blocks(group) | ||||
sync_mempools(group) | sync_mempools(group) | ||||
# Private helper methods. These should not be accessed by the subclass test scripts. | # Private helper methods. These should not be accessed by the subclass | ||||
# test scripts. | |||||
def _start_logging(self): | def _start_logging(self): | ||||
# Add logger and logging handlers | # Add logger and logging handlers | ||||
self.log = logging.getLogger('TestFramework') | self.log = logging.getLogger('TestFramework') | ||||
self.log.setLevel(logging.DEBUG) | self.log.setLevel(logging.DEBUG) | ||||
# Create file handler to log all messages | # Create file handler to log all messages | ||||
fh = logging.FileHandler(self.options.tmpdir + '/test_framework.log') | fh = logging.FileHandler(self.options.tmpdir + '/test_framework.log') | ||||
fh.setLevel(logging.DEBUG) | fh.setLevel(logging.DEBUG) | ||||
# Create console handler to log messages to stderr. By default this | # Create console handler to log messages to stderr. By default this | ||||
# logs only error messages, but can be configured with --loglevel. | # logs only error messages, but can be configured with --loglevel. | ||||
ch = logging.StreamHandler(sys.stdout) | ch = logging.StreamHandler(sys.stdout) | ||||
# User can provide log level as a number or string (eg DEBUG). loglevel | # 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 | # was caught as a string, so try to convert it to an int | ||||
ll = int(self.options.loglevel) if self.options.loglevel.isdigit( | ll = int(self.options.loglevel) if self.options.loglevel.isdigit( | ||||
) else self.options.loglevel.upper() | ) else self.options.loglevel.upper() | ||||
ch.setLevel(ll) | ch.setLevel(ll) | ||||
# Format logs the same as bitcoind's debug.log with microprecision (so log files can be concatenated and sorted) | # Format logs the same as bitcoind's debug.log with microprecision (so | ||||
# log files can be concatenated and sorted) | |||||
formatter = logging.Formatter( | formatter = logging.Formatter( | ||||
fmt='%(asctime)s.%(msecs)03d000Z %(name)s (%(levelname)s): %(message)s', datefmt='%Y-%m-%dT%H:%M:%S') | fmt='%(asctime)s.%(msecs)03d000Z %(name)s (%(levelname)s): %(message)s', datefmt='%Y-%m-%dT%H:%M:%S') | ||||
formatter.converter = time.gmtime | formatter.converter = time.gmtime | ||||
fh.setFormatter(formatter) | fh.setFormatter(formatter) | ||||
ch.setFormatter(formatter) | ch.setFormatter(formatter) | ||||
# add the handlers to the logger | # add the handlers to the logger | ||||
self.log.addHandler(fh) | self.log.addHandler(fh) | ||||
self.log.addHandler(ch) | self.log.addHandler(ch) | ||||
Show All 25 Lines | def _initialize_chain(self): | ||||
for i in range(MAX_NODES): | for i in range(MAX_NODES): | ||||
if os.path.isdir(os.path.join(self.options.cachedir, "node" + str(i))): | if os.path.isdir(os.path.join(self.options.cachedir, "node" + str(i))): | ||||
shutil.rmtree(os.path.join( | shutil.rmtree(os.path.join( | ||||
self.options.cachedir, "node" + str(i))) | self.options.cachedir, "node" + str(i))) | ||||
# Create cache directories, run bitcoinds: | # Create cache directories, run bitcoinds: | ||||
for i in range(MAX_NODES): | for i in range(MAX_NODES): | ||||
datadir = initialize_datadir(self.options.cachedir, i) | datadir = initialize_datadir(self.options.cachedir, i) | ||||
self.nodes.append(TestNode(i, self.options.cachedir, extra_args=[], host=None, rpc_port=rpc_port(i), p2p_port=p2p_port(i), | self.nodes.append( | ||||
TestNode( | |||||
i, self.options.cachedir, extra_args=[], host=None, rpc_port=rpc_port(i), p2p_port=p2p_port(i), | |||||
timewait=None, binary=None, stderr=None, mocktime=self.mocktime, coverage_dir=None)) | timewait=None, binary=None, stderr=None, mocktime=self.mocktime, coverage_dir=None)) | ||||
self.nodes[i].clear_default_args() | self.nodes[i].clear_default_args() | ||||
self.nodes[i].extend_default_args([ | self.nodes[i].extend_default_args([ | ||||
"-server", "-keypool=1", "-datadir=" + datadir, | "-server", "-keypool=1", "-datadir=" + datadir, | ||||
"-discover=0"]) | "-discover=0"]) | ||||
if i > 0: | if i > 0: | ||||
self.nodes[i].extend_default_args( | self.nodes[i].extend_default_args( | ||||
["-connect=127.0.0.1:" + str(p2p_port(0))]) | ["-connect=127.0.0.1:" + str(p2p_port(0))]) | ||||
▲ Show 20 Lines • Show All 50 Lines • ▼ Show 20 Lines | def _initialize_chain_clean(self): | ||||
Create an empty blockchain and num_nodes wallets. | Create an empty blockchain and num_nodes wallets. | ||||
Useful if a test case wants complete control over initialization.""" | Useful if a test case wants complete control over initialization.""" | ||||
for i in range(self.num_nodes): | for i in range(self.num_nodes): | ||||
initialize_datadir(self.options.tmpdir, i) | initialize_datadir(self.options.tmpdir, i) | ||||
class ComparisonTestFramework(BitcoinTestFramework): | class ComparisonTestFramework(BitcoinTestFramework): | ||||
"""Test framework for doing p2p comparison testing | """Test framework for doing p2p comparison testing | ||||
Sets up some bitcoind binaries: | Sets up some bitcoind binaries: | ||||
- 1 binary: test binary | - 1 binary: test binary | ||||
- 2 binaries: 1 test binary, 1 ref binary | - 2 binaries: 1 test binary, 1 ref binary | ||||
- n>2 binaries: 1 test binary, n-1 ref binaries""" | - n>2 binaries: 1 test binary, n-1 ref binaries""" | ||||
def set_test_params(self): | def set_test_params(self): | ||||
Show All 14 Lines | def setup_network(self): | ||||
extra_args = self.extra_args | extra_args = self.extra_args | ||||
self.add_nodes(self.num_nodes, extra_args, | self.add_nodes(self.num_nodes, extra_args, | ||||
binary=[self.options.testbinary] + | binary=[self.options.testbinary] + | ||||
[self.options.refbinary] * (self.num_nodes - 1)) | [self.options.refbinary] * (self.num_nodes - 1)) | ||||
self.start_nodes() | self.start_nodes() | ||||
class SkipTest(Exception): | class SkipTest(Exception): | ||||
"""This exception is raised to skip a test""" | """This exception is raised to skip a test""" | ||||
def __init__(self, message): | def __init__(self, message): | ||||
self.message = message | self.message = message |