Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2697,6 +2697,8 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
CService onion_service_target;
if (!connOptions.onion_binds.empty()) {
onion_service_target = connOptions.onion_binds.front();
} else if (!connOptions.vBinds.empty()) {
onion_service_target = connOptions.vBinds.front();
} else {
onion_service_target = DefaultOnionServiceTarget();
connOptions.onion_binds.push_back(onion_service_target);
Expand Down
2 changes: 1 addition & 1 deletion src/logging.h
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ std::string SafeStringFormat(const std::string& fmt, const Args&... args)
}
}

// Be conservative when using LogPrintf/error or other things which
// Be conservative when using functions that
// unconditionally log to debug.log! It should not be the case that an inbound
// peer can fill up a user's disk with debug.log entries.

Expand Down
28 changes: 20 additions & 8 deletions src/net.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3942,24 +3942,36 @@ bool CConnman::Bind(const CService& addr_, unsigned int flags, NetPermissionFlag

bool CConnman::InitBinds(const Options& options)
{
bool fBound = false;
for (const auto& addrBind : options.vBinds) {
fBound |= Bind(addrBind, BF_REPORT_ERROR, NetPermissionFlags::None);
if (!Bind(addrBind, BF_REPORT_ERROR, NetPermissionFlags::None)) {
return false;
}
}
for (const auto& addrBind : options.vWhiteBinds) {
fBound |= Bind(addrBind.m_service, BF_REPORT_ERROR, addrBind.m_flags);
if (!Bind(addrBind.m_service, BF_REPORT_ERROR, addrBind.m_flags)) {
return false;
}
}
for (const auto& addr_bind : options.onion_binds) {
fBound |= Bind(addr_bind, BF_DONT_ADVERTISE, NetPermissionFlags::None);
if (!Bind(addr_bind, BF_REPORT_ERROR | BF_DONT_ADVERTISE, NetPermissionFlags::None)) {
return false;
Comment on lines +3956 to +3957

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep implicit onion bind conflicts non-fatal

AppInitMain() still inserts DefaultOnionServiceTarget() into options.onion_binds when no -bind/-whitebind is supplied, even if the user sets -listenonion=0; making every onion bind failure fatal here means a conflict on the implicit 127.0.0.1 onion target port aborts startup before the later 0.0.0.0 bind can succeed. This regresses running another node with a different -port and onion listening disabled, where the implicit Tor target is occupied but normal P2P listening would otherwise work; only explicit onion binds should be fatal, or the default should be skipped when onion listening is off.

Useful? React with 👍 / 👎.

}
}
if (options.bind_on_any) {
// Don't consider errors to bind on IPv6 "::" fatal because the host OS
// may not have IPv6 support and the user did not explicitly ask us to
// bind on that.
const CService ipv6_any{in6_addr(IN6ADDR_ANY_INIT), GetListenPort()}; // ::
Bind(ipv6_any, BF_NONE, NetPermissionFlags::None);

struct in_addr inaddr_any;
inaddr_any.s_addr = htonl(INADDR_ANY);
struct in6_addr inaddr6_any = IN6ADDR_ANY_INIT;
fBound |= Bind(CService(inaddr6_any, GetListenPort()), BF_NONE, NetPermissionFlags::None);
fBound |= Bind(CService(inaddr_any, GetListenPort()), !fBound ? BF_REPORT_ERROR : BF_NONE, NetPermissionFlags::None);
const CService ipv4_any{inaddr_any, GetListenPort()}; // 0.0.0.0
if (!Bind(ipv4_any, BF_REPORT_ERROR, NetPermissionFlags::None)) {
return false;
}
}
return fBound;
return true;
}

bool CConnman::Start(CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_metaman, CMasternodeSync& mn_sync,
Expand Down
28 changes: 14 additions & 14 deletions src/test/util/net.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,20 +118,20 @@ std::vector<NodeEvictionCandidate> GetRandomNodeEvictionCandidates(int n_candida
std::vector<NodeEvictionCandidate> candidates;
for (int id = 0; id < n_candidates; ++id) {
candidates.push_back({
/*id=*/id,
/*m_connected=*/std::chrono::seconds{random_context.randrange(100)},
/*m_min_ping_time=*/std::chrono::microseconds{random_context.randrange(100)},
/*m_last_block_time=*/std::chrono::seconds{random_context.randrange(100)},
/*m_last_tx_time=*/std::chrono::seconds{random_context.randrange(100)},
/*fRelevantServices=*/random_context.randbool(),
/*m_relay_txs=*/random_context.randbool(),
/*fBloomFilter=*/random_context.randbool(),
/*nKeyedNetGroup=*/random_context.randrange(100),
/*prefer_evict=*/random_context.randbool(),
/*m_is_local=*/random_context.randbool(),
/*m_network=*/ALL_NETWORKS[random_context.randrange(ALL_NETWORKS.size())],
/*m_noban=*/false,
/*m_conn_type=*/ConnectionType::INBOUND,
.id=id,
.m_connected=std::chrono::seconds{random_context.randrange(100)},
.m_min_ping_time=std::chrono::microseconds{random_context.randrange(100)},
.m_last_block_time=std::chrono::seconds{random_context.randrange(100)},
.m_last_tx_time=std::chrono::seconds{random_context.randrange(100)},
.fRelevantServices=random_context.randbool(),
.m_relay_txs=random_context.randbool(),
.fBloomFilter=random_context.randbool(),
.nKeyedNetGroup=random_context.randrange(100),
.prefer_evict=random_context.randbool(),
.m_is_local=random_context.randbool(),
.m_network=ALL_NETWORKS[random_context.randrange(ALL_NETWORKS.size())],
.m_noban=false,
.m_conn_type=ConnectionType::INBOUND,
});
}
return candidates;
Expand Down
23 changes: 14 additions & 9 deletions test/functional/feature_bind_extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def set_test_params(self):
# Avoid any -bind= on the command line. Force the framework to avoid
# adding -bind=127.0.0.1.
self.bind_to_localhost_only = False
self.num_nodes = 2
self.num_nodes = 3

def setup_network(self):
# Due to OS-specific network stats queries, we only run on Linux.
Expand Down Expand Up @@ -64,14 +64,21 @@ def setup_network(self):
)
port += 2

# Node2, no -bind=...=onion, thus no extra port for Tor target.
self.expected.append(
[
[f"-bind=127.0.0.1:{port}"],
[(loopback_ipv4, port)]
],
)
port += 1

self.extra_args = list(map(lambda e: e[0], self.expected))
self.add_nodes(self.num_nodes, self.extra_args)
# Don't start the nodes, as some of them would collide trying to bind on the same port.
self.setup_nodes()

def run_test(self):
for i in range(len(self.expected)):
self.log.info(f"Starting node {i} with {self.expected[i][0]}")
self.start_node(i)
for i, (args, expected_services) in enumerate(self.expected):
self.log.info(f"Checking listening ports of node {i} with {args}")
pid = self.nodes[i].process.pid
binds = set(get_bind_addrs(pid))
# Remove IPv6 addresses because on some CI environments "::1" is not configured
Expand All @@ -82,9 +89,7 @@ def run_test(self):
binds = set(filter(lambda e: len(e[0]) != ipv6_addr_len_bytes, binds))
# Remove RPC ports. They are not relevant for this test.
binds = set(filter(lambda e: e[1] != rpc_port(i), binds))
assert_equal(binds, set(self.expected[i][1]))
self.stop_node(i)
self.log.info(f"Stopped node {i}")
assert_equal(binds, set(expected_services))

if __name__ == '__main__':
BindExtraTest().main()
6 changes: 6 additions & 0 deletions test/functional/mempool_accept.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ def run_test(self):
txid_in_block = self.wallet.sendrawtransaction(from_node=node, tx_hex=raw_tx_in_block)
self.generate(node, 1)
self.mempool_size = 0
# Check negative feerate
assert_raises_rpc_error(-3, "Amount out of range", lambda: self.check_mempool_result(
result_expected=None,
rawtxs=[raw_tx_in_block],
maxfeerate=-0.01,
))
Comment on lines +80 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Missing prerequisite: bitcoin#29434

bitcoin#29459 was added upstream immediately after the maxfeerate coverage introduced by bitcoin#29434. In upstream, bitcoin#29434 added the 1 BTC/kvB rejection test, the 0.99 BTC/kvB passing test, and the src/rpc/mempool.cpp ParseFeeRate path that rejects fee rates >= 1 BTC/kvB. Dash's current backport of bitcoin#29459 only adds the negative maxfeerate assertion and adapts around the missing bitcoin#29434 context: the following already-known check still has no maxfeerate=0.99, and src/rpc/mempool.cpp still parses maxfeerate with CFeeRate(AmountFromValue(...)) instead of ParseFeeRate. This is a soft prerequisite gap because the new negative-feerate test is cleanly adapted and should pass, but the upstream dependency chain around this test is incomplete.


Policy gate (backport-prereq-restore): For full upstream backport PRs, a missing prerequisite is blocking unless the finding is explicitly allowlisted (e.g. intentional_exclusion: true or a matching entry in policy_overrides). The agent's original evidence above is the basis for this block; either backport the prerequisite or annotate the intentional exclusion in the PR description.

source: ['codex-backport-reviewer']

self.check_mempool_result(
result_expected=[{'txid': txid_in_block, 'allowed': False, 'reject-reason': 'txn-already-known'}],
rawtxs=[raw_tx_in_block],
Expand Down
2 changes: 1 addition & 1 deletion test/functional/test-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ can be called after the TestShell is shut down.

| Test parameter key | Default Value | Description |
|---|---|---|
| `bind_to_localhost_only` | `True` | Binds bitcoind RPC services to `127.0.0.1` if set to `True`.|
| `bind_to_localhost_only` | `True` | Binds bitcoind P2P services to `127.0.0.1` if set to `True`.|
| `cachedir` | `"/path/to/bitcoin/test/cache"` | Sets the bitcoind datadir directory. |
| `chain` | `"regtest"` | Sets the chain-type for the underlying test bitcoind processes. |
| `configfile` | `"/path/to/bitcoin/test/config.ini"` | Sets the location of the test framework config file. |
Expand Down
15 changes: 15 additions & 0 deletions test/functional/test_framework/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
wait_until_helper,
p2p_port,
get_chain_folder,
tor_port,
)

BITCOIND_PROC_WAIT_TIMEOUT = 60
Expand Down Expand Up @@ -90,8 +91,11 @@ def __init__(self, i, datadir, extra_args_from_options, *, chain, rpchost, timew
self.cwd = cwd
self.mocktime = mocktime
self.descriptors = descriptors
self.has_explicit_bind = False
if extra_conf is not None:
append_config(datadir, extra_conf)
# Remember if there is bind=... in the config file.
self.has_explicit_bind = any(e.startswith("bind=") for e in extra_conf)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Most callers will just need to add extra args to the standard list below.
# For those callers that need more flexibity, they can just set the args property directly.
# Note that common args are set in the config file (see initialize_datadir)
Expand Down Expand Up @@ -227,6 +231,17 @@ def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, **kwargs
if extra_args is None:
extra_args = self.extra_args

Comment on lines 231 to 233

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid mutating shared node args when auto-inserting binds

start() appends auto-generated -bind arguments directly into extra_args, and when extra_args is omitted this aliases self.extra_args (line 232). In the framework, add_nodes() commonly seeds node args with shared empty lists ([[]] * num_nodes), so the first node that starts can mutate the list used by other nodes; subsequent nodes then inherit the first node’s bind ports and may fail with port-collision startup errors. Copy extra_args before appending so each node gets an isolated argument list.

Useful? React with 👍 / 👎.

# If listening and no -bind is given, then bitcoind would bind P2P ports on
# 0.0.0.0:P and 127.0.0.1:18445 (for incoming Tor connections), where P is
# a unique port chosen by the test framework and configured as port=P in
# bitcoin.conf. To avoid collisions on 127.0.0.1:18445, change it to
# 127.0.0.1:tor_port().
Comment thread
knst marked this conversation as resolved.
Comment on lines +234 to +238

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Carry-over comment references Bitcoin regtest port 18445 and bitcoind

The block comment cherry-picked from bitcoin#22729 still mentions bitcoind, bitcoin.conf, and 127.0.0.1:18445 — the latter is Bitcoin Core's regtest OnionServiceTargetPort. Dash's onion-service target ports differ per network (9996 / 19996 / 19796 / 19896) and the code already uses the dynamic tor_port(self.index) helper, so the literal 18445 is misleading for Dash test authors. Rephrase to reference dashd / dash.conf and a generic onion-service target port instead of the Bitcoin-specific literal.

Suggested change
# If listening and no -bind is given, then bitcoind would bind P2P ports on
# 0.0.0.0:P and 127.0.0.1:18445 (for incoming Tor connections), where P is
# a unique port chosen by the test framework and configured as port=P in
# bitcoin.conf. To avoid collisions on 127.0.0.1:18445, change it to
# 127.0.0.1:tor_port().
# If listening and no -bind is given, then dashd would bind P2P ports on
# 0.0.0.0:P and 127.0.0.1:<default onion-service target port> (for incoming
# Tor connections), where P is a unique port chosen by the test framework
# and configured as port=P in dash.conf. To avoid collisions on the default
# onion target port, change it to 127.0.0.1:tor_port().

source: ['claude', 'codex']

Comment on lines +234 to +238

@thepastaclaw thepastaclaw Jun 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded duplicate

This inline comment was a duplicate created by an over-broad policy-gate promotion. Please ignore this thread and use the corrected nitpick comment on the same block instead.

Comment on lines +234 to +238

@thepastaclaw thepastaclaw Jun 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded duplicate

This inline comment duplicated the stale-comment nitpick and incorrectly labeled it as a blocking backport-prerequisite issue. Please ignore this thread and use the corrected nitpick comment instead.

will_listen = all(e != "-nolisten" and e != "-listen=0" for e in extra_args)
has_explicit_bind = self.has_explicit_bind or any(e.startswith("-bind=") for e in extra_args)
if will_listen and not has_explicit_bind:
extra_args.append(f"-bind=0.0.0.0:{p2p_port(self.index)}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep no-bind test starts from gaining a normal bind

When a test opts out with bind_to_localhost_only = False, some scripts are explicitly exercising the daemon path where no -bind is present: feature_bind_port_discover.py node 0 expects bind_on_any to call Discover(), and feature_bind_port_externalip.py cases rely on GetListenPort() honoring -port. Appending this normal -bind=0.0.0.0:<p2p_port> makes args.GetArgs("-bind") nonempty for those starts, so discovery is suppressed and -port is ignored in favor of the injected bind port. This should be opt-in or avoided for tests that intentionally requested the no-bind path.

Useful? React with 👍 / 👎.

extra_args.append(f"-bind=127.0.0.1:{tor_port(self.index)}=onion")
Comment thread
knst marked this conversation as resolved.
Comment on lines +239 to +243

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Implicit -bind injection ignores --dashd-extra-args (extra_args_from_options)

will_listen and has_explicit_bind inspect only the per-node extra_args, but on line 258 the process is launched with self.args + self.extra_args_from_options + extra_args. That means a global --dashd-extra-args=-listen=0 or --dashd-extra-args=-bind=... passed via the test runner does not influence the implicit-bind decision: the auto-injected -bind= flags get appended anyway, which can fight a user-supplied -listen=0 (dashd will warn/refuse listening with explicit binds) or duplicate a user-supplied bind. Decide based on the same combined argument set that is actually passed to dashd.

Suggested change
will_listen = all(e != "-nolisten" and e != "-listen=0" for e in extra_args)
has_explicit_bind = self.has_explicit_bind or any(e.startswith("-bind=") for e in extra_args)
if will_listen and not has_explicit_bind:
extra_args.append(f"-bind=0.0.0.0:{p2p_port(self.index)}")
extra_args.append(f"-bind=127.0.0.1:{tor_port(self.index)}=onion")
startup_args = [*self.extra_args_from_options, *extra_args]
will_listen = all(e != "-nolisten" and e != "-listen=0" for e in startup_args)
has_explicit_bind = self.has_explicit_bind or any(e.startswith("-bind=") for e in startup_args)
if will_listen and not has_explicit_bind:
extra_args.append(f"-bind=0.0.0.0:{p2p_port(self.index)}")
extra_args.append(f"-bind=127.0.0.1:{tor_port(self.index)}=onion")

source: ['codex']

Comment on lines 231 to +243

@thepastaclaw thepastaclaw Jun 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: start() mutates caller's extra_args list — shared-reference footgun

Carried forward from the prior review at 314e0a3 and still valid at 086d42e. When extra_args is None, start() aliases self.extra_args and appends the auto-injected -bind=... flags onto that referenced list. BitcoinTestFramework.add_nodes can install shared inner lists via [[]] * num_nodes, so the first node/restart can leak stale bind flags into later starts. Copy the selected list before appending, e.g. extra_args = list(self.extra_args) if extra_args is None else list(extra_args).

Correction note: this is a suggestion, not a backport-prerequisite blocker; the earlier policy-gate text on this comment was incorrect.

Comment on lines 231 to +243

@thepastaclaw thepastaclaw Jun 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded duplicate

This inline comment duplicated the carried-forward extra_args mutation finding and incorrectly labeled it as a blocking backport-prerequisite issue. Please ignore this thread and use the corrected suggestion comment instead.


self.use_v2transport = "-v2transport=1" in extra_args or (self.default_to_v2 and "-v2transport=0" not in extra_args)

# Add a new stdout and stderr file each time dashd is started
Expand Down
10 changes: 7 additions & 3 deletions test/functional/test_framework/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,9 @@ def random_bytes(n):

# The maximum number of nodes a single test can spawn
MAX_NODES = 20
# Don't assign rpc or p2p ports lower than this
# Don't assign p2p, rpc or tor ports lower than this
PORT_MIN = int(os.getenv('TEST_RUNNER_PORT_MIN', default=11000))
# The number of ports to "reserve" for p2p and rpc, each
# The number of ports to "reserve" for p2p, rpc and tor, each
PORT_RANGE = 10000


Expand Down Expand Up @@ -362,7 +362,11 @@ def p2p_port(n):


def rpc_port(n):
return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
return p2p_port(n) + PORT_RANGE


def tor_port(n):
return p2p_port(n) + PORT_RANGE * 2


def rpc_url(datadir, i, chain, rpchost=None):
Expand Down
29 changes: 27 additions & 2 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
import argparse
from collections import deque
import configparser
import csv
import datetime
import os
import pathlib
import time
import shutil
import signal
Expand Down Expand Up @@ -451,6 +453,7 @@ def main():
parser.add_argument('--failfast', '-F', action='store_true', help='stop execution after the first test failure')
parser.add_argument('--filter', help='filter scripts to run by regular expression')
parser.add_argument('--skipunit', '-u', action='store_true', help='skip unit tests for the test framework')
parser.add_argument('--resultsfile', '-r', help='store test results (as CSV) to the provided file')


args, unknown_args = parser.parse_known_args()
Expand Down Expand Up @@ -483,6 +486,13 @@ def main():

logging.debug("Temporary test directory at %s" % tmpdir)

results_filepath = None
if args.resultsfile:
results_filepath = pathlib.Path(args.resultsfile)
# Stop early if the parent directory doesn't exist
assert results_filepath.parent.exists(), "Results file parent directory does not exist"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace assertion with explicit error handling for consistency.

Using assert for user input validation is inconsistent with the error handling pattern used elsewhere in this file (e.g., lines 497-498, 546-547), and assertions can be disabled with python -O.

Suggested fix
-        assert results_filepath.parent.exists(), "Results file parent directory does not exist"
+        if not results_filepath.parent.exists():
+            print("ERROR: Results file parent directory does not exist")
+            sys.exit(1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert results_filepath.parent.exists(), "Results file parent directory does not exist"
if not results_filepath.parent.exists():
print("ERROR: Results file parent directory does not exist")
sys.exit(1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/functional/test_runner.py` at line 490, Replace the bare assertion on
results_filepath.parent.exists() with explicit error handling: check if not
results_filepath.parent.exists() and raise a clear exception (e.g., ValueError
or FileNotFoundError) or call the same error-reporting helper used elsewhere in
this module, using the same message "Results file parent directory does not
exist" so the check on results_filepath.parent is enforced even when Python is
run with -O; update the code near the results_filepath usage (the
results_filepath variable check) to follow the existing pattern used around the
other validations in this file.

logging.debug("Test results will be written to " + str(results_filepath))

enable_bitcoind = config["components"].getboolean("ENABLE_BITCOIND")

if not enable_bitcoind:
Expand Down Expand Up @@ -564,9 +574,10 @@ def main():
failfast=args.failfast,
use_term_control=args.ansi,
skipunit=args.skipunit,
results_filepath=results_filepath,
)

def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, attempts=1, enable_coverage=False, args=None, combined_logs_len=0, failfast=False, use_term_control, skipunit=False):
def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, attempts=1, enable_coverage=False, args=None, combined_logs_len=0, failfast=False, use_term_control, skipunit=False, results_filepath=None):
args = args or []

# Warn if dashd is already running
Expand Down Expand Up @@ -662,7 +673,10 @@ def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, attempts=1, enab
logging.debug("Early exiting after test failure")
break

print_results(test_results, max_len_name, (int(time.time() - start_time)))
runtime = int(time.time() - start_time)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: mark 30291 as partial due to missing fix of typo on this line:

 sys.exit(f"Early exiting after test failure due to insufficient free space in {tmpdir}\n"

print_results(test_results, max_len_name, runtime)
if results_filepath:
write_results(test_results, results_filepath, runtime)

if coverage:
coverage_passed = coverage.report_rpc_coverage()
Expand Down Expand Up @@ -709,6 +723,17 @@ def print_results(test_results, max_len_name, runtime):
results += "Runtime: %s s\n" % (runtime)
print(results)


def write_results(test_results, filepath, total_runtime):
with open(filepath, mode="w", encoding="utf8") as results_file:
results_writer = csv.writer(results_file)
results_writer.writerow(['test', 'status', 'duration(seconds)'])
all_passed = True
for test_result in test_results:
all_passed = all_passed and test_result.was_successful
results_writer.writerow([test_result.name, test_result.status, str(test_result.time)])
results_writer.writerow(['ALL', ("Passed" if all_passed else "Failed"), str(total_runtime)])

class TestHandler:
"""
Trigger the test scripts passed in via the list.
Expand Down
Loading