Merge bitcoin/bitcoin#33362: Run feature_bind_port_(discover|externalip).py in CI

75cf9708a0 ci: add one more routable address to the VMs (docker containers) (Vasil Dimov)
1b93983bf5 test: make feature_bind_port_(discover|externalip).py auto-detect the skip condition (Vasil Dimov)

Pull request description:

  `feature_bind_port_discover.py` and `feature_bind_port_externalip.py` require a routable address on the machine to run. Since that was not predictably available on CI, those tests required a manual setting up of IP addresses (e.g. using `ifconfig`) and then running the tests with a command line option telling them that the addresses are set up. The tests were not run in CI and [got rot](https://github.com/bitcoin/bitcoin/issues/31293#issuecomment-2497792487).

  Change that to auto-detect, from the tests, whether the needed IP addresses are present and if yes, run the test, otherwise skip it. Also change the CI to configure the needed addresses when running the functional tests. This way the tests will be run regularly on CI.

  Fixes: https://github.com/bitcoin/bitcoin/issues/31336

ACKs for top commit:
  willcl-ark:
    ACK 75cf9708a0
  frankomosh:
    Tested ACK 75cf9708a0. Built from source.
  ryanofsky:
    Code review ACK 75cf9708a0. Tested locally with and without the special addresses, and the detection seems to work well.

Tree-SHA512: 252911a37a06764f644a1a83c808f5255ac3bc74919426afa5d082c59e1ea924196354735f229d381cb5aff2340e001c2240bbadc8b5f27e5321fb4cfaef0fdb
This commit is contained in:
Ryan Ofsky
2026-05-19 13:05:17 -04:00
4 changed files with 78 additions and 41 deletions

View File

@@ -115,6 +115,7 @@ def main():
CI_CCACHE_MOUNT = f"type=bind,src={os.environ['CCACHE_DIR']},dst={os.environ['CCACHE_DIR']}" CI_CCACHE_MOUNT = f"type=bind,src={os.environ['CCACHE_DIR']},dst={os.environ['CCACHE_DIR']}"
run(["docker", "network", "create", "--ipv6", "--subnet", "1111:1111::/112", "ci-ip6net"], check=False) run(["docker", "network", "create", "--ipv6", "--subnet", "1111:1111::/112", "ci-ip6net"], check=False)
run(["docker", "network", "create", "--subnet", "1.1.1.0/24", "ci-ip4net"], check=False)
if os.getenv("RESTART_CI_DOCKER_BEFORE_RUN"): if os.getenv("RESTART_CI_DOCKER_BEFORE_RUN"):
print("Restart docker before run to stop and clear all containers started with --rm") print("Restart docker before run to stop and clear all containers started with --rm")
@@ -144,6 +145,7 @@ def main():
f"--env-file={env_file}", f"--env-file={env_file}",
f"--name={os.environ['CONTAINER_NAME']}", f"--name={os.environ['CONTAINER_NAME']}",
"--network=ci-ip6net", "--network=ci-ip6net",
"--ip6=1111:1111::5", # Used by some of the tests, don't change it just here (keep them in sync).
f"--platform={os.environ['CI_IMAGE_PLATFORM']}", f"--platform={os.environ['CI_IMAGE_PLATFORM']}",
os.environ["CONTAINER_NAME"], os.environ["CONTAINER_NAME"],
] ]
@@ -154,6 +156,8 @@ def main():
text=True, text=True,
).stdout.strip() ).stdout.strip()
run(["docker", "network", "connect", "--ip=1.1.1.5", "ci-ip4net", container_id]) # The IP address is used by some of the tests, don't change it just here (keep them in sync).
def ci_exec(cmd_inner, **kwargs): def ci_exec(cmd_inner, **kwargs):
if os.getenv("DANGER_RUN_CI_ON_HOST"): if os.getenv("DANGER_RUN_CI_ON_HOST"):
prefix = [] prefix = []

View File

@@ -7,6 +7,9 @@ Test that -discover does not add all interfaces' addresses if we listen on only
""" """
from test_framework.test_framework import BitcoinTestFramework, SkipTest from test_framework.test_framework import BitcoinTestFramework, SkipTest
from test_framework.test_node import (
FailedToStartError,
)
from test_framework.util import ( from test_framework.util import (
assert_equal, assert_equal,
assert_not_equal, assert_not_equal,
@@ -20,18 +23,14 @@ from test_framework.util import (
# Linux: # Linux:
# First find your interfaces: ip addr show # First find your interfaces: ip addr show
# Then use your actual interface names (replace INTERFACE_NAME with yours): # Then use your actual interface names (replace INTERFACE_NAME with yours):
# ip addr add 1.1.1.1/32 dev INTERFACE_NAME && ip addr add 2.2.2.2/32 dev INTERFACE_NAME # to set up # ip addr add 1.1.1.5/32 dev INTERFACE_NAME && ip addr add 1111:1111::5/128 dev INTERFACE_NAME # to set up
# ip addr del 1.1.1.1/32 dev INTERFACE_NAME && ip addr del 2.2.2.2/32 dev INTERFACE_NAME # to remove it # ip addr del 1.1.1.5/32 dev INTERFACE_NAME && ip addr del 1111:1111::5/128 dev INTERFACE_NAME # to remove it
# #
# macOS: # FreeBSD and MacOS:
# ifconfig en0 alias 1.1.1.1 && ifconfig en0 alias 2.2.2.2 # to set up # ifconfig INTERFACE_NAME 1.1.1.5/32 alias && ifconfig INTERFACE_NAME inet6 1111:1111::5/128 alias # to set up
# ifconfig en0 1.1.1.1 -alias && ifconfig en0 2.2.2.2 -alias # to remove it, after the test # ifconfig INTERFACE_NAME 1.1.1.5 -alias && ifconfig INTERFACE_NAME inet6 1111:1111::5 -alias # to remove it, after the test
# ADDR1 = '1.1.1.5' # This and the address below are set in the CI environment, don't change it just here (keep them in sync).
# FreeBSD: ADDR2 = '1111:1111::5'
# ifconfig em0 1.1.1.1/32 alias && ifconfig wlan0 2.2.2.2/32 alias # to set up
# ifconfig em0 1.1.1.1 -alias && ifconfig wlan0 2.2.2.2 -alias # to remove it, after the test
ADDR1 = '1.1.1.1'
ADDR2 = '2.2.2.2'
class BindPortDiscoverTest(BitcoinTestFramework): class BindPortDiscoverTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
@@ -69,19 +68,17 @@ class BindPortDiscoverTest(BitcoinTestFramework):
# False. We do not want any -bind= thus set has_explicit_bind to True. # False. We do not want any -bind= thus set has_explicit_bind to True.
for node in self.nodes: for node in self.nodes:
node.has_explicit_bind = True node.has_explicit_bind = True
self.start_nodes()
def add_options(self, parser): try:
parser.add_argument( self.start_nodes()
"--ihave1111and2222", action='store_true', dest="ihave1111and2222", except FailedToStartError as e:
help=f"Run the test, assuming {ADDR1} and {ADDR2} are configured on the machine", self.cleanup_partially_started_nodes()
default=False) if 'Unable to bind to ' in str(e):
raise SkipTest(
def skip_test_if_missing_module(self): f'To run this test make sure that {ADDR1} and {ADDR2} '
if not self.options.ihave1111and2222: '(routable addresses) are assigned to non-loopback '
raise SkipTest( 'interfaces on this machine')
f"To run this test make sure that {ADDR1} and {ADDR2} (routable addresses) are " raise
"assigned to the interfaces on this machine and rerun with --ihave1111and2222")
def run_test(self): def run_test(self):
self.log.info( self.log.info(

View File

@@ -7,17 +7,24 @@ Test that the proper port is used for -externalip=
""" """
from test_framework.test_framework import BitcoinTestFramework, SkipTest from test_framework.test_framework import BitcoinTestFramework, SkipTest
from test_framework.test_node import (
FailedToStartError,
)
from test_framework.util import assert_equal, p2p_port from test_framework.util import assert_equal, p2p_port
# We need to bind to a routable address for this test to exercise the relevant code. # We need to bind to a routable address for this test to exercise the relevant
# To set a routable address on the machine use: # code. Those addresses must be on an interface that is UP and is not a loopback
# interface (IFF_LOOPBACK).
# To set these routable addresses on the machine, use:
# Linux: # Linux:
# ifconfig lo:0 1.1.1.1/32 up # to set up # First find your interfaces: ip addr show
# ifconfig lo:0 down # to remove it, after the test # Then use your actual interface names (replace INTERFACE_NAME with yours):
# FreeBSD: # ip addr add 1.1.1.5/32 dev INTERFACE_NAME # to set up
# ifconfig lo0 1.1.1.1/32 alias # to set up # ip addr del 1.1.1.5/32 dev INTERFACE_NAME # to remove it
# ifconfig lo0 1.1.1.1 -alias # to remove it, after the test # FreeBSD and MacOS:
ADDR = '1.1.1.1' # ifconfig INTERFACE_NAME 1.1.1.5/32 alias # to set up
# ifconfig INTERFACE_NAME 1.1.1.5 -alias # to remove it, after the test
ADDR = '1.1.1.5' # This is set in the CI environment, don't change it just here (keep them in sync).
# array of tuples [arguments, expected port in localaddresses] # array of tuples [arguments, expected port in localaddresses]
EXPECTED = [ EXPECTED = [
@@ -45,17 +52,25 @@ class BindPortExternalIPTest(BitcoinTestFramework):
self.num_nodes = len(EXPECTED) self.num_nodes = len(EXPECTED)
self.extra_args = list(map(lambda e: e[0], EXPECTED)) self.extra_args = list(map(lambda e: e[0], EXPECTED))
def add_options(self, parser): def setup_network(self):
parser.add_argument( self.setup_nodes()
"--ihave1111", action='store_true', dest="ihave1111",
help=f"Run the test, assuming {ADDR} is configured on the machine",
default=False)
def skip_test_if_missing_module(self): def setup_nodes(self):
if not self.options.ihave1111: self.add_nodes(self.num_nodes, self.extra_args)
raise SkipTest( # TestNode.start() will add -bind= to extra_args if has_explicit_bind is
f"To run this test make sure that {ADDR} (a routable address) is assigned " # False. We do not want any -bind= thus set has_explicit_bind to True.
"to one of the interfaces on this machine and rerun with --ihave1111") for node in self.nodes:
node.has_explicit_bind = True
try:
self.start_nodes()
except FailedToStartError as e:
self.cleanup_partially_started_nodes()
if 'Unable to bind to ' in str(e):
raise SkipTest(
f'To run this test make sure that {ADDR} '
'(routable address) is assigned to non-loopback '
'interface on this machine')
raise
def run_test(self): def run_test(self):
self.log.info("Test the proper port is used for -externalip=") self.log.info("Test the proper port is used for -externalip=")

View File

@@ -535,6 +535,27 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
# Wait for nodes to stop # Wait for nodes to stop
node.wait_until_stopped() node.wait_until_stopped()
def cleanup_partially_started_nodes(self):
"""Tear down nodes left running after a failed start_nodes().
After start_nodes() raises (e.g. FailedToStartError), some nodes may be
RPC-connected, some may have a live process without RPC, and some may
already have exited. Stop the connected ones cleanly and force-kill the
rest so the framework's teardown can proceed.
"""
for node in self.nodes:
if not node.running:
continue
if node.rpc_connected:
node.stop_node(wait=node.rpc_timeout)
else:
node.process.kill()
node.process.wait(timeout=node.rpc_timeout)
node.process = None
node.stdout.close()
node.stderr.close()
node.running = False
def restart_node(self, i, extra_args=None, clear_addrman=False, *, expected_stderr=''): def restart_node(self, i, extra_args=None, clear_addrman=False, *, expected_stderr=''):
"""Stop and start a test node""" """Stop and start a test node"""
self.stop_node(i, expected_stderr=expected_stderr) self.stop_node(i, expected_stderr=expected_stderr)