test: Add TestNode ipcbind option

With this change, tests can specify `self.extra_init = [{ipcbind: True}]` to
start a node listening on an IPC socket, instead of needing to choose which
node binary to invoke and what `self.extra_args=[["-ipcbind=..."]]` value to
pass to it.

The eliminates boilerplate code #30437 (interface_ipc_mining.py), #32297
(interface_ipc_cli.py), and #33201 (interface_ipc.py) previously needed in
their test setup.
This commit is contained in:
Ryan Ofsky
2025-08-18 11:19:22 -04:00
committed by Pieter Wuille
parent 3cceb60a71
commit 3cc9a06c8d
2 changed files with 46 additions and 14 deletions

View File

@@ -76,9 +76,9 @@ class Binaries:
self.paths = paths self.paths = paths
self.bin_dir = bin_dir self.bin_dir = bin_dir
def node_argv(self): def node_argv(self, **kwargs):
"Return argv array that should be used to invoke bitcoind" "Return argv array that should be used to invoke bitcoind"
return self._argv("node", self.paths.bitcoind) return self._argv("node", self.paths.bitcoind, **kwargs)
def rpc_argv(self): def rpc_argv(self):
"Return argv array that should be used to invoke bitcoin-cli" "Return argv array that should be used to invoke bitcoin-cli"
@@ -101,16 +101,19 @@ class Binaries:
"Return argv array that should be used to invoke bitcoin-chainstate" "Return argv array that should be used to invoke bitcoin-chainstate"
return self._argv("chainstate", self.paths.bitcoinchainstate) return self._argv("chainstate", self.paths.bitcoinchainstate)
def _argv(self, command, bin_path): def _argv(self, command, bin_path, need_ipc=False):
"""Return argv array that should be used to invoke the command. It """Return argv array that should be used to invoke the command. It
either uses the bitcoin wrapper executable (if BITCOIN_CMD is set), or either uses the bitcoin wrapper executable (if BITCOIN_CMD is set or
the direct binary path (bitcoind, etc). When bin_dir is set (by tests need_ipc is True), or the direct binary path (bitcoind, etc). When
calling binaries from previous releases) it always uses the direct bin_dir is set (by tests calling binaries from previous releases) it
path.""" always uses the direct path."""
if self.bin_dir is not None: if self.bin_dir is not None:
return [os.path.join(self.bin_dir, os.path.basename(bin_path))] return [os.path.join(self.bin_dir, os.path.basename(bin_path))]
elif self.paths.bitcoin_cmd is not None: elif self.paths.bitcoin_cmd is not None or need_ipc:
return self.paths.bitcoin_cmd + [command] # If the current test needs IPC functionality, use the bitcoin
# wrapper binary and append -m so it calls multiprocess binaries.
bitcoin_cmd = self.paths.bitcoin_cmd or [self.paths.bitcoin_bin]
return bitcoin_cmd + (["-m"] if need_ipc else []) + [command]
else: else:
return [bin_path] return [bin_path]
@@ -158,6 +161,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
self.noban_tx_relay: bool = False self.noban_tx_relay: bool = False
self.nodes: list[TestNode] = [] self.nodes: list[TestNode] = []
self.extra_args = None self.extra_args = None
self.extra_init = None
self.network_thread = None self.network_thread = None
self.rpc_timeout = 60 # Wait for up to 60 seconds for the RPC server to respond self.rpc_timeout = 60 # Wait for up to 60 seconds for the RPC server to respond
self.supports_cli = True self.supports_cli = True
@@ -553,15 +557,15 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
bin_dirs.append(bin_dir) bin_dirs.append(bin_dir)
extra_init = [{}] * num_nodes if self.extra_init is None else self.extra_init # type: ignore[var-annotated]
assert_equal(len(extra_init), num_nodes)
assert_equal(len(extra_confs), num_nodes) assert_equal(len(extra_confs), num_nodes)
assert_equal(len(extra_args), num_nodes) assert_equal(len(extra_args), num_nodes)
assert_equal(len(versions), num_nodes) assert_equal(len(versions), num_nodes)
assert_equal(len(bin_dirs), num_nodes) assert_equal(len(bin_dirs), num_nodes)
for i in range(num_nodes): for i in range(num_nodes):
args = list(extra_args[i]) args = list(extra_args[i])
test_node_i = TestNode( init = dict(
i,
get_datadir_path(self.options.tmpdir, i),
chain=self.chain, chain=self.chain,
rpchost=rpchost, rpchost=rpchost,
timewait=self.rpc_timeout, timewait=self.rpc_timeout,
@@ -578,6 +582,11 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
v2transport=self.options.v2transport, v2transport=self.options.v2transport,
uses_wallet=self.uses_wallet, uses_wallet=self.uses_wallet,
) )
init.update(extra_init[i])
test_node_i = TestNode(
i,
get_datadir_path(self.options.tmpdir, i),
**init)
self.nodes.append(test_node_i) self.nodes.append(test_node_i)
if not test_node_i.version_is_at_least(170000): if not test_node_i.version_is_at_least(170000):
# adjust conf for pre 17 # adjust conf for pre 17

View File

@@ -11,6 +11,7 @@ from enum import Enum
import json import json
import logging import logging
import os import os
import pathlib
import platform import platform
import re import re
import subprocess import subprocess
@@ -19,6 +20,7 @@ import time
import urllib.parse import urllib.parse
import collections import collections
import shlex import shlex
import shutil
import sys import sys
from pathlib import Path from pathlib import Path
@@ -52,6 +54,13 @@ CLI_MAX_ARG_SIZE = 131071 # many systems have a 128kb limit per arg (MAX_ARG_STR
NULL_BLK_XOR_KEY = bytes([0] * NUM_XOR_BYTES) NULL_BLK_XOR_KEY = bytes([0] * NUM_XOR_BYTES)
BITCOIN_PID_FILENAME_DEFAULT = "bitcoind.pid" BITCOIN_PID_FILENAME_DEFAULT = "bitcoind.pid"
if sys.platform.startswith("linux"):
UNIX_PATH_MAX = 108 # includes the trailing NUL
elif sys.platform.startswith(("darwin", "freebsd", "netbsd", "openbsd")):
UNIX_PATH_MAX = 104
else: # safest portable value
UNIX_PATH_MAX = 92
class FailedToStartError(Exception): class FailedToStartError(Exception):
"""Raised when a node fails to start correctly.""" """Raised when a node fails to start correctly."""
@@ -77,7 +86,7 @@ class TestNode():
To make things easier for the test writer, any unrecognised messages will To make things easier for the test writer, any unrecognised messages will
be dispatched to the RPC connection.""" be dispatched to the RPC connection."""
def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, binaries, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, v2transport=False, uses_wallet=False): def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, binaries, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, v2transport=False, uses_wallet=False, ipcbind=False):
""" """
Kwargs: Kwargs:
start_perf (bool): If True, begin profiling the node with `perf` as soon as start_perf (bool): If True, begin profiling the node with `perf` as soon as
@@ -109,7 +118,7 @@ class TestNode():
# Configuration for logging is set as command-line args rather than in the bitcoin.conf file. # Configuration for logging is set as command-line args rather than in the bitcoin.conf file.
# This means that starting a bitcoind using the temp dir to debug a failed test won't # This means that starting a bitcoind using the temp dir to debug a failed test won't
# spam debug.log. # spam debug.log.
self.args = self.binaries.node_argv() + [ self.args = self.binaries.node_argv(need_ipc=ipcbind) + [
f"-datadir={self.datadir_path}", f"-datadir={self.datadir_path}",
"-logtimemicros", "-logtimemicros",
"-debug", "-debug",
@@ -121,6 +130,17 @@ class TestNode():
if uses_wallet is not None and not uses_wallet: if uses_wallet is not None and not uses_wallet:
self.args.append("-disablewallet") self.args.append("-disablewallet")
self.ipc_tmp_dir = None
if ipcbind:
self.ipc_socket_path = self.chain_path / "node.sock"
if len(os.fsencode(self.ipc_socket_path)) < UNIX_PATH_MAX:
self.args.append("-ipcbind=unix")
else:
# Work around default CI path exceeding maximum socket path length.
self.ipc_tmp_dir = pathlib.Path(tempfile.mkdtemp(prefix="test-ipc-"))
self.ipc_socket_path = self.ipc_tmp_dir / "node.sock"
self.args.append(f"-ipcbind=unix:{self.ipc_socket_path}")
# Use valgrind, expect for previous release binaries # Use valgrind, expect for previous release binaries
if use_valgrind and version is None: if use_valgrind and version is None:
default_suppressions_file = Path(__file__).parents[3] / "contrib" / "valgrind.supp" default_suppressions_file = Path(__file__).parents[3] / "contrib" / "valgrind.supp"
@@ -208,6 +228,9 @@ class TestNode():
# this destructor is called. # this destructor is called.
print(self._node_msg("Cleaning up leftover process"), file=sys.stderr) print(self._node_msg("Cleaning up leftover process"), file=sys.stderr)
self.process.kill() self.process.kill()
if self.ipc_tmp_dir:
print(self._node_msg(f"Cleaning up ipc directory {str(self.ipc_tmp_dir)!r}"))
shutil.rmtree(self.ipc_tmp_dir)
def __getattr__(self, name): def __getattr__(self, name):
"""Dispatches any unrecognised messages to the RPC connection or a CLI instance.""" """Dispatches any unrecognised messages to the RPC connection or a CLI instance."""