Changeset View
Changeset View
Standalone View
Standalone View
test/functional/test_framework/test_node.py
Show First 20 Lines • Show All 50 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, 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.stdout_dir = os.path.join(self.datadir, "stdout") | ||||
self.stderr_dir = os.path.join(self.datadir, "stderr") | 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) | ||||
▲ Show 20 Lines • Show All 105 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, stdout=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 | # Add a new stdout and stderr file each time bitcoind is started | ||||
if stderr is None: | if stderr is None: | ||||
stderr = tempfile.NamedTemporaryFile( | stderr = tempfile.NamedTemporaryFile( | ||||
dir=self.stderr_dir, delete=False) | dir=self.stderr_dir, delete=False) | ||||
if stdout is None: | if stdout is None: | ||||
stdout = tempfile.NamedTemporaryFile( | stdout = tempfile.NamedTemporaryFile( | ||||
dir=self.stdout_dir, delete=False) | dir=self.stdout_dir, delete=False) | ||||
self.stderr = stderr | self.stderr = stderr | ||||
self.stdout = stdout | 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 | # 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") | subp_env = dict(os.environ, LIBC_FATAL_STDERR_="1") | ||||
self.process = subprocess.Popen( | self.process = subprocess.Popen( | ||||
[self.binary] + self.default_args + extra_args, env=subp_env, stdout=stdout, stderr=stderr, *args, **kwargs) | [self.binary] + self.default_args + extra_args, env=subp_env, stdout=stdout, 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): | ||||
if self.process.poll() is not None: | if self.process.poll() is not None: | ||||
raise FailedToStartError(self._node_msg( | 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: | ||||
rpc = get_rpc_proxy(rpc_url(self.datadir, self.host, self.rpc_port), | 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) | ||||
rpc.getblockcount() | 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.log.debug("RPC successfully started") | self.log.debug("RPC successfully started") | ||||
if self.use_cli: | if self.use_cli: | ||||
return | return | ||||
self.rpc = rpc | self.rpc = rpc | ||||
self.rpc_connected = True | self.rpc_connected = True | ||||
self.url = self.rpc.url | self.url = self.rpc.url | ||||
return | return | ||||
except IOError as e: | except IOError as e: | ||||
▲ Show 20 Lines • Show All 73 Lines • ▼ Show 20 Lines | def assert_debug_log(self, expected_msgs): | ||||
try: | try: | ||||
yield | yield | ||||
finally: | finally: | ||||
with open(debug_log, encoding='utf-8') as dl: | with open(debug_log, encoding='utf-8') as dl: | ||||
dl.seek(prev_size) | dl.seek(prev_size) | ||||
log = dl.read() | log = dl.read() | ||||
print_log = " - " + "\n - ".join(log.splitlines()) | print_log = " - " + "\n - ".join(log.splitlines()) | ||||
for expected_msg in expected_msgs: | for expected_msg in expected_msgs: | ||||
if re.search(re.escape(expected_msg), log, flags=re.MULTILINE) is None: | if re.search(re.escape(expected_msg), log, | ||||
flags=re.MULTILINE) is None: | |||||
self._raise_assertion_error( | self._raise_assertion_error( | ||||
'Expected message "{}" does not partially match log:\n\n{}\n\n'.format(expected_msg, print_log)) | 'Expected message "{}" does not partially match log:\n\n{}\n\n'.format(expected_msg, print_log)) | ||||
def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, match=ErrorMatch.FULL_TEXT, *args, **kwargs): | def assert_start_raises_init_error( | ||||
self, extra_args=None, expected_msg=None, match=ErrorMatch.FULL_TEXT, *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.NamedTemporaryFile(dir=self.stderr_dir, delete=False) 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: | tempfile.NamedTemporaryFile(dir=self.stdout_dir, delete=False) as log_stdout: | ||||
try: | try: | ||||
self.start(extra_args, stdout=log_stdout, | self.start(extra_args, stdout=log_stdout, | ||||
stderr=log_stderr, *args, **kwargs) | 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 | ||||
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: | |||||
self._raise_assertion_error( | 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: | ||||
self._raise_assertion_error( | 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: | ||||
▲ Show 20 Lines • Show All 147 Lines • Show Last 20 Lines |