Skip to content

Commit f42da9d

Browse files
fanquakevijaydasmp
authored andcommitted
Merge bitcoin#27831: test: handle failed assert_equal() assertions in bcc callback functions
61f4b9b Manage exceptions in bcc callback functions (virtu) Pull request description: Address bitcoin#27380 (and similar future issues) by handling failed `assert_equal()` assertions in bcc callback functions ### Problem Exceptions are not propagated in ctype callback functions used by bcc. This means an AssertionError exception raised by `assert_equal()` to signal a failed assertion is not getting caught and properly logged. Instead, the error is logged to stdout and execution of the callback stops. The current workaround to check whether all `assert_equal()` assertions in a callback succeeded is to increment a success counter after the assertions (which only gets incremented if none exception is raised and stops execution). Then, outside the callback, the success counter can be used to check whether a callback executed successfully. One issue with the described workaround is that when an exception occurs, there is no way of telling which of the `assert_equal()` statements caused the exception; moreover, there is no way of inspecting how the pieces of data that got compared in `assert_equal()` differed (often a crucial clue when debugging what went wrong). This problem is happening in bitcoin#27380: Sporadically, in the `mempool:rejected` test, execution does not reach the end of the callback function and the success counter is not incremented. Thus, the test fails when comparing the counter to its expected value of one. Without knowing which of the asserts failed any why it failed, this issue is hard to debug. ### Solution Two fixes come to mind. The first involves having the callback function make event data accessible outside the callback and inspecting the event using `assert_equal()` outside the callback. This solution still requires a counter in the callback in order to tell whether a callback was actually executed or if instead the call to perf_buffer_poll() timed out. The second fix entails wrapping all relevant `assert_equal()` statements inside callback functions into try-catch blocks and manually logging AssertionErrors. While not as elegant in terms of design, this approach can be more pragmatic for more complex tests (e.g., ones involving multiple events, events of different types, or the order of events). The solution proposed here is to select the most pragmatic fix on a case-by-case basis: Tests in `interface_usdt_net.py`, `interface_usdt_mempool.py` and `interface_usdt_validation.py` have been refactored to use the first approach, while the second approach was chosen for `interface_usdt_utxocache.py` (partly to provide a reference for the second approach, but mainly because the utxocache tests are the most intricate tests, and refactoring them to use the first approach would negatively impact their readability). Lastly, `interface_usdt_coinselection.py` was kept unchanged because it does not use `assert_equal()` statements inside callback functions. ACKs for top commit: 0xB10C: Reviewed the changes since my last review. ACK 61f4b9b. I've tested that the combined log contains both exceptions by modifying `interface_usdt_utxocache.py`. willcl-ark: utACK 61f4b9b stickies-v: utACK 61f4b9b Tree-SHA512: 85cdaabf370d4f09a9eab6af9ce7c796cd9d08cb91f38f021f71adda34c5f643331022dd09cadb95be2185dad6016c95cbb8942e41e4fbd566a49bf431c5141a
1 parent 6d62746 commit f42da9d

File tree

3 files changed

+54
-33
lines changed

3 files changed

+54
-33
lines changed

test/functional/interface_usdt_net.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,10 @@ def __repr__(self):
116116
fn_name="trace_outbound_message")
117117
bpf = BPF(text=net_tracepoints_program, usdt_contexts=[ctx], debug=0)
118118

119-
# The handle_* function is a ctypes callback function called from C. When
120-
# we assert in the handle_* function, the AssertError doesn't propagate
121-
# back to Python. The exception is ignored. We manually count and assert
122-
# that the handle_* functions succeeded.
123119
EXPECTED_INOUTBOUND_VERSION_MSG = 1
124120
checked_inbound_version_msg = 0
125121
checked_outbound_version_msg = 0
122+
events = []
126123

127124
def check_p2p_message(event, inbound):
128125
nonlocal checked_inbound_version_msg, checked_outbound_version_msg
@@ -142,12 +139,13 @@ def check_p2p_message(event, inbound):
142139
checked_outbound_version_msg += 1
143140

144141
def handle_inbound(_, data, __):
142+
nonlocal events
145143
event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents
146-
check_p2p_message(event, True)
144+
events.append((event, True))
147145

148146
def handle_outbound(_, data, __):
149147
event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents
150-
check_p2p_message(event, False)
148+
events.append((event, False))
151149

152150
bpf["inbound_messages"].open_perf_buffer(handle_inbound)
153151
bpf["outbound_messages"].open_perf_buffer(handle_outbound)
@@ -158,12 +156,15 @@ def handle_outbound(_, data, __):
158156
bpf.perf_buffer_poll(timeout=200)
159157

160158
self.log.info(
161-
"check that we got both an inbound and outbound version message")
159+
"check receipt and content of in- and outbound version messages")
160+
for event, inbound in events:
161+
check_p2p_message(event, inbound)
162162
assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG,
163163
checked_inbound_version_msg)
164164
assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG,
165165
checked_outbound_version_msg)
166166

167+
167168
bpf.cleanup()
168169

169170

test/functional/interface_usdt_utxocache.py

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,16 @@ def handle_utxocache_uncache(_, data, __):
190190
nonlocal handle_uncache_succeeds
191191
event = ctypes.cast(data, ctypes.POINTER(UTXOCacheChange)).contents
192192
self.log.info(f"handle_utxocache_uncache(): {event}")
193-
assert_equal(block_1_coinbase_txid, bytes(event.txid[::-1]).hex())
194-
assert_equal(0, event.index) # prevout index
195-
assert_equal(EARLY_BLOCK_HEIGHT, event.height)
196-
assert_equal(500 * COIN, event.value)
197-
assert_equal(True, event.is_coinbase)
198-
199-
handle_uncache_succeeds += 1
193+
try:
194+
assert_equal(block_1_coinbase_txid, bytes(event.txid[::-1]).hex())
195+
assert_equal(0, event.index) # prevout index
196+
assert_equal(EARLY_BLOCK_HEIGHT, event.height)
197+
assert_equal(50 * COIN, event.value)
198+
assert_equal(True, event.is_coinbase)
199+
except AssertionError:
200+
self.log.exception("Assertion failed")
201+
else:
202+
handle_uncache_succeeds += 1
200203

201204
bpf["utxocache_uncache"].open_perf_buffer(handle_utxocache_uncache)
202205

@@ -262,24 +265,32 @@ def handle_utxocache_add(_, data, __):
262265
event = ctypes.cast(data, ctypes.POINTER(UTXOCacheChange)).contents
263266
self.log.info(f"handle_utxocache_add(): {event}")
264267
add = expected_utxocache_adds.pop(0)
265-
assert_equal(add["txid"], bytes(event.txid[::-1]).hex())
266-
assert_equal(add["index"], event.index)
267-
assert_equal(add["height"], event.height)
268-
assert_equal(add["value"], event.value)
269-
assert_equal(add["is_coinbase"], event.is_coinbase)
270-
handle_add_succeeds += 1
268+
try:
269+
assert_equal(add["txid"], bytes(event.txid[::-1]).hex())
270+
assert_equal(add["index"], event.index)
271+
assert_equal(add["height"], event.height)
272+
assert_equal(add["value"], event.value)
273+
assert_equal(add["is_coinbase"], event.is_coinbase)
274+
except AssertionError:
275+
self.log.exception("Assertion failed")
276+
else:
277+
handle_add_succeeds += 1
271278

272279
def handle_utxocache_spent(_, data, __):
273280
nonlocal handle_spent_succeeds
274281
event = ctypes.cast(data, ctypes.POINTER(UTXOCacheChange)).contents
275282
self.log.info(f"handle_utxocache_spent(): {event}")
276283
spent = expected_utxocache_spents.pop(0)
277-
assert_equal(spent["txid"], bytes(event.txid[::-1]).hex())
278-
assert_equal(spent["index"], event.index)
279-
assert_equal(spent["height"], event.height)
280-
assert_equal(spent["value"], event.value)
281-
assert_equal(spent["is_coinbase"], event.is_coinbase)
282-
handle_spent_succeeds += 1
284+
try:
285+
assert_equal(spent["txid"], bytes(event.txid[::-1]).hex())
286+
assert_equal(spent["index"], event.index)
287+
assert_equal(spent["height"], event.height)
288+
assert_equal(spent["value"], event.value)
289+
assert_equal(spent["is_coinbase"], event.is_coinbase)
290+
except AssertionError:
291+
self.log.exception("Assertion failed")
292+
else:
293+
handle_spent_succeeds += 1
283294

284295
bpf["utxocache_add"].open_perf_buffer(handle_utxocache_add)
285296
bpf["utxocache_spent"].open_perf_buffer(handle_utxocache_spent)

test/functional/interface_usdt_validation.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,10 @@ def __repr__(self):
8585
self.sigops,
8686
self.duration)
8787

88-
# The handle_* function is a ctypes callback function called from C. When
89-
# we assert in the handle_* function, the AssertError doesn't propagate
90-
# back to Python. The exception is ignored. We manually count and assert
91-
# that the handle_* functions succeeded.
9288
BLOCKS_EXPECTED = 2
9389
blocks_checked = 0
9490
expected_blocks = dict()
91+
events = []
9592

9693
self.log.info("hook into the validation:block_connected tracepoint")
9794
ctx = USDT(path=str(self.options.bitcoind))
@@ -101,7 +98,7 @@ def __repr__(self):
10198
usdt_contexts=[ctx], debug=0)
10299

103100
def handle_blockconnected(_, data, __):
104-
nonlocal expected_blocks, blocks_checked
101+
nonlocal events, blocks_checked
105102
event = ctypes.cast(data, ctypes.POINTER(Block)).contents
106103
self.log.info(f"handle_blockconnected(): {event}")
107104
block_hash = bytes(event.hash[::-1]).hex()
@@ -126,12 +123,24 @@ def handle_blockconnected(_, data, __):
126123
expected_blocks[block_hash] = self.nodes[0].getblock(block_hash, 2)
127124

128125
bpf.perf_buffer_poll(timeout=200)
129-
bpf.cleanup()
130126

131-
self.log.info(f"check that we traced {BLOCKS_EXPECTED} blocks")
127+
self.log.info(f"check that we correctly traced {BLOCKS_EXPECTED} blocks")
128+
for event in events:
129+
block_hash = bytes(event.hash[::-1]).hex()
130+
block = expected_blocks[block_hash]
131+
assert_equal(block["hash"], block_hash)
132+
assert_equal(block["height"], event.height)
133+
assert_equal(len(block["tx"]), event.transactions)
134+
assert_equal(len([tx["vin"] for tx in block["tx"]]), event.inputs)
135+
assert_equal(0, event.sigops) # no sigops in coinbase tx
136+
# only plausibility checks
137+
assert event.duration > 0
138+
del expected_blocks[block_hash]
132139
assert_equal(BLOCKS_EXPECTED, blocks_checked)
133140
assert_equal(0, len(expected_blocks))
134141

142+
bpf.cleanup()
143+
135144

136145
if __name__ == '__main__':
137146
ValidationTracepointTest().main()

0 commit comments

Comments
 (0)