Skip to content

Commit d69af93

Browse files
author
MarcoFalke
committed
Merge bitcoin/bitcoin#23737: test: make feature_init more robust
8904f17 test: enable txindex in feature_init (James O'Beirne) 93db6d8 test: feature_init: retain debug.log and improve detection (James O'Beirne) 24fcf6e test: feature_init: tweak all .ldb files of a certain type (James O'Beirne) a8ffbc0 test: add TestNode.wait_for_debug_log (James O'Beirne) a2fb62b test: introduce TestNode.debug_log_bytes (James O'Beirne) Pull request description: Hopefully fixes #23646. This makes a few changes to feature_init (along the lines of the discussion here: bitcoin/bitcoin#23289 (comment)). The changes are detailed in the specific commits, but a summary might be: - more robust log-line detection (at the expense of maybe being slower) - retain all debug.log content - perturb .ldb files in a more complete way Top commit has no ACKs. Tree-SHA512: 9c8900998fdb96d3cc92bd30c4eeda2a47e9787e6432546d458c17f24eb9e2b6b3eb573db225172888ca71d7a62260f497f83827597b982b9e35c5452cb6e514
2 parents 31f385c + 8904f17 commit d69af93

File tree

2 files changed

+67
-52
lines changed

2 files changed

+67
-52
lines changed

test/functional/feature_init.py

Lines changed: 25 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,9 @@ def run_test(self):
4444
def sigterm_node():
4545
node.process.terminate()
4646
node.process.wait()
47-
node.debug_log_path.unlink()
48-
node.debug_log_path.touch()
4947

5048
def check_clean_start():
5149
"""Ensure that node restarts successfully after various interrupts."""
52-
# TODO: add -txindex=1 to fully test index initiatlization.
53-
# See https://github.com/bitcoin/bitcoin/pull/23289#discussion_r735159180 for
54-
# a discussion of the related bug.
5550
node.start()
5651
node.wait_for_rpc_connection()
5752
assert_equal(200, node.getblockcount())
@@ -71,56 +66,33 @@ def check_clean_start():
7166
'net thread start',
7267
'addcon thread start',
7368
'loadblk thread start',
74-
# TODO: reenable - see above TODO
75-
# 'txindex thread start',
76-
'msghand thread start'
69+
'txindex thread start',
70+
'msghand thread start',
71+
'net thread start',
72+
'addcon thread start',
7773
]
7874
if self.is_wallet_compiled():
7975
lines_to_terminate_after.append('Verifying wallet')
8076

8177
for terminate_line in lines_to_terminate_after:
8278
self.log.info(f"Starting node and will exit after line '{terminate_line}'")
83-
node.start(
84-
# TODO: add -txindex=1 to fully test index initiatlization.
85-
# extra_args=['-txindex=1'],
86-
)
87-
logfile = open(node.debug_log_path, 'r', encoding='utf8')
88-
89-
MAX_SECS_TO_WAIT = 30
90-
start = time.time()
91-
num_lines = 0
92-
93-
while True:
94-
line = logfile.readline()
95-
if line:
96-
num_lines += 1
97-
98-
if line and terminate_line.lower() in line.lower():
99-
self.log.debug(f"Terminating node after {num_lines} log lines seen")
100-
sigterm_node()
101-
break
102-
103-
if (time.time() - start) > MAX_SECS_TO_WAIT:
104-
raise AssertionError(
105-
f"missed line {terminate_line}; terminating now after {num_lines} lines")
79+
node.start(extra_args=['-txindex=1'])
10680

107-
if node.process.poll() is not None:
108-
raise AssertionError(f"node failed to start (line: '{terminate_line}')")
81+
num_total_logs = node.wait_for_debug_log([terminate_line], ignore_case=True)
82+
self.log.debug(f"Terminating node after {num_total_logs} log lines seen")
83+
sigterm_node()
10984

11085
check_clean_start()
111-
num_total_logs = len(node.debug_log_path.read_text().splitlines())
11286
self.stop_node(0)
11387

11488
self.log.info(
11589
f"Terminate at some random point in the init process (max logs: {num_total_logs})")
11690

11791
for _ in range(40):
118-
terminate_after = random.randint(1, num_total_logs)
119-
self.log.debug(f"Starting node and will exit after {terminate_after} lines")
120-
node.start(
121-
# TODO: add -txindex=1 to fully test index initiatlization.
122-
# extra_args=['-txindex=1'],
123-
)
92+
num_logs = len(Path(node.debug_log_path).read_text().splitlines())
93+
additional_lines = random.randint(1, num_total_logs)
94+
self.log.debug(f"Starting node and will exit after {additional_lines} lines")
95+
node.start(extra_args=['-txindex=1'])
12496
logfile = open(node.debug_log_path, 'r', encoding='utf8')
12597

12698
MAX_SECS_TO_WAIT = 10
@@ -132,7 +104,8 @@ def check_clean_start():
132104
if line:
133105
num_lines += 1
134106

135-
if num_lines >= terminate_after or (time.time() - start) > MAX_SECS_TO_WAIT:
107+
if num_lines >= (num_logs + additional_lines) or \
108+
(time.time() - start) > MAX_SECS_TO_WAIT:
136109
self.log.debug(f"Terminating node after {num_lines} log lines seen")
137110
sigterm_node()
138111
break
@@ -152,11 +125,12 @@ def check_clean_start():
152125
}
153126

154127
for file_patt, err_fragment in files_to_disturb.items():
155-
target_file = list(node.chain_path.glob(file_patt))[0]
128+
target_files = list(node.chain_path.glob(file_patt))
156129

157-
self.log.info(f"Tweaking file to ensure failure {target_file}")
158-
bak_path = str(target_file) + ".bak"
159-
target_file.rename(bak_path)
130+
for target_file in target_files:
131+
self.log.info(f"Tweaking file to ensure failure {target_file}")
132+
bak_path = str(target_file) + ".bak"
133+
target_file.rename(bak_path)
160134

161135
# TODO: at some point, we should test perturbing the files instead of removing
162136
# them, e.g.
@@ -170,14 +144,16 @@ def check_clean_start():
170144
# investigate doing this later.
171145

172146
node.assert_start_raises_init_error(
173-
# TODO: add -txindex=1 to fully test index initiatlization.
174-
# extra_args=['-txindex=1'],
147+
extra_args=['-txindex=1'],
175148
expected_msg=err_fragment,
176149
match=ErrorMatch.PARTIAL_REGEX,
177150
)
178151

179-
self.log.info(f"Restoring file from {bak_path} and restarting")
180-
Path(bak_path).rename(target_file)
152+
for target_file in target_files:
153+
bak_path = str(target_file) + ".bak"
154+
self.log.debug(f"Restoring file from {bak_path} and restarting")
155+
Path(bak_path).rename(target_file)
156+
181157
check_clean_start()
182158
self.stop_node(0)
183159

test/functional/test_framework/test_node.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -389,14 +389,17 @@ def chain_path(self) -> Path:
389389
def debug_log_path(self) -> Path:
390390
return self.chain_path / 'debug.log'
391391

392+
def debug_log_bytes(self) -> int:
393+
with open(self.debug_log_path, encoding='utf-8') as dl:
394+
dl.seek(0, 2)
395+
return dl.tell()
396+
392397
@contextlib.contextmanager
393398
def assert_debug_log(self, expected_msgs, unexpected_msgs=None, timeout=2):
394399
if unexpected_msgs is None:
395400
unexpected_msgs = []
396401
time_end = time.time() + timeout * self.timeout_factor
397-
with open(self.debug_log_path, encoding='utf-8') as dl:
398-
dl.seek(0, 2)
399-
prev_size = dl.tell()
402+
prev_size = self.debug_log_bytes()
400403

401404
yield
402405

@@ -419,6 +422,42 @@ def assert_debug_log(self, expected_msgs, unexpected_msgs=None, timeout=2):
419422
time.sleep(0.05)
420423
self._raise_assertion_error('Expected messages "{}" does not partially match log:\n\n{}\n\n'.format(str(expected_msgs), print_log))
421424

425+
def wait_for_debug_log(self, expected_msgs, timeout=10, ignore_case=False) -> int:
426+
"""
427+
Block until we see a particular debug log message fragment or until we exceed the timeout.
428+
Return:
429+
the number of log lines we encountered when matching
430+
"""
431+
time_end = time.time() + timeout * self.timeout_factor
432+
prev_size = self.debug_log_bytes()
433+
re_flags = re.MULTILINE | (re.IGNORECASE if ignore_case else 0)
434+
435+
while True:
436+
found = True
437+
with open(self.debug_log_path, encoding='utf-8') as dl:
438+
dl.seek(prev_size)
439+
log = dl.read()
440+
441+
for expected_msg in expected_msgs:
442+
if re.search(re.escape(expected_msg), log, flags=re_flags) is None:
443+
found = False
444+
445+
if found:
446+
num_logs = len(log.splitlines())
447+
return num_logs
448+
449+
if time.time() >= time_end:
450+
print_log = " - " + "\n - ".join(log.splitlines())
451+
break
452+
453+
# No sleep here because we want to detect the message fragment as fast as
454+
# possible.
455+
456+
self._raise_assertion_error(
457+
'Expected messages "{}" does not partially match log:\n\n{}\n\n'.format(
458+
str(expected_msgs), print_log))
459+
return -1 # useless return to satisfy linter
460+
422461
@contextlib.contextmanager
423462
def profile_with_perf(self, profile_name: str):
424463
"""

0 commit comments

Comments
 (0)