diff options
author | James O'Beirne <james.obeirne@pm.me> | 2021-10-15 17:29:48 -0400 |
---|---|---|
committer | James O'Beirne <james.obeirne@pm.me> | 2021-10-26 12:46:36 -0400 |
commit | d9803f7a0a33688f7429cf10384244f4770851ca (patch) | |
tree | d9e122f2d82a132c2fced62f6b66691520afc970 /test/functional/feature_init.py | |
parent | 23f85616a8d9c9a1b054e492eca4d199028f34dc (diff) |
test: add stress tests for initialization
Diffstat (limited to 'test/functional/feature_init.py')
-rwxr-xr-x | test/functional/feature_init.py | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py new file mode 100755 index 0000000000..cffbd40271 --- /dev/null +++ b/test/functional/feature_init.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Stress tests related to node initialization.""" +import random +import time +import os +from pathlib import Path + +from test_framework.test_framework import BitcoinTestFramework, SkipTest +from test_framework.test_node import ErrorMatch +from test_framework.util import assert_equal + + +class InitStressTest(BitcoinTestFramework): + """ + Ensure that initialization can be interrupted at a number of points and not impair + subsequent starts. + """ + + def set_test_params(self): + self.setup_clean_chain = False + self.num_nodes = 1 + + def run_test(self): + """ + - test terminating initialization after seeing a certain log line. + - test terminating init after seeing a random number of log lines. + - test removing certain essential files to test startup error paths. + """ + # TODO: skip Windows for now since it isn't clear how to SIGTERM. + # + # Windows doesn't support `process.terminate()`. + # and other approaches (like below) don't work: + # + # os.kill(node.process.pid, signal.CTRL_C_EVENT) + if os.name == 'nt': + raise SkipTest("can't SIGTERM on Windows") + + self.stop_node(0) + node = self.nodes[0] + + def sigterm_node(): + node.process.terminate() + node.process.wait() + node.debug_log_path.unlink() + node.debug_log_path.touch() + + def check_clean_start(): + """Ensure that node restarts successfully after various interrupts.""" + # TODO: add -txindex=1 to fully test index initiatlization. + # See https://github.com/bitcoin/bitcoin/pull/23289#discussion_r735159180 for + # a discussion of the related bug. + node.start() + node.wait_for_rpc_connection() + assert_equal(200, node.getblockcount()) + + lines_to_terminate_after = [ + 'scheduler thread start', + 'Loading P2P addresses', + 'Loading banlist', + 'Loading block index', + 'Switching active chainstate', + 'Loaded best chain:', + 'init message: Verifying blocks', + 'loadblk thread start', + # TODO: reenable - see above TODO + # 'txindex thread start', + 'net thread start', + 'addcon thread start', + 'msghand thread start', + ] + if self.is_wallet_compiled(): + lines_to_terminate_after.append('Verifying wallet') + + for terminate_line in lines_to_terminate_after: + self.log.info(f"Starting node and will exit after line '{terminate_line}'") + node.start( + # TODO: add -txindex=1 to fully test index initiatlization. + # extra_args=['-txindex=1'], + ) + logfile = open(node.debug_log_path, 'r', encoding='utf8') + + MAX_SECS_TO_WAIT = 30 + start = time.time() + num_lines = 0 + + while True: + line = logfile.readline() + if line: + num_lines += 1 + + if line and terminate_line.lower() in line.lower(): + self.log.debug(f"Terminating node after {num_lines} log lines seen") + sigterm_node() + break + + if (time.time() - start) > MAX_SECS_TO_WAIT: + raise AssertionError( + f"missed line {terminate_line}; terminating now after {num_lines} lines") + + if node.process.poll() is not None: + raise AssertionError(f"node failed to start (line: '{terminate_line}')") + + check_clean_start() + num_total_logs = len(node.debug_log_path.read_text().splitlines()) + self.stop_node(0) + + self.log.info( + f"Terminate at some random point in the init process (max logs: {num_total_logs})") + + for _ in range(40): + terminate_after = random.randint(1, num_total_logs) + self.log.debug(f"Starting node and will exit after {terminate_after} lines") + node.start( + # TODO: add -txindex=1 to fully test index initiatlization. + # extra_args=['-txindex=1'], + ) + logfile = open(node.debug_log_path, 'r', encoding='utf8') + + MAX_SECS_TO_WAIT = 10 + start = time.time() + num_lines = 0 + + while True: + line = logfile.readline() + if line: + num_lines += 1 + + if num_lines >= terminate_after or (time.time() - start) > MAX_SECS_TO_WAIT: + self.log.debug(f"Terminating node after {num_lines} log lines seen") + sigterm_node() + break + + if node.process.poll() is not None: + raise AssertionError("node failed to start") + + check_clean_start() + self.stop_node(0) + + self.log.info("Test startup errors after removing certain essential files") + + files_to_disturb = { + 'blocks/index/*.ldb': 'Error opening block database.', + 'chainstate/*.ldb': 'Error opening block database.', + 'blocks/blk*.dat': 'Error loading block database.', + } + + for file_patt, err_fragment in files_to_disturb.items(): + target_file = list(node.chain_path.glob(file_patt))[0] + + self.log.info(f"Tweaking file to ensure failure {target_file}") + bak_path = str(target_file) + ".bak" + target_file.rename(bak_path) + + # TODO: at some point, we should test perturbing the files instead of removing + # them, e.g. + # + # contents = target_file.read_bytes() + # tweaked_contents = bytearray(contents) + # tweaked_contents[50:250] = b'1' * 200 + # target_file.write_bytes(bytes(tweaked_contents)) + # + # At the moment I can't get this to work (bitcoind loads successfully?) so + # investigate doing this later. + + node.assert_start_raises_init_error( + # TODO: add -txindex=1 to fully test index initiatlization. + # extra_args=['-txindex=1'], + expected_msg=err_fragment, + match=ErrorMatch.PARTIAL_REGEX, + ) + + self.log.info(f"Restoring file from {bak_path} and restarting") + Path(bak_path).rename(target_file) + check_clean_start() + self.stop_node(0) + + +if __name__ == '__main__': + InitStressTest().main() |