From d8fe5f0326c5c0505e0f7e6e978961d24c27b175 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Mon, 8 Dec 2025 22:22:43 -0500 Subject: [PATCH] test: improve interface_ipc.py waitNext tests As pointed out by Sjors in https://github.com/bitcoin/bitcoin/pull/34003#discussion_r2598844209 and https://github.com/bitcoin/bitcoin/pull/34003#discussion_r2598858386 the original intention of having waitNext and waitTipChanged calls in the test was to ensure that if new blocks were connected or fees were increased *during* the waits, that the calls would wake up and return. But the tests were written incorrectly, to generate blocks and transactions before the wait calls instead of during the calls. So the tests were less meaningful then they should be. There was also a similar problem in the interruptWait test. The test was intended to test the interruptWait method, but it was never actually calling the method due to a missing await keyword. Instead it was testing that miniwallet.send_self_transfer would interrupt the wait. This commit fixes these issues by introducing a wait_and_do() helper function to start parallel tasks and trigger an action after a wait call is started. Co-authored-by: Sjors Provoost --- test/functional/interface_ipc.py | 59 ++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/test/functional/interface_ipc.py b/test/functional/interface_ipc.py index c04a05762b..7c8f51a81c 100755 --- a/test/functional/interface_ipc.py +++ b/test/functional/interface_ipc.py @@ -4,6 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the IPC (multiprocess) interface.""" import asyncio +import inspect from contextlib import asynccontextmanager, AsyncExitStack from io import BytesIO from pathlib import Path @@ -38,6 +39,29 @@ async def wait_next_template(template, stack, ctx, opts): """Call template.waitNext() and return template, then call template.destroy() when stack exits.""" return await stack.enter_async_context(destroying((await template.waitNext(ctx, opts)).result, ctx)) +async def wait_and_do(wait_fn, do_fn): + """Call wait_fn, then sleep, then call do_fn in a parallel task. Wait for + both tasks to complete.""" + wait_started = asyncio.Event() + result = None + + async def wait(): + nonlocal result + wait_started.set() + result = await wait_fn + + async def do(): + await wait_started.wait() + await asyncio.sleep(0.1) + # Let do_fn be either a callable or an awaitable object + if inspect.isawaitable(do_fn): + await do_fn + else: + do_fn() + + await asyncio.gather(wait(), do()) + return result + class IPCInterfaceTest(BitcoinTestFramework): def skip_test_if_missing_module(self): @@ -138,8 +162,9 @@ class IPCInterfaceTest(BitcoinTestFramework): current_block_height = self.nodes[0].getchaintips()[0]["height"] assert blockref.result.height == current_block_height self.log.debug("Mine a block") - self.generate(self.nodes[0], 1) - newblockref = (await mining.waitTipChanged(ctx, blockref.result.hash)).result + newblockref = (await wait_and_do( + mining.waitTipChanged(ctx, blockref.result.hash, timeout), + lambda: self.generate(self.nodes[0], 1))).result assert_equal(len(newblockref.hash), block_hash_size) assert_equal(newblockref.height, current_block_height + 1) self.log.debug("Wait for timeout") @@ -175,8 +200,9 @@ class IPCInterfaceTest(BitcoinTestFramework): waitoptions = self.capnp_modules['mining'].BlockWaitOptions() waitoptions.timeout = timeout waitoptions.feeThreshold = 1 - self.generate(self.nodes[0], 1) - template2 = await wait_next_template(template, stack, ctx, waitoptions) + template2 = await wait_and_do( + wait_next_template(template, stack, ctx, waitoptions), + lambda: self.generate(self.nodes[0], 1)) block2 = await self.parse_and_deserialize_block(template2, ctx) assert_equal(len(block2.vtx), 1) @@ -185,8 +211,9 @@ class IPCInterfaceTest(BitcoinTestFramework): assert_equal(template3._has("result"), False) self.log.debug("Wait for another, get one after increase in fees in the mempool") - miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]) - template4 = await wait_next_template(template2, stack, ctx, waitoptions) + template4 = await wait_and_do( + wait_next_template(template2, stack, ctx, waitoptions), + lambda: miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])) block3 = await self.parse_and_deserialize_block(template4, ctx) assert_equal(len(block3.vtx), 2) @@ -198,8 +225,9 @@ class IPCInterfaceTest(BitcoinTestFramework): waitoptions.feeThreshold = 1 self.log.debug("Wait for another, get one after increase in fees in the mempool") - miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]) - template6 = await wait_next_template(template5, stack, ctx, waitoptions) + template6 = await wait_and_do( + wait_next_template(template5, stack, ctx, waitoptions), + lambda: miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])) block4 = await self.parse_and_deserialize_block(template6, ctx) assert_equal(len(block4.vtx), 3) @@ -208,26 +236,13 @@ class IPCInterfaceTest(BitcoinTestFramework): assert_equal(template7._has("result"), False) self.log.debug("interruptWait should abort the current wait") - wait_started = asyncio.Event() async def wait_for_block(): new_waitoptions = self.capnp_modules['mining'].BlockWaitOptions() new_waitoptions.timeout = waitoptions.timeout * 60 # 1 minute wait new_waitoptions.feeThreshold = 1 - wait_started.set() template7 = await template6.waitNext(ctx, new_waitoptions) assert_equal(template7._has("result"), False) - - async def interrupt_wait(): - await wait_started.wait() # Wait for confirmation wait started - await asyncio.sleep(0.1) # Minimal buffer - template6.interruptWait() - miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]) - - wait_task = asyncio.create_task(wait_for_block()) - interrupt_task = asyncio.create_task(interrupt_wait()) - - result = await wait_task - await interrupt_task + await wait_and_do(wait_for_block(), template6.interruptWait()) current_block_height = self.nodes[0].getchaintips()[0]["height"] check_opts = self.capnp_modules['mining'].BlockCheckOptions()