Changeset View
Changeset View
Standalone View
Standalone View
test/functional/test_framework/test_node.py
Show All 16 Lines | |||||
import sys | import sys | ||||
import tempfile | import tempfile | ||||
import time | import time | ||||
from .authproxy import JSONRPCException | from .authproxy import JSONRPCException | ||||
from .messages import COIN, CTransaction, FromHex | from .messages import COIN, CTransaction, FromHex | ||||
from .util import ( | from .util import ( | ||||
append_config, | append_config, | ||||
assert_equal, | |||||
delete_cookie_file, | delete_cookie_file, | ||||
get_rpc_proxy, | get_rpc_proxy, | ||||
p2p_port, | p2p_port, | ||||
rpc_url, | rpc_url, | ||||
wait_until, | wait_until, | ||||
) | ) | ||||
# For Python 3.4 compatibility | # For Python 3.4 compatibility | ||||
▲ Show 20 Lines • Show All 68 Lines • ▼ Show 20 Lines | def __init__(self, i, datadir, host, rpc_port, p2p_port, timewait, bitcoind, bitcoin_cli, mocktime, coverage_dir, extra_conf=None, extra_args=None, use_cli=False): | ||||
self.rpc = None | self.rpc = None | ||||
self.url = None | self.url = None | ||||
self.relay_fee_cache = None | self.relay_fee_cache = None | ||||
self.log = logging.getLogger('TestFramework.node{}'.format(i)) | self.log = logging.getLogger('TestFramework.node{}'.format(i)) | ||||
# Whether to kill the node when this object goes away | # Whether to kill the node when this object goes away | ||||
self.cleanup_on_exit = True | self.cleanup_on_exit = True | ||||
self.p2ps = [] | self.p2ps = [] | ||||
def _node_msg(self, msg: str) -> str: | |||||
"""Return a modified msg that identifies this node by its index as a debugging aid.""" | |||||
return "[node {}] {}".format(self.index, msg) | |||||
def _raise_assertion_error(self, msg: str): | |||||
"""Raise an AssertionError with msg modified to identify this node.""" | |||||
raise AssertionError(self._node_msg(msg)) | |||||
def __del__(self): | def __del__(self): | ||||
# Ensure that we don't leave any bitcoind processes lying around after | # Ensure that we don't leave any bitcoind processes lying around after | ||||
# the test ends | # the test ends | ||||
if self.process and self.cleanup_on_exit: | if self.process and self.cleanup_on_exit: | ||||
# Should only happen on test failure | # Should only happen on test failure | ||||
# Avoid using logger, as that may have already been shutdown when | # Avoid using logger, as that may have already been shutdown when | ||||
# this destructor is called. | # this destructor is called. | ||||
print("Cleaning up leftover process") | print(self._node_msg("Cleaning up leftover process")) | ||||
self.process.kill() | self.process.kill() | ||||
def __getattr__(self, name): | def __getattr__(self, name): | ||||
"""Dispatches any unrecognised messages to the RPC connection or a CLI instance.""" | """Dispatches any unrecognised messages to the RPC connection or a CLI instance.""" | ||||
if self.use_cli: | if self.use_cli: | ||||
return getattr(self.cli, name) | return getattr(self.cli, name) | ||||
else: | else: | ||||
assert self.rpc is not None, "Error: RPC not initialized" | assert self.rpc is not None, self._node_msg( | ||||
assert self.rpc_connected, "Error: No RPC connection" | "Error: RPC not initialized") | ||||
assert self.rpc_connected, self._node_msg( | |||||
"Error: No RPC connection") | |||||
return getattr(self.rpc, name) | return getattr(self.rpc, name) | ||||
def clear_default_args(self): | def clear_default_args(self): | ||||
self.default_args.clear() | self.default_args.clear() | ||||
def extend_default_args(self, args): | def extend_default_args(self, args): | ||||
self.default_args.extend(args) | self.default_args.extend(args) | ||||
Show All 37 Lines | def start(self, extra_args=None, stdout=None, stderr=None, *args, **kwargs): | ||||
self.log.debug("bitcoind started, waiting for RPC to come up") | self.log.debug("bitcoind started, waiting for RPC to come up") | ||||
def wait_for_rpc_connection(self): | def wait_for_rpc_connection(self): | ||||
"""Sets up an RPC connection to the bitcoind process. Returns False if unable to connect.""" | """Sets up an RPC connection to the bitcoind process. Returns False if unable to connect.""" | ||||
# Poll at a rate of four times per second | # Poll at a rate of four times per second | ||||
poll_per_s = 4 | poll_per_s = 4 | ||||
for _ in range(poll_per_s * self.rpc_timeout): | for _ in range(poll_per_s * self.rpc_timeout): | ||||
if self.process.poll() is not None: | if self.process.poll() is not None: | ||||
raise FailedToStartError( | raise FailedToStartError(self._node_msg( | ||||
'bitcoind exited with status {} during initialization'.format(self.process.returncode)) | 'bitcoind exited with status {} during initialization'.format(self.process.returncode))) | ||||
try: | try: | ||||
self.rpc = get_rpc_proxy(rpc_url(self.datadir, self.host, self.rpc_port), | self.rpc = get_rpc_proxy(rpc_url(self.datadir, self.host, self.rpc_port), | ||||
self.index, timeout=self.rpc_timeout, coveragedir=self.coverage_dir) | self.index, timeout=self.rpc_timeout, coveragedir=self.coverage_dir) | ||||
self.rpc.getblockcount() | self.rpc.getblockcount() | ||||
# If the call to getblockcount() succeeds then the RPC connection is up | # If the call to getblockcount() succeeds then the RPC connection is up | ||||
self.rpc_connected = True | self.rpc_connected = True | ||||
self.url = self.rpc.url | self.url = self.rpc.url | ||||
self.log.debug("RPC successfully started") | self.log.debug("RPC successfully started") | ||||
return | return | ||||
except IOError as e: | except IOError as e: | ||||
if e.errno != errno.ECONNREFUSED: # Port not yet open? | if e.errno != errno.ECONNREFUSED: # Port not yet open? | ||||
raise # unknown IO error | raise # unknown IO error | ||||
except JSONRPCException as e: # Initialization phase | except JSONRPCException as e: # Initialization phase | ||||
if e.error['code'] != -28: # RPC in warmup? | if e.error['code'] != -28: # RPC in warmup? | ||||
raise # unknown JSON RPC exception | raise # unknown JSON RPC exception | ||||
except ValueError as e: # cookie file not found and no rpcuser or rpcassword. bitcoind still starting | except ValueError as e: # cookie file not found and no rpcuser or rpcassword. bitcoind still starting | ||||
if "No RPC credentials" not in str(e): | if "No RPC credentials" not in str(e): | ||||
raise | raise | ||||
time.sleep(1.0 / poll_per_s) | time.sleep(1.0 / poll_per_s) | ||||
raise AssertionError("Unable to connect to bitcoind") | self._raise_assertion_error("Unable to connect to bitcoind") | ||||
def get_wallet_rpc(self, wallet_name): | def get_wallet_rpc(self, wallet_name): | ||||
if self.use_cli: | if self.use_cli: | ||||
return self.cli("-rpcwallet={}".format(wallet_name)) | return self.cli("-rpcwallet={}".format(wallet_name)) | ||||
else: | else: | ||||
assert self.rpc_connected | assert self.rpc is not None, self._node_msg( | ||||
assert self.rpc | "Error: RPC not initialized") | ||||
assert self.rpc_connected, self._node_msg( | |||||
"Error: RPC not connected") | |||||
wallet_path = "wallet/{}".format(wallet_name) | wallet_path = "wallet/{}".format(wallet_name) | ||||
return self.rpc / wallet_path | return self.rpc / wallet_path | ||||
def stop_node(self, expected_stderr=''): | def stop_node(self, expected_stderr=''): | ||||
"""Stop the node.""" | """Stop the node.""" | ||||
if not self.running: | if not self.running: | ||||
return | return | ||||
self.log.debug("Stopping node") | self.log.debug("Stopping node") | ||||
Show All 18 Lines | def is_node_stopped(self): | ||||
This method is responsible for freeing resources (self.process).""" | This method is responsible for freeing resources (self.process).""" | ||||
if not self.running: | if not self.running: | ||||
return True | return True | ||||
return_code = self.process.poll() | return_code = self.process.poll() | ||||
if return_code is None: | if return_code is None: | ||||
return False | return False | ||||
# process has stopped. Assert that it didn't return an error code. | # process has stopped. Assert that it didn't return an error code. | ||||
assert_equal(return_code, 0) | assert return_code == 0, self._node_msg( | ||||
"Node returned non-zero exit code ({}) when stopping".format(return_code)) | |||||
self.running = False | self.running = False | ||||
self.process = None | self.process = None | ||||
self.rpc_connected = False | self.rpc_connected = False | ||||
self.rpc = None | self.rpc = None | ||||
self.log.debug("Node stopped") | self.log.debug("Node stopped") | ||||
return True | return True | ||||
def wait_until_stopped(self, timeout=BITCOIND_PROC_WAIT_TIMEOUT): | def wait_until_stopped(self, timeout=BITCOIND_PROC_WAIT_TIMEOUT): | ||||
Show All 38 Lines | def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, match=ErrorMatch.FULL_TEXT, *args, **kwargs): | ||||
self.running = False | self.running = False | ||||
self.process = None | self.process = None | ||||
# Check stderr for expected message | # Check stderr for expected message | ||||
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').strip() | stderr = log_stderr.read().decode('utf-8').strip() | ||||
if match == ErrorMatch.PARTIAL_REGEX: | if match == ErrorMatch.PARTIAL_REGEX: | ||||
if re.search(expected_msg, stderr, flags=re.MULTILINE) is None: | if re.search(expected_msg, stderr, flags=re.MULTILINE) is None: | ||||
raise AssertionError( | self._raise_assertion_error( | ||||
'Expected message "{}" does not partially match stderr:\n"{}"'.format(expected_msg, stderr)) | 'Expected message "{}" does not partially match stderr:\n"{}"'.format(expected_msg, stderr)) | ||||
elif match == ErrorMatch.FULL_REGEX: | elif match == ErrorMatch.FULL_REGEX: | ||||
if re.fullmatch(expected_msg, stderr) is None: | if re.fullmatch(expected_msg, stderr) is None: | ||||
raise AssertionError( | self._raise_assertion_error( | ||||
'Expected message "{}" does not fully match stderr:\n"{}"'.format(expected_msg, stderr)) | 'Expected message "{}" does not fully match stderr:\n"{}"'.format(expected_msg, stderr)) | ||||
elif match == ErrorMatch.FULL_TEXT: | elif match == ErrorMatch.FULL_TEXT: | ||||
if expected_msg != stderr: | if expected_msg != stderr: | ||||
raise AssertionError( | self._raise_assertion_error( | ||||
'Expected message "{}" does not fully match stderr:\n"{}"'.format(expected_msg, stderr)) | 'Expected message "{}" does not fully match stderr:\n"{}"'.format(expected_msg, 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) | self._raise_assertion_error(assert_msg) | ||||
def node_encrypt_wallet(self, passphrase): | def node_encrypt_wallet(self, passphrase): | ||||
""""Encrypts the wallet. | """"Encrypts the wallet. | ||||
This causes bitcoind to shutdown, so this method takes | This causes bitcoind to shutdown, so this method takes | ||||
care of cleaning up resources.""" | care of cleaning up resources.""" | ||||
self.encryptwallet(passphrase) | self.encryptwallet(passphrase) | ||||
self.wait_until_stopped() | self.wait_until_stopped() | ||||
Show All 37 Lines | def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, **kwargs): | ||||
return p2p_conn | return p2p_conn | ||||
@property | @property | ||||
def p2p(self): | def p2p(self): | ||||
"""Return the first p2p connection | """Return the first p2p connection | ||||
Convenience property - most tests only use a single p2p connection to each | Convenience property - most tests only use a single p2p connection to each | ||||
node, so this saves having to write node.p2ps[0] many times.""" | node, so this saves having to write node.p2ps[0] many times.""" | ||||
assert self.p2ps, "No p2p connection" | assert self.p2ps, self._node_msg("No p2p connection") | ||||
return self.p2ps[0] | return self.p2ps[0] | ||||
def disconnect_p2ps(self): | def disconnect_p2ps(self): | ||||
"""Close all p2p connections to the node.""" | """Close all p2p connections to the node.""" | ||||
for p in self.p2ps: | for p in self.p2ps: | ||||
p.peer_disconnect() | p.peer_disconnect() | ||||
del self.p2ps[:] | del self.p2ps[:] | ||||
▲ Show 20 Lines • Show All 75 Lines • Show Last 20 Lines |