Changeset View
Changeset View
Standalone View
Standalone View
test/functional/test_framework/test_node.py
Show First 20 Lines • Show All 45 Lines • ▼ Show 20 Lines | class TestNode(): | ||||
- state about the node (whether it's running, etc) | - state about the node (whether it's running, etc) | ||||
- a Python subprocess.Popen object representing the running process | - a Python subprocess.Popen object representing the running process | ||||
- an RPC connection to the node | - an RPC connection to the node | ||||
- one or more P2P connections to the node | - one or more P2P connections to the node | ||||
To make things easier for the test writer, any unrecognised messages will | To make things easier for the test writer, any unrecognised messages will | ||||
be dispatched to the RPC connection.""" | be dispatched to the RPC connection.""" | ||||
def __init__(self, i, datadir, host, rpc_port, p2p_port, timewait, bitcoind, bitcoin_cli, stderr, mocktime, coverage_dir, extra_conf=None, extra_args=None, use_cli=False): | 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.index = i | self.index = i | ||||
self.datadir = datadir | self.datadir = datadir | ||||
self.stdout_dir = os.path.join(self.datadir, "stdout") | |||||
self.stderr_dir = os.path.join(self.datadir, "stderr") | |||||
self.host = host | self.host = host | ||||
self.rpc_port = rpc_port | self.rpc_port = rpc_port | ||||
self.p2p_port = p2p_port | self.p2p_port = p2p_port | ||||
self.name = "testnode-{}".format(i) | self.name = "testnode-{}".format(i) | ||||
if timewait: | if timewait: | ||||
self.rpc_timeout = timewait | self.rpc_timeout = timewait | ||||
else: | else: | ||||
# Wait for up to 60 seconds for the RPC server to respond | # Wait for up to 60 seconds for the RPC server to respond | ||||
self.rpc_timeout = 60 | self.rpc_timeout = 60 | ||||
self.binary = bitcoind | self.binary = bitcoind | ||||
if not os.path.isfile(self.binary): | if not os.path.isfile(self.binary): | ||||
raise FileNotFoundError( | raise FileNotFoundError( | ||||
"Binary '{}' could not be found.\nTry setting it manually:\n\tBITCOIND=<path/to/bitcoind> {}".format(self.binary, sys.argv[0])) | "Binary '{}' could not be found.\nTry setting it manually:\n\tBITCOIND=<path/to/bitcoind> {}".format(self.binary, sys.argv[0])) | ||||
self.stderr = stderr | |||||
self.coverage_dir = coverage_dir | self.coverage_dir = coverage_dir | ||||
if extra_conf != None: | if extra_conf != None: | ||||
append_config(datadir, extra_conf) | append_config(datadir, extra_conf) | ||||
# Most callers will just need to add extra args to the default list | # Most callers will just need to add extra args to the default list | ||||
# below. | # below. | ||||
# For those callers that need more flexibity, they can access the | # For those callers that need more flexibity, they can access the | ||||
# default args using the provided facilities | # default args using the provided facilities | ||||
self.extra_args = extra_args | self.extra_args = extra_args | ||||
▲ Show 20 Lines • Show All 47 Lines • ▼ Show 20 Lines | def remove_default_args(self, args): | ||||
# Remove all occurrences of rm_arg in self.default_args: | # Remove all occurrences of rm_arg in self.default_args: | ||||
# - if the arg is a flag (-flag), then the names must match | # - if the arg is a flag (-flag), then the names must match | ||||
# - if the arg is a value (-key=value) then the name must starts | # - if the arg is a value (-key=value) then the name must starts | ||||
# with "-key=" (the '"' char is to avoid removing "-key_suffix" | # with "-key=" (the '"' char is to avoid removing "-key_suffix" | ||||
# arg is "-key" is the argument to remove). | # arg is "-key" is the argument to remove). | ||||
self.default_args = [def_arg for def_arg in self.default_args | self.default_args = [def_arg for def_arg in self.default_args | ||||
if rm_arg != def_arg and not def_arg.startswith(rm_arg + '=')] | if rm_arg != def_arg and not def_arg.startswith(rm_arg + '=')] | ||||
def start(self, extra_args=None, stderr=None, *args, **kwargs): | def start(self, extra_args=None, stdout=None, stderr=None, *args, **kwargs): | ||||
"""Start the node.""" | """Start the node.""" | ||||
if extra_args is None: | if extra_args is None: | ||||
extra_args = self.extra_args | extra_args = self.extra_args | ||||
# Add a new stdout and stderr file each time bitcoind is started | |||||
if stderr is None: | if stderr is None: | ||||
stderr = self.stderr | stderr = tempfile.NamedTemporaryFile( | ||||
dir=self.stderr_dir, delete=False) | |||||
if stdout is None: | |||||
stdout = tempfile.NamedTemporaryFile( | |||||
dir=self.stdout_dir, delete=False) | |||||
self.stderr = stderr | |||||
self.stdout = stdout | |||||
# Delete any existing cookie file -- if such a file exists (eg due to | # Delete any existing cookie file -- if such a file exists (eg due to | ||||
# unclean shutdown), it will get overwritten anyway by bitcoind, and | # unclean shutdown), it will get overwritten anyway by bitcoind, and | ||||
# potentially interfere with our attempt to authenticate | # potentially interfere with our attempt to authenticate | ||||
delete_cookie_file(self.datadir) | delete_cookie_file(self.datadir) | ||||
# add environment variable LIBC_FATAL_STDERR_=1 so that libc errors are written to stderr and not the terminal | |||||
subp_env = dict(os.environ, LIBC_FATAL_STDERR_="1") | |||||
self.process = subprocess.Popen( | self.process = subprocess.Popen( | ||||
[self.binary] + self.default_args + extra_args, | [self.binary] + self.default_args + extra_args, env=subp_env, stdout=stdout, stderr=stderr, *args, **kwargs) | ||||
stderr=stderr, *args, **kwargs) | |||||
self.running = True | self.running = True | ||||
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): | ||||
Show All 25 Lines | 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_connected | ||||
assert self.rpc | assert self.rpc | ||||
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): | 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") | ||||
try: | try: | ||||
self.stop() | self.stop() | ||||
except http.client.CannotSendRequest: | except http.client.CannotSendRequest: | ||||
self.log.exception("Unable to stop node.") | self.log.exception("Unable to stop node.") | ||||
# Check that stderr is as expected | |||||
self.stderr.seek(0) | |||||
stderr = self.stderr.read().decode('utf-8').strip() | |||||
if stderr != expected_stderr: | |||||
raise AssertionError( | |||||
"Unexpected stderr {} != {}".format(stderr, expected_stderr)) | |||||
del self.p2ps[:] | del self.p2ps[:] | ||||
def is_node_stopped(self): | def is_node_stopped(self): | ||||
"""Checks whether the node has stopped. | """Checks whether the node has stopped. | ||||
Returns True if the node has stopped. False otherwise. | Returns True if the node has stopped. False otherwise. | ||||
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: | ||||
Show All 17 Lines | class TestNode(): | ||||
def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, partial_match=False, *args, **kwargs): | def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, partial_match=False, *args, **kwargs): | ||||
"""Attempt to start the node and expect it to raise an error. | """Attempt to start the node and expect it to raise an error. | ||||
extra_args: extra arguments to pass through to bitcoind | extra_args: extra arguments to pass through to bitcoind | ||||
expected_msg: regex that stderr should match when bitcoind fails | expected_msg: regex that stderr should match when bitcoind fails | ||||
Will throw if bitcoind starts without an error. | Will throw if bitcoind starts without an error. | ||||
Will throw if an expected_msg is provided and it does not match bitcoind's stdout.""" | Will throw if an expected_msg is provided and it does not match bitcoind's stdout.""" | ||||
with tempfile.SpooledTemporaryFile(max_size=2**16) as log_stderr: | with tempfile.NamedTemporaryFile(dir=self.stderr_dir, delete=False) as log_stderr, \ | ||||
tempfile.NamedTemporaryFile(dir=self.stdout_dir, delete=False) as log_stdout: | |||||
try: | try: | ||||
self.start(extra_args, stderr=log_stderr, *args, **kwargs) | self.start(extra_args, stdout=log_stdout, | ||||
stderr=log_stderr, *args, **kwargs) | |||||
self.wait_for_rpc_connection() | self.wait_for_rpc_connection() | ||||
self.stop_node() | self.stop_node() | ||||
self.wait_until_stopped() | self.wait_until_stopped() | ||||
except FailedToStartError as e: | except FailedToStartError as e: | ||||
self.log.debug('bitcoind failed to start: {}'.format(e)) | self.log.debug('bitcoind failed to start: {}'.format(e)) | ||||
self.running = False | self.running = False | ||||
self.process = None | self.process = None | ||||
# Check stderr for expected message | # Check stderr for expected message | ||||
▲ Show 20 Lines • Show All 150 Lines • Show Last 20 Lines |