Changeset View
Changeset View
Standalone View
Standalone View
test/functional/test_framework/test_framework.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2014-2016 The Bitcoin Core developers | # Copyright (c) 2014-2016 The Bitcoin Core developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
from collections import deque | from collections import deque | ||||
import errno | |||||
from enum import Enum | from enum import Enum | ||||
import http.client | |||||
import logging | import logging | ||||
import optparse | import optparse | ||||
import os | import os | ||||
import shutil | import shutil | ||||
import subprocess | |||||
import sys | import sys | ||||
import tempfile | import tempfile | ||||
import time | import time | ||||
import traceback | import traceback | ||||
from .authproxy import JSONRPCException | from .authproxy import JSONRPCException | ||||
from . import coverage | from . import coverage | ||||
from .test_node import TestNode | |||||
from .util import ( | from .util import ( | ||||
MAX_NODES, | MAX_NODES, | ||||
PortSeed, | PortSeed, | ||||
assert_equal, | assert_equal, | ||||
check_json_precision, | check_json_precision, | ||||
connect_nodes_bi, | connect_nodes_bi, | ||||
disconnect_nodes, | disconnect_nodes, | ||||
get_rpc_proxy, | |||||
initialize_datadir, | initialize_datadir, | ||||
get_datadir_path, | |||||
log_filename, | log_filename, | ||||
p2p_port, | p2p_port, | ||||
rpc_url, | |||||
set_node_times, | set_node_times, | ||||
sync_blocks, | sync_blocks, | ||||
sync_mempools, | sync_mempools, | ||||
) | ) | ||||
class TestStatus(Enum): | class TestStatus(Enum): | ||||
PASSED = 1 | PASSED = 1 | ||||
Show All 23 Lines | class BitcoinTestFramework(): | ||||
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. | # Methods to override in subclass test scripts. | ||||
def __init__(self): | def __init__(self): | ||||
self.num_nodes = 4 | self.num_nodes = 4 | ||||
self.setup_clean_chain = False | self.setup_clean_chain = False | ||||
self.nodes = [] | self.nodes = [] | ||||
self.bitcoind_processes = {} | |||||
self.mocktime = 0 | self.mocktime = 0 | ||||
def add_options(self, parser): | def add_options(self, parser): | ||||
pass | pass | ||||
def setup_chain(self): | def setup_chain(self): | ||||
self.log.info("Initializing test directory " + self.options.tmpdir) | self.log.info("Initializing test directory " + self.options.tmpdir) | ||||
if self.setup_clean_chain: | if self.setup_clean_chain: | ||||
▲ Show 20 Lines • Show All 131 Lines • ▼ Show 20 Lines | def main(self): | ||||
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. | # Public helper methods. These can be accessed by the subclass test scripts. | ||||
def start_node(self, i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, stderr=None): | 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""" | """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: | if binary is None: | ||||
binary = os.getenv("BITCOIND", "bitcoind") | binary = os.getenv("BITCOIND", "bitcoind") | ||||
args = [binary, "-datadir=" + datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-logtimemicros", "-debug", | node = TestNode(i, dirname, extra_args, rpchost, timewait, binary, | ||||
"-debugexclude=libevent", "-debugexclude=leveldb", "-mocktime=" + str(self.mocktime), "-uacomment=testnode%d" % i] | stderr, self.mocktime, coverage_dir=self.options.coveragedir) | ||||
if extra_args is not None: | node.start() | ||||
args.extend(extra_args) | node.wait_for_rpc_connection() | ||||
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: | if self.options.coveragedir is not None: | ||||
coverage.write_all_rpc_commands(self.options.coveragedir, proxy) | coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc) | ||||
return proxy | return node | ||||
def start_nodes(self, num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None): | def start_nodes(self, num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None): | ||||
"""Start multiple bitcoinds, return RPC connections to them""" | """Start multiple bitcoinds, return RPC connections to them""" | ||||
if extra_args is None: | if extra_args is None: | ||||
extra_args = [None] * 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) | ||||
rpcs = [] | nodes = [] | ||||
try: | try: | ||||
for i in range(num_nodes): | for i in range(num_nodes): | ||||
rpcs.append(self.start_node( | nodes.append(TestNode(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i], | ||||
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: | except: | ||||
# If one node failed to start, stop the others | # 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.stop_nodes() | ||||
self.nodes = [] | |||||
raise | 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): | def stop_node(self, i): | ||||
"""Stop a bitcoind test node""" | """Stop a bitcoind test node""" | ||||
self.nodes[i].stop_node() | |||||
self.log.debug("Stopping node %d" % i) | while not self.nodes[i].is_node_stopped(): | ||||
try: | time.sleep(0.1) | ||||
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) | |||||
def stop_nodes(self): | def stop_nodes(self): | ||||
"""Stop multiple bitcoind test nodes""" | """Stop multiple bitcoind test nodes""" | ||||
for node in self.nodes: | |||||
for i in range(len(self.nodes)): | # Issue RPC to stop nodes | ||||
self.stop_node(i) | node.stop_node() | ||||
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, dirname, 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, dirname, 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].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') | ||||
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.bitcoind_processes[i].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. | ||||
""" | """ | ||||
disconnect_nodes(self.nodes[1], 2) | disconnect_nodes(self.nodes[1], 2) | ||||
disconnect_nodes(self.nodes[2], 1) | disconnect_nodes(self.nodes[2], 1) | ||||
self.sync_all([self.nodes[:2], self.nodes[2:]]) | self.sync_all([self.nodes[:2], self.nodes[2:]]) | ||||
▲ Show 20 Lines • Show All 86 Lines • ▼ Show 20 Lines | def _initialize_chain(self, test_dir, num_nodes, cachedir): | ||||
# 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(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.bitcoind_processes[i] = subprocess.Popen(args) | self.nodes.append(TestNode(i, cachedir, extra_args=[], rpchost=None, timewait=None, | ||||
self.log.debug( | binary=None, stderr=None, mocktime=self.mocktime, coverage_dir=None)) | ||||
"initialize_chain: bitcoind started, waiting for RPC to come up") | self.nodes[i].args = args | ||||
self._wait_for_bitcoind_start( | self.nodes[i].start() | ||||
self.bitcoind_processes[i], datadir, i) | |||||
self.log.debug("initialize_chain: RPC successfully started") | # Wait for RPC connections to be ready | ||||
for node in self.nodes: | |||||
self.nodes = [] | node.wait_for_rpc_connection() | ||||
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) | |||||
# 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 | ||||
# initialize_chain, only 4 nodes will generate coins. | # initialize_chain, only 4 nodes will generate coins. | ||||
# | # | ||||
# blocks are created with timestamps 10 minutes apart | # blocks are created with timestamps 10 minutes apart | ||||
# starting from 2010 minutes in the past | # starting from 2010 minutes in the past | ||||
Show All 28 Lines | class BitcoinTestFramework(): | ||||
def _initialize_chain_clean(self, test_dir, num_nodes): | def _initialize_chain_clean(self, test_dir, num_nodes): | ||||
"""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(num_nodes): | ||||
initialize_datadir(test_dir, i) | 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): | 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""" | ||||
Show All 29 Lines |