Repository: bitcoin/bitcoin
File: test/functional/interface_ipc.py (Python)

Code before:
            # includes at this location. If pycapnp was installed with bundled capnp,
            # capnp/c++.capnp can be found here.
            capnp_dir = Path(capnp.__path__[0]).parent
        src_dir = Path(self.config['environment']['SRCDIR']) / "src"
        mp_dir = src_dir / "ipc" / "libmultiprocess" / "include"
        # List of import directories. Note: it is important for mp_dir to be
        # listed first, in case there are other libmultiprocess installations on
        # the system, to ensure that `import "/mp/proxy.capnp"` lines load the
        # same file as capnp.load() loads directly below, and there are not
        # "failed: Duplicate ID @0xcc316e3f71a040fb" errors.
        imports = [str(mp_dir), str(capnp_dir), str(src_dir)]
        return {
            "proxy": capnp.load(str(mp_dir / "mp" / "proxy.capnp"), imports=imports),
            "init": capnp.load(str(src_dir / "ipc" / "capnp" / "init.capnp"), imports=imports),
            "echo": capnp.load(str(src_dir / "ipc" / "capnp" / "echo.capnp"), imports=imports),
            "mining": capnp.load(str(src_dir / "ipc" / "capnp" / "mining.capnp"), imports=imports),
        }

    def set_test_params(self):
        self.num_nodes = 2

    def setup_nodes(self):
        self.extra_init = [{"ipcbind": True}, {}]
        super().setup_nodes()
        # Use this function to also load the capnp modules (we cannot use set_test_params for this,
        # as it is being called before knowing whether capnp is available).
        self.capnp_modules = self.load_capnp_modules()

    async def make_capnp_init_ctx(self):
        node = self.nodes[0]
        # Establish a connection, and create Init proxy object.
        connection = await capnp.AsyncIoStream.create_unix_connection(node.ipc_socket_path)
        client = capnp.TwoPartyClient(connection)
        init = client.bootstrap().cast_as(self.capnp_modules['init'].Init)
        # Create a remote thread on the server for the IPC calls to be executed in.
        threadmap = init.construct().threadMap
        thread = threadmap.makeThread("pythread").result
        ctx = self.capnp_modules['proxy'].Context()
        ctx.thread = thread
        # Return both.
        return ctx, init

    async def parse_and_deserialize_block(self, block_template, ctx):
        block_data = BytesIO((await block_template.result.getBlock(ctx)).result)
        block = CBlock()
        block.deserialize(block_data)
        return block

    async def parse_and_deserialize_coinbase_tx(self, block_template, ctx):
        coinbase_data = BytesIO((await block_template.result.getCoinbaseTx(ctx)).result)
        tx = CTransaction()

Review comment (suggestion):
In d8fe5f0326c5c0505e0f7e6e978961d24c27b175 "test: improve interface_ipc.py waitNext tests"

All are nits now I believe.

1. I don't suppose the following is guaranteed? When event is set, then the wait of `do` is also over that makes it call the sleep in parallel. Reworded slightly to highlight the broader intent of the `wait_and_do` function.
> Call wait_fn, then sleep,

2. From an event's POV, it is set in `wait()` and waited for in `do()`; suggested `s/wait/setter` and `s/do/waiter`.

3. `s/wait_started/event_over` because I got confused by `wait_started.set()` that could also suggest that the event wait starts now when instead it gets over at this stage.

```diff
diff --git a/test/functional/interface_ipc.py b/test/functional/interface_ipc.py
index 7c8f51a81c..0da75d25cd 100755
--- a/test/functional/interface_ipc.py
+++ b/test/functional/interface_ipc.py
@@ -40,18 +40,18 @@ async def wait_next_template(template, stack, ctx, opts):
     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()
+    """Call wait_fn and do_fn in parallel tasks while ensuring do_fn is
+    called after wait_fn is started. Wait for both tasks to complete."""
+    event_over = asyncio.Event()
     result = None
 
-    async def wait():
+    async def setter():
         nonlocal result
-        wait_started.set()
+        event_over.set()
         result = await wait_fn
 
-    async def do():
-        await wait_started.wait()
+    async def waiter():
+        await event_over.wait()
         await asyncio.sleep(0.1)
         # Let do_fn be either a callable or an awaitable object
         if inspect.isawaitable(do_fn):
@@ -59,7 +59,7 @@ async def wait_and_do(wait_fn, do_fn):
         else:
             do_fn()
 
-    await asyncio.gather(wait(), do())
+    await asyncio.gather(setter(), waiter())
     return result
 
 class IPCInterfaceTest(BitcoinTestFramework):
```

Code after:

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):
        self.skip_if_no_ipc()
        self.skip_if_no_py_capnp()

    def load_capnp_modules(self):
        if capnp_bin := shutil.which("capnp"):
            # Add the system cap'nproto path so include/capnp/c++.capnp can be found.
            capnp_dir = Path(capnp_bin).resolve().parent.parent / "include"
        else:
            # If there is no system cap'nproto, the pycapnp module should have its own "bundled"
            # includes at this location. If pycapnp was installed with bundled capnp,
            # capnp/c++.capnp can be found here.
            capnp_dir = Path(capnp.__path__[0]).parent
        src_dir = Path(self.config['environment']['SRCDIR']) / "src"
        mp_dir = src_dir / "ipc" / "libmultiprocess" / "include"
        # List of import directories. Note: it is important for mp_dir to be
        # listed first, in case there are other libmultiprocess installations on
        # the system, to ensure that `import "/mp/proxy.capnp"` lines load the
        # same file as capnp.load() loads directly below, and there are not
        # "failed: Duplicate ID @0xcc316e3f71a040fb" errors.
        imports = [str(mp_dir), str(capnp_dir), str(src_dir)]