This commit is contained in:
Ryan Ofsky 2025-01-08 20:44:24 +01:00 committed by GitHub
commit c61233b883
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 297 additions and 37 deletions

View file

@ -76,6 +76,7 @@ list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/module)
#=============================
include(CMakeDependentOption)
# When adding a new option, end the <help_text> with a full stop for consistency.
option(BUILD_BITCOIN_BIN "Build bitcoin executable." ON)
option(BUILD_DAEMON "Build bitcoind executable." ON)
option(BUILD_GUI "Build bitcoin-qt executable." OFF)
option(BUILD_CLI "Build bitcoin-cli executable." ON)
@ -210,6 +211,7 @@ target_link_libraries(core_interface INTERFACE
if(BUILD_FOR_FUZZING)
message(WARNING "BUILD_FOR_FUZZING=ON will disable all other targets and force BUILD_FUZZ_BINARY=ON.")
set(BUILD_BITCOIN_BIN OFF)
set(BUILD_DAEMON OFF)
set(BUILD_CLI OFF)
set(BUILD_TX OFF)
@ -602,6 +604,7 @@ message("\n")
message("Configure summary")
message("=================")
message("Executables:")
message(" bitcoin ............................. ${BUILD_BITCOIN_BIN}")
message(" bitcoind ............................ ${BUILD_DAEMON}")
message(" bitcoin-node (multiprocess) ......... ${WITH_MULTIPROCESS}")
message(" bitcoin-qt (GUI) .................... ${BUILD_GUI}")

View file

@ -20,4 +20,4 @@ export BITCOIN_CONFIG="\
-DCMAKE_CXX_FLAGS='-Wno-error=documentation' \
-DAPPEND_CPPFLAGS='-DBOOST_MULTI_INDEX_ENABLE_SAFE_MODE' \
"
export BITCOIND=bitcoin-node # Used in functional tests
export BITCOIN_CMD="bitcoin -m" # Used in functional tests

View file

@ -9,6 +9,7 @@ import logging
import math
import os
import re
import shlex
import struct
import sys
import time
@ -86,7 +87,7 @@ def finish_block(block, signet_solution, grind_cmd):
block.solve()
else:
headhex = CBlockHeader.serialize(block).hex()
cmd = grind_cmd.split(" ") + [headhex]
cmd = shlex.split(grind_cmd) + [headhex]
newheadhex = subprocess.run(cmd, stdout=subprocess.PIPE, input=b"", check=True).stdout.strip()
newhead = from_hex(CBlockHeader(), newheadhex.decode('utf8'))
block.nNonce = newhead.nNonce
@ -479,7 +480,7 @@ def do_calibrate(args):
header.nTime = i
header.nNonce = 0
headhex = header.serialize().hex()
cmd = args.grind_cmd.split(" ") + [headhex]
cmd = shlex.split(args.grind_cmd) + [headhex]
newheadhex = subprocess.run(cmd, stdout=subprocess.PIPE, input=b"", check=True).stdout.strip()
avg = (time.time() - start) * 1.0 / TRIALS
@ -549,7 +550,7 @@ def main():
args = parser.parse_args(sys.argv[1:])
args.bcli = lambda *a, input=b"", **kwargs: bitcoin_cli(args.cli.split(" "), list(a), input=input, **kwargs)
args.bcli = lambda *a, input=b"", **kwargs: bitcoin_cli(shlex.split(args.cli), list(a), input=input, **kwargs)
if hasattr(args, "address") and hasattr(args, "descriptor"):
args.derived_addresses = {}

View file

@ -305,6 +305,11 @@ target_link_libraries(bitcoin_node
$<TARGET_NAME_IF_EXISTS:USDT::headers>
)
# Bitcoin wrapper executable that can call other executables.
if(BUILD_BITCOIN_BIN)
add_executable(bitcoin bitcoin.cpp)
target_link_libraries(bitcoin core_interface bitcoin_util)
endif()
# Bitcoin Core bitcoind.
if(BUILD_DAEMON)

213
src/bitcoin.cpp Normal file
View file

@ -0,0 +1,213 @@
// Copyright (c) 2024 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <bitcoin-build-config.h> // IWYU pragma: keep
#include <clientversion.h>
#include <util/fs.h>
#include <util/strencodings.h>
#include <iostream>
#include <optional>
#include <string>
#include <tinyformat.h>
#include <vector>
#ifdef WIN32
#include <process.h>
#include <windows.h>
#define execvp _execvp
#else
#include <unistd.h>
#endif
extern const std::function<std::string(const char*)> G_TRANSLATION_FUN = nullptr;
static constexpr auto HELP_USAGE = R"(Usage: %1$s [OPTIONS] COMMAND...
Commands (run help command for more information):
{gui,daemon,rpc,wallet,test,help}
Options:
-m, --multiprocess Run multiprocess binaries bitcoin-node, bitcoin-gui.\n"
-M, --monolothic Run monolithic binaries bitcoind, bitcoin-qt. (Default behavior)\n"
-v, --version Show version information
-h, --help Show this help message
)";
static constexpr auto HELP_COMMANDS = R"(Command overview:
%1$s gui [ARGS] Start GUI, equivalent to running 'bitcoin-qt [ARGS]' or 'bitcoin-gui [ARGS]'.
%1$s daemon [ARGS] Start daemon, equivalent to running 'bitcoind [ARGS]' or 'bitcoin-node [ARGS]'.
%1$s rpc [ARGS] Call RPC method, equivalent to running 'bitcoin-cli -named [ARGS]'.
%1$s wallet [ARGS] Call wallet command, equivalent to running 'bitcoin-wallet [ARGS]'.
%1$s tx [ARGS] Manipulate hex-encoded transactions, equivalent to running 'bitcoin-tx [ARGS]'.
%1$s test [ARGS] Run unit tests, equivalent to running 'test_bitcoin [ARGS]'.
%1$s help Show this help message.
)";
struct CommandLine {
bool use_multiprocess{false};
bool show_version{false};
bool show_help{false};
std::string_view command;
std::vector<std::string_view> args;
};
CommandLine ParseCommandLine(int argc, char* argv[]);
void ExecCommand(const std::vector<std::string>& args, std::string_view argv0);
int main(int argc, char* argv[])
{
try {
CommandLine cmd{ParseCommandLine(argc, argv)};
if (cmd.show_version) {
tfm::format(std::cout, "%s version %s\n%s", CLIENT_NAME, FormatFullVersion(), FormatParagraph(LicenseInfo()));
return EXIT_SUCCESS;
}
if (cmd.show_help || cmd.command.empty()) {
tfm::format(std::cout, HELP_USAGE, argv[0]);
}
std::vector<std::string> args;
if (cmd.command == "gui") {
args.emplace_back(cmd.use_multiprocess ? "qt/bitcoin-gui" : "qt/bitcoin-qt");
} else if (cmd.command == "daemon") {
args.emplace_back(cmd.use_multiprocess ? "bitcoin-node" : "bitcoind");
} else if (cmd.command == "rpc") {
args.emplace_back("bitcoin-cli");
args.emplace_back("-named");
} else if (cmd.command == "wallet") {
args.emplace_back("bitcoin-wallet");
} else if (cmd.command == "tx") {
args.emplace_back("bitcoin-tx");
} else if (cmd.command == "test") {
args.emplace_back("test/test_bitcoin");
} else if (cmd.command == "help") {
tfm::format(std::cout, HELP_COMMANDS, argv[0]);
} else if (cmd.command == "mine") { // undocumented, used by tests
args.emplace_back("bitcoin-mine");
} else if (cmd.command == "util") { // undocumented, used by tests
args.emplace_back("bitcoin-util");
} else if (!cmd.command.empty()){
throw std::runtime_error(strprintf("Unrecognized command: '%s'", cmd.command));
}
if (!args.empty()) {
args.insert(args.end(), cmd.args.begin(), cmd.args.end());
ExecCommand(args, argv[0]);
}
} catch (const std::exception& e) {
tfm::format(std::cerr, "Error: %s\nTry '%s --help' for more information.\n", e.what(), argv[0]);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
CommandLine ParseCommandLine(int argc, char* argv[])
{
CommandLine cmd;
cmd.args.reserve(argc);
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (!cmd.command.empty()) {
cmd.args.emplace_back(arg);
} else if (arg == "-m" || arg == "--multiprocess") {
cmd.use_multiprocess = true;
} else if (arg == "-M" || arg == "--monolithic") {
cmd.use_multiprocess = false;
} else if (arg == "-v" || arg == "--version") {
cmd.show_version = true;
} else if (arg == "-h" || arg == "--help") {
cmd.show_help = true;
} else if (arg.starts_with("-")) {
throw std::runtime_error(strprintf("Unknown option: %s", arg));
} else if (!arg.empty()) {
cmd.command = arg;
}
}
return cmd;
}
// Execute the specified bitcoind, bitcoin-qt or other command line in `args`
// using src, bin and libexec directory paths relative to this executable, where
// the path to this executable is specified `argv0`.
//
// This function doesn't currently print anything but can be debugged from
// the command line using strace like:
//
// strace -e trace=execve -s 10000 build/src/bitcoin ...
void ExecCommand(const std::vector<std::string>& args, std::string_view argv0)
{
// Construct argument string for execvp
std::vector<const char*> cstr_args{};
cstr_args.reserve(args.size() + 1);
for (const auto& arg : args) {
cstr_args.emplace_back(arg.c_str());
}
cstr_args.emplace_back(nullptr);
// Try to call execvp with given exe path.
auto try_exec = [&](fs::path exe_path, bool allow_notfound = true) {
std::string exe_path_str{fs::PathToString(exe_path)};
cstr_args[0] = exe_path_str.c_str();
if (execvp(cstr_args[0], (char*const*)cstr_args.data()) == -1) {
if (allow_notfound && errno == ENOENT) return false;
throw std::system_error(errno, std::system_category(), strprintf("execvp failed to execute '%s'", cstr_args[0]));
}
return true; // Will not actually be reached if execvp succeeds
};
// Try to figure out where current executable is located. This is a
// simplified search that won't work perfectly on every platform and doesn't
// need to, as it is only trying to prioritize locally built or installed
// executables over system executables. We may want to add options to
// override this behavior in the future, though.
const fs::path argv0_path{fs::PathFromString(std::string{argv0})};
fs::path exe_path{argv0_path};
std::error_code ec;
#ifndef WIN32
if (argv0.find('/') == std::string_view::npos) {
if (const char* path_env = std::getenv("PATH")) {
size_t start{0}, end{0};
for (std::string_view paths{path_env}; end != std::string_view::npos; start = end + 1) {
end = paths.find(':', start);
fs::path candidate = fs::path(paths.substr(start, end - start)) / argv0_path;
if (fs::exists(candidate, ec) && fs::is_regular_file(candidate, ec)) {
exe_path = candidate;
break;
}
}
}
}
#else
wchar_t module_path[MAX_PATH];
if (GetModuleFileNameW(nullptr, module_path, MAX_PATH) > 0) {
exe_path = fs::path{module_path};
} else {
tfm::format(std::cerr, "Warning: Failed to get executable path. Error: %s\n", GetLastError());
}
#endif
// Try to resolve any symlinks and figure out actual directory containing this executable.
fs::path exe_dir{fs::weakly_canonical(exe_path, ec)};
if (exe_dir.empty()) exe_dir = exe_path; // Restore previous path if weakly_canonical failed.
exe_dir = exe_dir.parent_path();
// Search for executables on system PATH only if this executable was invoked
// from the PATH, to avoid unintentionally launching system executables in a
// local build
// (https://github.com/bitcoin/bitcoin/pull/31375#discussion_r1861814807)
const bool use_system_path{!argv0_path.has_parent_path()};
const fs::path arg0{fs::PathFromString(args[0])};
// If exe is in a CMake build tree, first look for target executable
// relative to it.
(exe_dir.filename() == "src" && try_exec(exe_dir / arg0)) ||
// Otherwise if exe is installed in a bin/ directory, first look for target
// executable in libexec/
(exe_dir.filename() == "bin" && try_exec(fs::path{exe_dir.parent_path()} / "libexec" / arg0.filename())) ||
// Otherwise look for target executable next to current exe
try_exec(exe_dir / arg0.filename(), use_system_path) ||
// Otherwise just look on the system path.
(use_system_path && try_exec(arg0.filename(), false));
};

View file

@ -90,6 +90,10 @@ static inline bool exists(const path& p)
{
return std::filesystem::exists(p);
}
static inline bool exists(const path& p, std::error_code& ec) noexcept
{
return std::filesystem::exists(p, ec);
}
// Allow explicit quoted stream I/O.
static inline auto quoted(const std::string& s)

View file

@ -13,11 +13,13 @@ import platform
import pdb
import random
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
import time
import types
from .address import create_deterministic_address_bcrt1_p2tr_op_true
from .authproxy import JSONRPCException
@ -56,6 +58,33 @@ class SkipTest(Exception):
self.message = message
class BitcoinEnv:
def __init__(self, paths, bin_path=None):
self.paths = paths
self.bin_path = bin_path
def daemon_args(self):
return self.args("daemon", "bitcoind")
def rpc_args(self):
# Add -nonamed because "bitcoin rpc" enables -named by default, but bitcoin-cli doesn't
return self.args("rpc", "bitcoincli") + ["-nonamed"]
def util_args(self):
return self.args("util", "bitcoinutil")
def wallet_args(self):
return self.args("wallet", "bitcoinwallet")
def args(self, command, path_attr):
if self.bin_path is not None:
return [os.path.join(self.bin_path, os.path.basename(getattr(self.paths, path_attr)))]
elif self.paths.bitcoin_cmd is not None:
return self.paths.bitcoin_cmd + [command]
else:
return [getattr(self.paths, path_attr)]
class BitcoinTestMetaClass(type):
"""Metaclass for BitcoinTestFramework.
@ -222,6 +251,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
config = configparser.ConfigParser()
config.read_file(open(self.options.configfile))
self.config = config
self.paths = self.get_binary_paths()
if self.options.v1transport:
self.options.v2transport=False
@ -245,9 +275,10 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
PortSeed.n = self.options.port_seed
def set_binary_paths(self):
"""Update self.options with the paths of all binaries from environment variables or their default values"""
def get_binary_paths(self):
"""Get paths of all binaries from environment variables or their default values"""
paths = types.SimpleNamespace()
binaries = {
"bitcoind": ("bitcoind", "BITCOIND"),
"bitcoin-cli": ("bitcoincli", "BITCOINCLI"),
@ -260,7 +291,12 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
"src",
binary + self.config["environment"]["EXEEXT"],
)
setattr(self.options, attribute_name, os.getenv(env_variable_name, default=default_filename))
setattr(paths, attribute_name, os.getenv(env_variable_name, default=default_filename))
# BITCOIN_CMD environment variable can be specified to invoke bitcoin
# binary wrapper binary instead of other executables.
paths.bitcoin_cmd = shlex.split(os.getenv("BITCOIN_CMD", "")) or None
return paths
def setup(self):
"""Call this method to start up the test framework object with options set."""
@ -271,8 +307,6 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
config = self.config
self.set_binary_paths()
os.environ['PATH'] = os.pathsep.join([
os.path.join(config['environment']['BUILDDIR'], 'src'),
os.path.join(config['environment']['BUILDDIR'], 'src', 'qt'), os.environ['PATH']
@ -482,14 +516,14 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
group.add_argument("--legacy-wallet", action='store_const', const=False, **kwargs,
help="Run test using legacy wallets", dest='descriptors')
def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, binary=None, binary_cli=None, versions=None):
def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, versions=None):
"""Instantiate TestNode objects.
Should only be called once after the nodes have been specified in
set_test_params()."""
def get_bin_from_version(version, bin_name, bin_default):
def bin_path_from_version(version):
if not version:
return bin_default
return None
if version > 219999:
# Starting at client version 220000 the first two digits represent
# the major version, e.g. v22.0 instead of v0.22.0.
@ -507,7 +541,6 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
),
),
'bin',
bin_name,
)
if self.bind_to_localhost_only:
@ -522,15 +555,11 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
extra_args[i] = extra_args[i] + ["-whitelist=noban,in,out@127.0.0.1"]
if versions is None:
versions = [None] * num_nodes
if binary is None:
binary = [get_bin_from_version(v, 'bitcoind', self.options.bitcoind) for v in versions]
if binary_cli is None:
binary_cli = [get_bin_from_version(v, 'bitcoin-cli', self.options.bitcoincli) for v in versions]
bin_path = [bin_path_from_version(v) for v in versions]
assert_equal(len(extra_confs), num_nodes)
assert_equal(len(extra_args), num_nodes)
assert_equal(len(versions), num_nodes)
assert_equal(len(binary), num_nodes)
assert_equal(len(binary_cli), num_nodes)
assert_equal(len(bin_path), num_nodes)
for i in range(num_nodes):
args = list(extra_args[i])
test_node_i = TestNode(
@ -540,8 +569,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
rpchost=rpchost,
timewait=self.rpc_timeout,
timeout_factor=self.options.timeout_factor,
bitcoind=binary[i],
bitcoin_cli=binary_cli[i],
env=BitcoinEnv(self.paths, bin_path[i]),
version=versions[i],
coverage_dir=self.options.coveragedir,
cwd=self.options.tmpdir,
@ -857,8 +885,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
rpchost=None,
timewait=self.rpc_timeout,
timeout_factor=self.options.timeout_factor,
bitcoind=self.options.bitcoind,
bitcoin_cli=self.options.bitcoincli,
env=BitcoinEnv(self.paths),
coverage_dir=None,
cwd=self.options.tmpdir,
descriptors=self.options.descriptors,

View file

@ -75,7 +75,7 @@ class TestNode():
To make things easier for the test writer, any unrecognised messages will
be dispatched to the RPC connection."""
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):
def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, env, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False, v2transport=False):
"""
Kwargs:
start_perf (bool): If True, begin profiling the node with `perf` as soon as
@ -91,7 +91,7 @@ class TestNode():
self.chain = chain
self.rpchost = rpchost
self.rpc_timeout = timewait
self.binary = bitcoind
self.env = env
self.coverage_dir = coverage_dir
self.cwd = cwd
self.descriptors = descriptors
@ -108,8 +108,7 @@ class TestNode():
# 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
# spam debug.log.
self.args = [
self.binary,
self.args = self.env.daemon_args() + [
f"-datadir={self.datadir_path}",
"-logtimemicros",
"-debug",
@ -148,7 +147,7 @@ class TestNode():
self.args.append("-v2transport=0")
# if v2transport is requested via global flag but not supported for node version, ignore it
self.cli = TestNodeCLI(bitcoin_cli, self.datadir_path)
self.cli = TestNodeCLI(env, self.datadir_path)
self.use_cli = use_cli
self.start_perf = start_perf
@ -865,16 +864,16 @@ def arg_to_cli(arg):
class TestNodeCLI():
"""Interface to bitcoin-cli for an individual node"""
def __init__(self, binary, datadir):
def __init__(self, env, datadir):
self.options = []
self.binary = binary
self.env = env
self.datadir = datadir
self.input = None
self.log = logging.getLogger('TestFramework.bitcoincli')
def __call__(self, *options, input=None):
# TestNodeCLI is callable with bitcoin-cli command-line options
cli = TestNodeCLI(self.binary, self.datadir)
cli = TestNodeCLI(self.env, self.datadir)
cli.options = [str(o) for o in options]
cli.input = input
return cli
@ -895,7 +894,7 @@ class TestNodeCLI():
"""Run bitcoin-cli command. Deserializes returned string as python object."""
pos_args = [arg_to_cli(arg) for arg in args]
named_args = [str(key) + "=" + arg_to_cli(value) for (key, value) in kwargs.items()]
p_args = [self.binary, f"-datadir={self.datadir}"] + self.options
p_args = self.env.rpc_args() + [f"-datadir={self.datadir}"] + self.options
if named_args:
p_args += ["-named"]
if clicommand is not None:
@ -911,7 +910,7 @@ class TestNodeCLI():
code, message = match.groups()
raise JSONRPCException(dict(code=int(code), message=message))
# Ignore cli_stdout, raise with cli_stderr
raise subprocess.CalledProcessError(returncode, self.binary, output=cli_stderr)
raise subprocess.CalledProcessError(returncode, p_args, output=cli_stderr)
try:
return json.loads(cli_stdout, parse_float=decimal.Decimal)
except (json.JSONDecodeError, decimal.InvalidOperation):

View file

@ -5,6 +5,7 @@
"""Test signet miner tool"""
import os.path
import shlex
import subprocess
import sys
import time
@ -48,13 +49,15 @@ class SignetMinerTest(BitcoinTestFramework):
# generate block with signet miner tool
base_dir = self.config["environment"]["SRCDIR"]
signet_miner_path = os.path.join(base_dir, "contrib", "signet", "miner")
rpc_args = node.env.rpc_args() + [f"-datadir={node.cli.datadir}"]
util_args = node.env.util_args() + ["grind"]
subprocess.run([
sys.executable,
signet_miner_path,
f'--cli={node.cli.binary} -datadir={node.cli.datadir}',
f'--cli={shlex.join(rpc_args)}',
'generate',
f'--address={node.getnewaddress()}',
f'--grind-cmd={self.options.bitcoinutil} grind',
f'--grind-cmd={shlex.join(util_args)}',
'--nbits=1d00ffff',
f'--set-block-time={int(time.time())}',
'--poolnum=99',

View file

@ -12,7 +12,10 @@ import textwrap
from collections import OrderedDict
from test_framework.test_framework import BitcoinTestFramework
from test_framework.test_framework import (
BitcoinEnv,
BitcoinTestFramework,
)
from test_framework.util import (
assert_equal,
assert_greater_than,
@ -44,7 +47,7 @@ class ToolWalletTest(BitcoinTestFramework):
if "dump" in args and self.options.bdbro:
default_args.append("-withinternalbdb")
return subprocess.Popen([self.options.bitcoinwallet] + default_args + list(args), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return subprocess.Popen(BitcoinEnv(self.paths).wallet_args() + default_args + list(args), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
def assert_raises_tool_error(self, error, *args):
p = self.bitcoin_wallet_process(*args)

View file

@ -13,6 +13,8 @@ import re
import sys
FALSE_POSITIVES = [
("src/bitcoin.cpp", "tfm::format(std::cout, HELP_USAGE, argv[0])"),
("src/bitcoin.cpp", "tfm::format(std::cout, HELP_COMMANDS, argv[0])"),
("src/clientversion.cpp", "strprintf(_(COPYRIGHT_HOLDERS), COPYRIGHT_HOLDERS_SUBSTITUTION)"),
("src/test/translation_tests.cpp", "strprintf(format, arg)"),
("src/test/util_string_tests.cpp", 'tfm::format(ConstevalFormatString<2>{"%*s"}, "hi", "hi")'),