test: Remove `system_tests/run_command` runtime dependencies
What changed, and why it matters
This commit only changes Bitcoin Core's internal test code. It replaces tests that previously relied on external system commands like 'echo', 'false', and 'cat' with a self-contained mock program. There is no change to the actual Bitcoin node software that users run, and no security issue is introduced or fixed.
No security action required. This is a test-only refactoring. Normal code review and CI verification are sufficient.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors src/test/system_tests.cpp to remove runtime dependencies on shell utilities by introducing src/test/mock_process.cpp, a disabled Boost.Test suite whose individual test cases are invoked as child processes. The CMake build is updated to compile mock_process.cpp into test_bitcoin, and lint-includes.py is updated to expect boost/cstdlib.hpp. The change is purely test infrastructure and does not modify production code paths.
Changed components
src/test/system_tests.cppsrc/test/mock_process.cppsrc/test/CMakeLists.txttest/lint/lint-includes.pyInspect captured patch +59 / −24
diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt
index d5f2776a..0bfcc244 100644
--- a/src/test/CMakeLists.txt
+++ b/src/test/CMakeLists.txt
@@ -61,6 +61,7 @@ add_executable(test_bitcoin
miniminer_tests.cpp
miniscript_tests.cpp
minisketch_tests.cpp
+ mock_process.cpp
multisig_tests.cpp
bip328_tests.cpp
net_peer_connection_tests.cpp
@@ -188,6 +189,8 @@ function(add_boost_test source_file)
list(TRANSFORM test_suite_macro
REPLACE "(BOOST_FIXTURE_TEST_SUITE|BOOST_AUTO_TEST_SUITE)\\(" ""
)
+ # The mock_process test suite does not contain unit tests.
+ list(REMOVE_ITEM test_suite_macro "mock_process")
foreach(test_suite_name IN LISTS test_suite_macro)
add_test(NAME ${test_suite_name}
COMMAND test_bitcoin --run_test=${test_suite_name} --catch_system_error=no --log_level=test_suite -- DEBUG_LOG_OUT
diff --git a/src/test/mock_process.cpp b/src/test/mock_process.cpp
new file mode 100644
index 00000000..c9100c9f
--- /dev/null
+++ b/src/test/mock_process.cpp
@@ -0,0 +1,40 @@
+// Copyright (c) 2025-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or https://opensource.org/license/mit/.
+
+#include <boost/test/unit_test.hpp>
+
+#include <iostream>
+#include <string>
+
+BOOST_AUTO_TEST_SUITE(mock_process, *boost::unit_test::disabled())
+
+BOOST_AUTO_TEST_CASE(valid_json, *boost::unit_test::disabled())
+{
+ std::cout << R"({"success": true})" << std::endl;
+}
+
+BOOST_AUTO_TEST_CASE(nonzeroexit_nooutput, *boost::unit_test::disabled())
+{
+ BOOST_FAIL("Test unconditionally fails.");
+}
+
+BOOST_AUTO_TEST_CASE(nonzeroexit_stderroutput, *boost::unit_test::disabled())
+{
+ std::cerr << "err\n";
+ BOOST_FAIL("Test unconditionally fails.");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_json, *boost::unit_test::disabled())
+{
+ std::cout << "{\n";
+}
+
+BOOST_AUTO_TEST_CASE(pass_stdin_to_stdout, *boost::unit_test::disabled())
+{
+ std::string s;
+ std::getline(std::cin, s);
+ std::cout << s << std::endl;
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/system_tests.cpp b/src/test/system_tests.cpp
index f4490dec..c5eac1cb 100644
--- a/src/test/system_tests.cpp
+++ b/src/test/system_tests.cpp
@@ -14,12 +14,20 @@
#include <util/subprocess.h>
#endif // ENABLE_EXTERNAL_SIGNER
+#include <boost/cstdlib.hpp>
#include <boost/test/unit_test.hpp>
+#include <string>
+
BOOST_FIXTURE_TEST_SUITE(system_tests, BasicTestingSetup)
#ifdef ENABLE_EXTERNAL_SIGNER
+static std::vector<std::string> mock_executable(std::string name)
+{
+ return {boost::unit_test::framework::master_test_suite().argv[0], "--log_level=nothing", "--report_level=no", "--run_test=mock_process/" + name};
+}
+
BOOST_AUTO_TEST_CASE(run_command)
{
{
@@ -27,11 +35,7 @@ BOOST_AUTO_TEST_CASE(run_command)
BOOST_CHECK(result.isNull());
}
{
-#ifdef WIN32
- const UniValue result = RunCommandParseJSON({"cmd.exe", "/c", "echo", "{\"success\":", "true}"}); // The command is intentionally split "incorrectly", to exactly preserve previous behavior. This is due to the cmd.exe internal echo quoting strings with spaces in it, unlike the normal 'echo' below.
-#else
- const UniValue result = RunCommandParseJSON({"echo", "{\"success\": true}"});
-#endif
+ const UniValue result = RunCommandParseJSON(mock_executable("valid_json"));
BOOST_CHECK(result.isObject());
const UniValue& success = result.find_value("success");
BOOST_CHECK(!success.isNull());
@@ -48,45 +52,32 @@ BOOST_AUTO_TEST_CASE(run_command)
}
{
// Return non-zero exit code, no output to stderr
-#ifdef WIN32
- const std::vector<std::string> command = {"cmd.exe", "/c", "exit 1"};
-#else
- const std::vector<std::string> command = {"false"};
-#endif
+ const std::vector<std::string> command = mock_executable("nonzeroexit_nooutput");
BOOST_CHECK_EXCEPTION(RunCommandParseJSON(command), std::runtime_error, [&](const std::runtime_error& e) {
const std::string what{e.what()};
- BOOST_CHECK(what.find(strprintf("RunCommandParseJSON error: process(%s) returned 1: \n", util::Join(command, " "))) != std::string::npos);
+ BOOST_CHECK(what.find(strprintf("RunCommandParseJSON error: process(%s) returned %d: \n", util::Join(command, " "), boost::exit_test_failure)) != std::string::npos);
return true;
});
}
{
// Return non-zero exit code, with error message for stderr
-#ifdef WIN32
- const std::vector<std::string> command = {"cmd.exe", "/c", "echo err 1>&2 && exit 1"};
-#else
- const std::vector<std::string> command = {"sh", "-c", "echo err 1>&2 && false"};
-#endif
+ const std::vector<std::string> command = mock_executable("nonzeroexit_stderroutput");
const std::string expected{"err"};
BOOST_CHECK_EXCEPTION(RunCommandParseJSON(command), std::runtime_error, [&](const std::runtime_error& e) {
const std::string what(e.what());
- BOOST_CHECK(what.find(strprintf("RunCommandParseJSON error: process(%s) returned %s: %s", util::Join(command, " "), 1, "err")) != std::string::npos);
+ BOOST_CHECK(what.find(strprintf("RunCommandParseJSON error: process(%s) returned %s: %s", util::Join(command, " "), boost::exit_test_failure, "err")) != std::string::npos);
BOOST_CHECK(what.find(expected) != std::string::npos);
return true;
});
}
{
// Unable to parse JSON
-#ifdef WIN32
- const std::vector<std::string> command = {"cmd.exe", "/c", "echo {"};
-#else
- const std::vector<std::string> command = {"echo", "{"};
-#endif
- BOOST_CHECK_EXCEPTION(RunCommandParseJSON(command), std::runtime_error, HasReason("Unable to parse JSON: {"));
+ BOOST_CHECK_EXCEPTION(RunCommandParseJSON(mock_executable("invalid_json")), std::runtime_error, HasReason("Unable to parse JSON: {"));
}
#ifndef WIN32
{
// Test stdin
- const UniValue result = RunCommandParseJSON({"cat"}, "{\"success\": true}");
+ const UniValue result = RunCommandParseJSON(mock_executable("pass_stdin_to_stdout"), "{\"success\": true}");
BOOST_CHECK(result.isObject());
const UniValue& success = result.find_value("success");
BOOST_CHECK(!success.isNull());
diff --git a/test/lint/lint-includes.py b/test/lint/lint-includes.py
index cccaec73..70c4e0e2 100755
--- a/test/lint/lint-includes.py
+++ b/test/lint/lint-includes.py
@@ -21,6 +21,7 @@ EXCLUDED_DIRS = ["contrib/devtools/bitcoin-tidy/",
] + SHARED_EXCLUDED_SUBTREES
EXPECTED_BOOST_INCLUDES = [
+ "boost/cstdlib.hpp",
"boost/multi_index/detail/hash_index_iterator.hpp",
"boost/multi_index/hashed_index.hpp",
"boost/multi_index/identity.hpp",
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.