Skip to content

Commit 998386d

Browse files
committed
Merge bitcoin/bitcoin#31866: test, refactor: Add TestNode.binaries to hold binary paths
d190f0f test, contrib: Fix signer/miner command line escaping (Ryan Ofsky) 0d2eefc test, refactor: Add TestNode.binaries to hold binary paths (Ryan Ofsky) Pull request description: Add new `TestNode.binaries` object to manage paths to bitcoin binaries. The `binaries` object makes it possible for the test framework to exercise the bitcoin wrapper executable introduced in bitcoin/bitcoin#31375 and also makes it easier in general to add new binaries, and new options and environment variables controlling how they are invoked, because logic for invoking them that was previously spread out is now consolidated in one place. These changes were originally part of #31375 but made that PR harder to review because they were unrelated to the other changes there. If this PR can get merged first, python changes in #31375 will be simple, and the test framework changes here should also get a higher quality review. ACKs for top commit: maflcko: re-review-ACK d190f0f 🍓 Sjors: ACK d190f0f vasild: ACK d190f0f Tree-SHA512: 5a6c0553cd2822585810d827ef1c1772cbf3097d3336daf733f8378dd3da79c00fc3721e50ed0f7455908fbd7a509e9739f9be33f588d6bc1aaa400b9d75c650
2 parents aa87e0b + d190f0f commit 998386d

File tree

6 files changed

+82
-38
lines changed

6 files changed

+82
-38
lines changed

contrib/signet/miner

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import logging
99
import math
1010
import os
1111
import re
12-
import struct
12+
import shlex
1313
import sys
1414
import time
1515
import subprocess
@@ -86,7 +86,7 @@ def finish_block(block, signet_solution, grind_cmd):
8686
block.solve()
8787
else:
8888
headhex = CBlockHeader.serialize(block).hex()
89-
cmd = grind_cmd.split(" ") + [headhex]
89+
cmd = shlex.split(grind_cmd) + [headhex]
9090
newheadhex = subprocess.run(cmd, stdout=subprocess.PIPE, input=b"", check=True).stdout.strip()
9191
newhead = from_hex(CBlockHeader(), newheadhex.decode('utf8'))
9292
block.nNonce = newhead.nNonce
@@ -479,7 +479,7 @@ def do_calibrate(args):
479479
header.nTime = i
480480
header.nNonce = 0
481481
headhex = header.serialize().hex()
482-
cmd = args.grind_cmd.split(" ") + [headhex]
482+
cmd = shlex.split(args.grind_cmd) + [headhex]
483483
newheadhex = subprocess.run(cmd, stdout=subprocess.PIPE, input=b"", check=True).stdout.strip()
484484

485485
avg = (time.time() - start) * 1.0 / TRIALS
@@ -549,7 +549,7 @@ def main():
549549

550550
args = parser.parse_args(sys.argv[1:])
551551

552-
args.bcli = lambda *a, input=b"", **kwargs: bitcoin_cli(args.cli.split(" "), list(a), input=input, **kwargs)
552+
args.bcli = lambda *a, input=b"", **kwargs: bitcoin_cli(shlex.split(args.cli), list(a), input=input, **kwargs)
553553

554554
if hasattr(args, "address") and hasattr(args, "descriptor"):
555555
args.derived_addresses = {}

test/functional/test_framework/test_framework.py

+62-20
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import sys
1919
import tempfile
2020
import time
21+
import types
2122

2223
from .address import create_deterministic_address_bcrt1_p2tr_op_true
2324
from .authproxy import JSONRPCException
@@ -56,6 +57,48 @@ def __init__(self, message):
5657
self.message = message
5758

5859

60+
class Binaries:
61+
"""Helper class to provide information about bitcoin binaries
62+
63+
Attributes:
64+
paths: Object returned from get_binary_paths() containing information
65+
which binaries and command lines to use from environment variables and
66+
the config file.
67+
bin_dir: An optional string containing a directory path to look for
68+
binaries, which takes precedence over the paths above, if specified.
69+
This is used by tests calling binaries from previous releases.
70+
"""
71+
def __init__(self, paths, bin_dir):
72+
self.paths = paths
73+
self.bin_dir = bin_dir
74+
75+
def daemon_argv(self):
76+
"Return argv array that should be used to invoke bitcoind"
77+
return self._argv(self.paths.bitcoind)
78+
79+
def rpc_argv(self):
80+
"Return argv array that should be used to invoke bitcoin-cli"
81+
return self._argv(self.paths.bitcoincli)
82+
83+
def util_argv(self):
84+
"Return argv array that should be used to invoke bitcoin-util"
85+
return self._argv(self.paths.bitcoinutil)
86+
87+
def wallet_argv(self):
88+
"Return argv array that should be used to invoke bitcoin-wallet"
89+
return self._argv(self.paths.bitcoinwallet)
90+
91+
def _argv(self, bin_path):
92+
"""Return argv array that should be used to invoke the command.
93+
Normally this will return binary paths directly from the paths object,
94+
but when bin_dir is set (by tests calling binaries from previous
95+
releases) it will return paths relative to bin_dir instead."""
96+
if self.bin_dir is not None:
97+
return [os.path.join(self.bin_dir, os.path.basename(bin_path))]
98+
else:
99+
return [bin_path]
100+
101+
59102
class BitcoinTestMetaClass(type):
60103
"""Metaclass for BitcoinTestFramework.
61104
@@ -220,6 +263,7 @@ def parse_args(self, test_file):
220263
config = configparser.ConfigParser()
221264
config.read_file(open(self.options.configfile))
222265
self.config = config
266+
self.binary_paths = self.get_binary_paths()
223267
if self.options.v1transport:
224268
self.options.v2transport=False
225269

@@ -239,9 +283,10 @@ def parse_args(self, test_file):
239283

240284
PortSeed.n = self.options.port_seed
241285

242-
def set_binary_paths(self):
243-
"""Update self.options with the paths of all binaries from environment variables or their default values"""
286+
def get_binary_paths(self):
287+
"""Get paths of all binaries from environment variables or their default values"""
244288

289+
paths = types.SimpleNamespace()
245290
binaries = {
246291
"bitcoind": ("bitcoind", "BITCOIND"),
247292
"bitcoin-cli": ("bitcoincli", "BITCOINCLI"),
@@ -254,7 +299,11 @@ def set_binary_paths(self):
254299
"bin",
255300
binary + self.config["environment"]["EXEEXT"],
256301
)
257-
setattr(self.options, attribute_name, os.getenv(env_variable_name, default=default_filename))
302+
setattr(paths, attribute_name, os.getenv(env_variable_name, default=default_filename))
303+
return paths
304+
305+
def get_binaries(self, bin_dir=None):
306+
return Binaries(self.binary_paths, bin_dir)
258307

259308
def setup(self):
260309
"""Call this method to start up the test framework object with options set."""
@@ -265,8 +314,6 @@ def setup(self):
265314

266315
config = self.config
267316

268-
self.set_binary_paths()
269-
270317
os.environ['PATH'] = os.pathsep.join([
271318
os.path.join(config['environment']['BUILDDIR'], 'bin'),
272319
os.environ['PATH']
@@ -473,14 +520,14 @@ def add_wallet_options(self, parser, *, descriptors=True, legacy=True):
473520
group.add_argument("--legacy-wallet", action='store_const', const=False, **kwargs,
474521
help="Run test using legacy wallets", dest='descriptors')
475522

476-
def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, binary=None, binary_cli=None, versions=None):
523+
def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, versions=None):
477524
"""Instantiate TestNode objects.
478525
479526
Should only be called once after the nodes have been specified in
480527
set_test_params()."""
481-
def get_bin_from_version(version, bin_name, bin_default):
528+
def bin_dir_from_version(version):
482529
if not version:
483-
return bin_default
530+
return None
484531
if version > 219999:
485532
# Starting at client version 220000 the first two digits represent
486533
# the major version, e.g. v22.0 instead of v0.22.0.
@@ -498,7 +545,6 @@ def get_bin_from_version(version, bin_name, bin_default):
498545
),
499546
),
500547
'bin',
501-
bin_name,
502548
)
503549

504550
if self.bind_to_localhost_only:
@@ -513,13 +559,12 @@ def get_bin_from_version(version, bin_name, bin_default):
513559
extra_args[i] = extra_args[i] + ["-whitelist=noban,in,[email protected]"]
514560
if versions is None:
515561
versions = [None] * num_nodes
516-
if binary is None:
517-
binary = [get_bin_from_version(v, 'bitcoind', self.options.bitcoind) for v in versions]
518-
if binary_cli is None:
519-
binary_cli = [get_bin_from_version(v, 'bitcoin-cli', self.options.bitcoincli) for v in versions]
562+
bin_dirs = [bin_dir_from_version(v) for v in versions]
520563
# Fail test if any of the needed release binaries is missing
521564
bins_missing = False
522-
for bin_path in binary + binary_cli:
565+
for bin_path in (argv[0] for bin_dir in bin_dirs
566+
for binaries in (self.get_binaries(bin_dir),)
567+
for argv in (binaries.daemon_argv(), binaries.rpc_argv())):
523568
if shutil.which(bin_path) is None:
524569
self.log.error(f"Binary not found: {bin_path}")
525570
bins_missing = True
@@ -529,8 +574,7 @@ def get_bin_from_version(version, bin_name, bin_default):
529574
assert_equal(len(extra_confs), num_nodes)
530575
assert_equal(len(extra_args), num_nodes)
531576
assert_equal(len(versions), num_nodes)
532-
assert_equal(len(binary), num_nodes)
533-
assert_equal(len(binary_cli), num_nodes)
577+
assert_equal(len(bin_dirs), num_nodes)
534578
for i in range(num_nodes):
535579
args = list(extra_args[i])
536580
test_node_i = TestNode(
@@ -540,8 +584,7 @@ def get_bin_from_version(version, bin_name, bin_default):
540584
rpchost=rpchost,
541585
timewait=self.rpc_timeout,
542586
timeout_factor=self.options.timeout_factor,
543-
bitcoind=binary[i],
544-
bitcoin_cli=binary_cli[i],
587+
binaries=self.get_binaries(bin_dirs[i]),
545588
version=versions[i],
546589
coverage_dir=self.options.coveragedir,
547590
cwd=self.options.tmpdir,
@@ -852,8 +895,7 @@ def _initialize_chain(self):
852895
rpchost=None,
853896
timewait=self.rpc_timeout,
854897
timeout_factor=self.options.timeout_factor,
855-
bitcoind=self.options.bitcoind,
856-
bitcoin_cli=self.options.bitcoincli,
898+
binaries=self.get_binaries(),
857899
coverage_dir=None,
858900
cwd=self.options.tmpdir,
859901
descriptors=self.options.descriptors,

test/functional/test_framework/test_node.py

+9-10
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class TestNode():
7676
To make things easier for the test writer, any unrecognised messages will
7777
be dispatched to the RPC connection."""
7878

79-
def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False, v2transport=False):
79+
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, descriptors=False, v2transport=False):
8080
"""
8181
Kwargs:
8282
start_perf (bool): If True, begin profiling the node with `perf` as soon as
@@ -92,7 +92,7 @@ def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor,
9292
self.chain = chain
9393
self.rpchost = rpchost
9494
self.rpc_timeout = timewait
95-
self.binary = bitcoind
95+
self.binaries = binaries
9696
self.coverage_dir = coverage_dir
9797
self.cwd = cwd
9898
self.descriptors = descriptors
@@ -109,8 +109,7 @@ def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor,
109109
# Configuration for logging is set as command-line args rather than in the bitcoin.conf file.
110110
# This means that starting a bitcoind using the temp dir to debug a failed test won't
111111
# spam debug.log.
112-
self.args = [
113-
self.binary,
112+
self.args = self.binaries.daemon_argv() + [
114113
f"-datadir={self.datadir_path}",
115114
"-logtimemicros",
116115
"-debug",
@@ -149,7 +148,7 @@ def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor,
149148
self.args.append("-v2transport=0")
150149
# if v2transport is requested via global flag but not supported for node version, ignore it
151150

152-
self.cli = TestNodeCLI(bitcoin_cli, self.datadir_path)
151+
self.cli = TestNodeCLI(binaries, self.datadir_path)
153152
self.use_cli = use_cli
154153
self.start_perf = start_perf
155154

@@ -870,16 +869,16 @@ def arg_to_cli(arg):
870869

871870
class TestNodeCLI():
872871
"""Interface to bitcoin-cli for an individual node"""
873-
def __init__(self, binary, datadir):
872+
def __init__(self, binaries, datadir):
874873
self.options = []
875-
self.binary = binary
874+
self.binaries = binaries
876875
self.datadir = datadir
877876
self.input = None
878877
self.log = logging.getLogger('TestFramework.bitcoincli')
879878

880879
def __call__(self, *options, input=None):
881880
# TestNodeCLI is callable with bitcoin-cli command-line options
882-
cli = TestNodeCLI(self.binary, self.datadir)
881+
cli = TestNodeCLI(self.binaries, self.datadir)
883882
cli.options = [str(o) for o in options]
884883
cli.input = input
885884
return cli
@@ -900,7 +899,7 @@ def send_cli(self, clicommand=None, *args, **kwargs):
900899
"""Run bitcoin-cli command. Deserializes returned string as python object."""
901900
pos_args = [arg_to_cli(arg) for arg in args]
902901
named_args = [str(key) + "=" + arg_to_cli(value) for (key, value) in kwargs.items()]
903-
p_args = [self.binary, f"-datadir={self.datadir}"] + self.options
902+
p_args = self.binaries.rpc_argv() + [f"-datadir={self.datadir}"] + self.options
904903
if named_args:
905904
p_args += ["-named"]
906905
if clicommand is not None:
@@ -916,7 +915,7 @@ def send_cli(self, clicommand=None, *args, **kwargs):
916915
code, message = match.groups()
917916
raise JSONRPCException(dict(code=int(code), message=message))
918917
# Ignore cli_stdout, raise with cli_stderr
919-
raise subprocess.CalledProcessError(returncode, self.binary, output=cli_stderr)
918+
raise subprocess.CalledProcessError(returncode, p_args, output=cli_stderr)
920919
try:
921920
return json.loads(cli_stdout, parse_float=decimal.Decimal)
922921
except (json.JSONDecodeError, decimal.InvalidOperation):

test/functional/tool_signet_miner.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""Test signet miner tool"""
66

77
import os.path
8+
import shlex
89
import subprocess
910
import sys
1011
import time
@@ -49,13 +50,15 @@ def run_test(self):
4950
# generate block with signet miner tool
5051
base_dir = self.config["environment"]["SRCDIR"]
5152
signet_miner_path = os.path.join(base_dir, "contrib", "signet", "miner")
53+
rpc_argv = node.binaries.rpc_argv() + [f"-datadir={node.cli.datadir}"]
54+
util_argv = node.binaries.util_argv() + ["grind"]
5255
subprocess.run([
5356
sys.executable,
5457
signet_miner_path,
55-
f'--cli={node.cli.binary} -datadir={node.cli.datadir}',
58+
f'--cli={shlex.join(rpc_argv)}',
5659
'generate',
5760
f'--address={node.getnewaddress()}',
58-
f'--grind-cmd={self.options.bitcoinutil} grind',
61+
f'--grind-cmd={shlex.join(util_argv)}',
5962
f'--nbits={DIFF_1_N_BITS:08x}',
6063
f'--set-block-time={int(time.time())}',
6164
'--poolnum=99',

test/functional/tool_wallet.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def bitcoin_wallet_process(self, *args):
4848
if "dump" in args and self.options.bdbro:
4949
default_args.append("-withinternalbdb")
5050

51-
return subprocess.Popen([self.options.bitcoinwallet] + default_args + list(args), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
51+
return subprocess.Popen(self.get_binaries().wallet_argv() + default_args + list(args), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
5252

5353
def assert_raises_tool_error(self, error, *args):
5454
p = self.bitcoin_wallet_process(*args)

test/functional/wallet_encryption.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def run_test(self):
112112

113113
def do_wallet_tool(*args):
114114
proc = subprocess.Popen(
115-
[self.options.bitcoinwallet, f"-datadir={self.nodes[0].datadir_path}", f"-chain={self.chain}"] + list(args),
115+
self.get_binaries().wallet_argv() + [f"-datadir={self.nodes[0].datadir_path}", f"-chain={self.chain}"] + list(args),
116116
stdin=subprocess.PIPE,
117117
stdout=subprocess.PIPE,
118118
stderr=subprocess.PIPE,

0 commit comments

Comments
 (0)