Changeset View
Changeset View
Standalone View
Standalone View
test/functional/test_framework/test_framework.py
Show First 20 Lines • Show All 43 Lines • ▼ Show 20 Lines | |||||
TEST_EXIT_SKIPPED = 77 | TEST_EXIT_SKIPPED = 77 | ||||
BITCOIND_PROC_WAIT_TIMEOUT = 60 | BITCOIND_PROC_WAIT_TIMEOUT = 60 | ||||
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 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() | - add_options() | ||||
- setup_chain() | - setup_chain() | ||||
- setup_network() | - 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.""" | This class also contains various public and private helper methods.""" | ||||
# Methods to override in subclass test scripts. | |||||
def __init__(self): | 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.setup_clean_chain = False | ||||
self.nodes = [] | self.nodes = [] | ||||
self.mocktime = 0 | self.mocktime = 0 | ||||
self.set_test_params() | |||||
def add_options(self, parser): | assert hasattr( | ||||
pass | self, "num_nodes"), "Test must set self.num_nodes in set_test_params()" | ||||
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): | |||||
''' | |||||
Sets up network including starting up nodes. | |||||
''' | |||||
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. | |||||
def main(self): | def main(self): | ||||
"""Main function. This should not be overridden by the subclass test scripts.""" | |||||
parser = optparse.OptionParser(usage="%prog [options]") | parser = optparse.OptionParser(usage="%prog [options]") | ||||
parser.add_option("--nocleanup", dest="nocleanup", default=False, action="store_true", | parser.add_option("--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_option("--noshutdown", dest="noshutdown", default=False, action="store_true", | parser.add_option("--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_option("--srcdir", dest="srcdir", default=os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../../../src"), | 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)") | 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"), | parser.add_option("--cachedir", dest="cachedir", default=os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../../cache"), | ||||
▲ Show 20 Lines • Show All 90 Lines • ▼ Show 20 Lines | def main(self): | ||||
self.log.info("Test skipped") | self.log.info("Test skipped") | ||||
sys.exit(TEST_EXIT_SKIPPED) | sys.exit(TEST_EXIT_SKIPPED) | ||||
else: | else: | ||||
self.log.error( | self.log.error( | ||||
"Test failed. Test logging available at %s/test_framework.log", self.options.tmpdir) | "Test failed. Test logging available at %s/test_framework.log", self.options.tmpdir) | ||||
logging.shutdown() | logging.shutdown() | ||||
sys.exit(TEST_EXIT_FAILED) | 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): | def add_options(self, parser): | ||||
"""Start a bitcoind and return RPC connection to it""" | """Override this method to add command-line options to the test""" | ||||
pass | |||||
if extra_args is None: | def setup_chain(self): | ||||
extra_args = [] | """Override this method to customize blockchain setup""" | ||||
if binary is None: | self.log.info("Initializing test directory " + self.options.tmpdir) | ||||
binary = os.getenv("BITCOIND", "bitcoind") | if self.setup_clean_chain: | ||||
node = TestNode(i, dirname, extra_args, rpchost, timewait, binary, | self._initialize_chain_clean() | ||||
stderr, self.mocktime, coverage_dir=self.options.coveragedir) | else: | ||||
node.start() | self._initialize_chain() | ||||
node.wait_for_rpc_connection() | |||||
if self.options.coveragedir is not None: | def setup_network(self): | ||||
coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc) | """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 | |||||
return node | # Public helper methods. These can be accessed by the subclass test scripts. | ||||
def start_nodes(self, num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None): | def add_nodes(self, num_nodes, extra_args=None, rpchost=None, timewait=None, binary=None): | ||||
"""Start multiple bitcoinds, return RPC connections to them""" | """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) | ||||
nodes = [] | |||||
try: | |||||
for i in range(num_nodes): | for i in range(num_nodes): | ||||
nodes.append(TestNode(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i], | self.nodes.append(TestNode(i, self.options.tmpdir, extra_args[i], rpchost, timewait=timewait, | ||||
stderr=None, mocktime=self.mocktime, coverage_dir=self.options.coveragedir)) | binary=binary[i], stderr=None, mocktime=self.mocktime, coverage_dir=self.options.coveragedir)) | ||||
nodes[i].start() | |||||
for node in nodes: | 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() | node.wait_for_rpc_connection() | ||||
except: | except: | ||||
# If one node failed to start, stop the others | # If one node failed to start, stop the others | ||||
self.stop_nodes() | self.stop_nodes() | ||||
raise | raise | ||||
if self.options.coveragedir is not None: | if self.options.coveragedir is not None: | ||||
for node in nodes: | for node in self.nodes: | ||||
coverage.write_all_rpc_commands( | coverage.write_all_rpc_commands( | ||||
self.options.coveragedir, node.rpc) | self.options.coveragedir, node.rpc) | ||||
return nodes | |||||
def stop_node(self, i): | def stop_node(self, i): | ||||
"""Stop a bitcoind test node""" | """Stop a bitcoind test node""" | ||||
self.nodes[i].stop_node() | self.nodes[i].stop_node() | ||||
while not self.nodes[i].is_node_stopped(): | while not self.nodes[i].is_node_stopped(): | ||||
time.sleep(0.1) | time.sleep(0.1) | ||||
def stop_nodes(self): | def stop_nodes(self): | ||||
"""Stop multiple bitcoind test nodes""" | """Stop multiple bitcoind test nodes""" | ||||
for node in self.nodes: | for node in self.nodes: | ||||
# Issue RPC to stop nodes | # Issue RPC to stop nodes | ||||
node.stop_node() | node.stop_node() | ||||
for node in self.nodes: | for node in self.nodes: | ||||
# Wait for nodes to stop | # Wait for nodes to stop | ||||
while not node.is_node_stopped(): | while not node.is_node_stopped(): | ||||
time.sleep(0.1) | 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: | with tempfile.SpooledTemporaryFile(max_size=2**16) as log_stderr: | ||||
try: | try: | ||||
self.start_node(i, dirname, extra_args, stderr=log_stderr) | self.start_node(i, extra_args, stderr=log_stderr) | ||||
self.stop_node(i) | self.stop_node(i) | ||||
except Exception as e: | except Exception as e: | ||||
assert 'bitcoind exited' in str(e) # node must have shutdown | assert 'bitcoind exited' in str(e) # node must have shutdown | ||||
self.nodes[i].running = False | self.nodes[i].running = False | ||||
self.nodes[i].process = None | self.nodes[i].process = None | ||||
if expected_msg is not None: | if expected_msg is not None: | ||||
log_stderr.seek(0) | log_stderr.seek(0) | ||||
stderr = log_stderr.read().decode('utf-8') | stderr = log_stderr.read().decode('utf-8') | ||||
▲ Show 20 Lines • Show All 78 Lines • ▼ Show 20 Lines | def _start_logging(self): | ||||
if self.options.trace_rpc: | if self.options.trace_rpc: | ||||
rpc_logger = logging.getLogger("BitcoinRPC") | rpc_logger = logging.getLogger("BitcoinRPC") | ||||
rpc_logger.setLevel(logging.DEBUG) | rpc_logger.setLevel(logging.DEBUG) | ||||
rpc_handler = logging.StreamHandler(sys.stdout) | rpc_handler = logging.StreamHandler(sys.stdout) | ||||
rpc_handler.setLevel(logging.DEBUG) | rpc_handler.setLevel(logging.DEBUG) | ||||
rpc_logger.addHandler(rpc_handler) | 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. | """Initialize a pre-mined blockchain for use by the test. | ||||
Create a cache of a 200-block-long chain (with wallet) for MAX_NODES | Create a cache of a 200-block-long chain (with wallet) for MAX_NODES | ||||
Afterward, create num_nodes copies from the cache.""" | Afterward, create num_nodes copies from the cache.""" | ||||
assert num_nodes <= MAX_NODES | assert self.num_nodes <= MAX_NODES | ||||
create_cache = False | create_cache = False | ||||
for i in range(MAX_NODES): | 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 | create_cache = True | ||||
break | break | ||||
if create_cache: | if create_cache: | ||||
self.log.debug("Creating data directories from cached datadir") | self.log.debug("Creating data directories from cached datadir") | ||||
# find and delete old cache directories if any exist | # find and delete old cache directories if any exist | ||||
for i in range(MAX_NODES): | for i in range(MAX_NODES): | ||||
if os.path.isdir(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(cachedir, "node" + str(i))) | shutil.rmtree(os.path.join( | ||||
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(cachedir, i) | datadir = initialize_datadir(self.options.cachedir, i) | ||||
args = [os.getenv("BITCOIND", "bitcoind"), "-server", | args = [os.getenv("BITCOIND", "bitcoind"), "-server", | ||||
"-keypool=1", "-datadir=" + datadir, "-discover=0"] | "-keypool=1", "-datadir=" + datadir, "-discover=0"] | ||||
if i > 0: | if i > 0: | ||||
args.append("-connect=127.0.0.1:" + str(p2p_port(0))) | args.append("-connect=127.0.0.1:" + str(p2p_port(0))) | ||||
self.nodes.append(TestNode(i, cachedir, extra_args=[], rpchost=None, timewait=None, | self.nodes.append(TestNode(i, self.options.cachedir, extra_args=[ | ||||
binary=None, stderr=None, mocktime=self.mocktime, coverage_dir=None)) | ], rpchost=None, timewait=None, binary=None, stderr=None, mocktime=self.mocktime, coverage_dir=None)) | ||||
self.nodes[i].args = args | self.nodes[i].args = args | ||||
self.nodes[i].start() | self.start_node(i) | ||||
# Wait for RPC connections to be ready | # Wait for RPC connections to be ready | ||||
for node in self.nodes: | for node in self.nodes: | ||||
node.wait_for_rpc_connection() | node.wait_for_rpc_connection() | ||||
# Create a 200-block-long chain; each of the 4 first nodes | # Create a 200-block-long chain; each of the 4 first nodes | ||||
# gets 25 mature blocks and 25 immature. | # gets 25 mature blocks and 25 immature. | ||||
# Note: To preserve compatibility with older versions of | # Note: To preserve compatibility with older versions of | ||||
Show All 12 Lines | def _initialize_chain(self): | ||||
# Must sync before next peer starts generating blocks | # Must sync before next peer starts generating blocks | ||||
sync_blocks(self.nodes) | sync_blocks(self.nodes) | ||||
# Shut them down, and clean up cache directories: | # Shut them down, and clean up cache directories: | ||||
self.stop_nodes() | self.stop_nodes() | ||||
self.nodes = [] | self.nodes = [] | ||||
self.disable_mocktime() | self.disable_mocktime() | ||||
for i in range(MAX_NODES): | for i in range(MAX_NODES): | ||||
os.remove(log_filename(cachedir, i, "debug.log")) | os.remove(log_filename(self.options.cachedir, i, "debug.log")) | ||||
os.remove(log_filename(cachedir, i, "db.log")) | os.remove(log_filename(self.options.cachedir, i, "db.log")) | ||||
os.remove(log_filename(cachedir, i, "peers.dat")) | os.remove(log_filename(self.options.cachedir, i, "peers.dat")) | ||||
os.remove(log_filename(cachedir, i, "fee_estimates.dat")) | os.remove(log_filename( | ||||
self.options.cachedir, i, "fee_estimates.dat")) | |||||
for i in range(num_nodes): | |||||
from_dir = os.path.join(cachedir, "node" + str(i)) | for i in range(self.num_nodes): | ||||
to_dir = os.path.join(test_dir, "node" + str(i)) | 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) | shutil.copytree(from_dir, to_dir) | ||||
# Overwrite port/rpcport in bitcoin.conf | # Overwrite port/rpcport in bitcoin.conf | ||||
initialize_datadir(test_dir, i) | initialize_datadir(self.options.tmpdir, i) | ||||
def _initialize_chain_clean(self, test_dir, num_nodes): | def _initialize_chain_clean(self): | ||||
"""Initialize empty blockchain for use by the test. | """Initialize empty blockchain for use by the test. | ||||
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(num_nodes): | for i in range(self.num_nodes): | ||||
initialize_datadir(test_dir, 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 __init__(self): | def set_test_params(self): | ||||
super().__init__() | |||||
self.num_nodes = 2 | self.num_nodes = 2 | ||||
self.setup_clean_chain = True | self.setup_clean_chain = True | ||||
def add_options(self, parser): | def add_options(self, parser): | ||||
parser.add_option("--testbinary", dest="testbinary", | parser.add_option("--testbinary", dest="testbinary", | ||||
default=os.getenv("BITCOIND", "bitcoind"), | default=os.getenv("BITCOIND", "bitcoind"), | ||||
help="bitcoind binary to test") | help="bitcoind binary to test") | ||||
parser.add_option("--refbinary", dest="refbinary", | parser.add_option("--refbinary", dest="refbinary", | ||||
default=os.getenv("BITCOIND", "bitcoind"), | default=os.getenv("BITCOIND", "bitcoind"), | ||||
help="bitcoind binary to use for reference nodes (if any)") | help="bitcoind binary to use for reference nodes (if any)") | ||||
def setup_network(self): | 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"): | if hasattr(self, "extra_args"): | ||||
extra_args = self.extra_args | extra_args = self.extra_args | ||||
self.nodes = self.start_nodes( | self.add_nodes(self.num_nodes, extra_args, | ||||
self.num_nodes, self.options.tmpdir, 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() | |||||
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 |