test: add interface_gui.py to test bitcoin-gui startup via RPC
What changed, and why it matters
This commit adds a new automated test that starts the Bitcoin GUI in a headless mode and checks it can be shut down by a remote command. It also makes a small code change so the GUI does not try to load custom fonts when running in the minimal headless Qt platform. There is no security vulnerability here; it is purely a testing and robustness improvement.
No security action required. Treat as a normal test-infrastructure and GUI robustness patch.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces test/functional/interface_gui.py, extends the functional test framework to launch bitcoin-gui via the ‘bitcoin gui’ wrapper with QT_QPA_PLATFORM=minimal, and adds BUILD_GUI detection to test/config.ini.in. The only production code change is in src/qt/guiutil.cpp::LoadFont, which returns early when QApplication::platformName() == ‘minimal’ because the qminimal platform plugin lacks font loading support. This prevents an assertion failure during headless GUI testing.
Changed components
src/qt/guiutil.cpptest/functional/interface_gui.pytest/functional/test_framework/test_node.pytest/functional/test_framework/test_framework.pytest/functional/test_framework/util.pytest/functional/test_runner.pytest/CMakeLists.txttest/config.ini.inInspect captured patch +79 / −8
diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp
index e0966b9c..6e5d1759 100644
--- a/src/qt/guiutil.cpp
+++ b/src/qt/guiutil.cpp
@@ -290,6 +290,8 @@ bool hasEntryData(const QAbstractItemView *view, int column, int role)
void LoadFont(const QString& file_name)
{
+ // The qminimal plugin does not provide font loading support.
+ if (QApplication::platformName() == "minimal") return;
const int id = QFontDatabase::addApplicationFont(file_name);
assert(id != -1);
}
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index eac3ec59..91a6afb1 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -29,6 +29,7 @@ function(create_test_config)
set_configure_variable(ENABLE_EXTERNAL_SIGNER ENABLE_EXTERNAL_SIGNER)
set_configure_variable(WITH_USDT ENABLE_USDT_TRACEPOINTS)
set_configure_variable(ENABLE_IPC ENABLE_IPC)
+ set_configure_variable(BUILD_GUI BUILD_GUI)
configure_file(config.ini.in config.ini USE_SOURCE_PERMISSIONS @ONLY)
endfunction()
diff --git a/test/config.ini.in b/test/config.ini.in
index 20fa36b9..45b69ded 100644
--- a/test/config.ini.in
+++ b/test/config.ini.in
@@ -29,3 +29,4 @@ RPCAUTH=@abs_top_srcdir@/share/rpcauth/rpcauth.py
@ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true
@ENABLE_USDT_TRACEPOINTS_TRUE@ENABLE_USDT_TRACEPOINTS=true
@ENABLE_IPC_TRUE@ENABLE_IPC=true
+@BUILD_GUI_TRUE@BUILD_GUI=true
diff --git a/test/functional/interface_gui.py b/test/functional/interface_gui.py
new file mode 100755
index 00000000..7f05d720
--- /dev/null
+++ b/test/functional/interface_gui.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+# Copyright (c) The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test that bitcoin-gui starts up and can be stopped via RPC."""
+
+import platform
+
+from test_framework.test_framework import (
+ BitcoinTestFramework,
+ SkipTest,
+)
+
+
+class GuiTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+ self.extra_args = [["-server"]]
+
+ def skip_test_if_missing_module(self):
+ self.skip_if_no_gui()
+ # On Windows, bitcoin.exe exits immediately when launching bitcoin-gui.exe,
+ # causing the test framework's process monitor to see a premature node exit.
+ # On macOS, bitcoin-qt's Cocoa code assumes NSApp is initialized, but the
+ # minimal Qt platform plugin skips that, causing crashes.
+ # Both issues are likely fixable.
+ if platform.system() in ("Windows", "Darwin"):
+ raise SkipTest("bitcoin-gui test not supported on Windows or macOS")
+
+ def setup_nodes(self):
+ self.extra_init = [{"use_gui": True}]
+ super().setup_nodes()
+
+ def run_test(self):
+ self.log.info("Test that bitcoin-gui starts up and can be stopped via RPC")
+ self.stop_node(0)
+
+
+if __name__ == "__main__":
+ GuiTest(__file__).main()
diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py
index 64dcbfd7..8559a2ae 100755
--- a/test/functional/test_framework/test_framework.py
+++ b/test/functional/test_framework/test_framework.py
@@ -1078,6 +1078,11 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
if not self.is_ipc_compiled():
raise SkipTest("ipc has not been compiled.")
+ def skip_if_no_gui(self):
+ """Skip the running test if the GUI has not been compiled."""
+ if not self.is_gui_compiled():
+ raise SkipTest("GUI has not been compiled.")
+
def skip_if_no_previous_releases(self):
"""Skip the running test if previous releases are not available."""
if not self.has_previous_releases():
@@ -1160,6 +1165,10 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
"""Checks whether ipc was compiled."""
return self.config["components"].getboolean("ENABLE_IPC")
+ def is_gui_compiled(self):
+ """Checks whether the GUI was compiled."""
+ return self.config["components"].getboolean("BUILD_GUI", fallback=False)
+
def has_blockfile(self, node, filenum: str):
return (node.blocks_path/ f"blk{filenum}.dat").is_file()
diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py
index a7c140a5..de96cc60 100755
--- a/test/functional/test_framework/test_node.py
+++ b/test/functional/test_framework/test_node.py
@@ -112,6 +112,7 @@ class TestNode():
v2transport=False,
uses_wallet=False,
ipcbind=False,
+ use_gui=False,
):
self.index = i
self.datadir_path = datadir_path
@@ -125,6 +126,7 @@ class TestNode():
self.binaries = binaries
self.coverage_dir = coverage_dir
self.cwd = cwd
+ self.use_gui = use_gui
self.has_explicit_bind = False
if extra_conf is not None:
append_config(self.datadir_path, extra_conf)
@@ -138,7 +140,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.binaries.node_argv(need_ipc=ipcbind) + [
+ self.args = self.binaries.node_argv(need_ipc=ipcbind, use_gui=use_gui) + [
f"-datadir={self.datadir_path}",
"-logtimemicros",
"-debug",
@@ -278,6 +280,20 @@ class TestNode():
# add environment variable LIBC_FATAL_STDERR_=1 so that libc errors are written to stderr and not the terminal
subp_env = dict(os.environ, LIBC_FATAL_STDERR_="1")
+ if self.use_gui:
+ subp_env.setdefault("QT_QPA_PLATFORM", "minimal")
+ subp_env.setdefault("LC_ALL", "nl_NL.UTF-8") # Set language to try to trigger translation bugs
+ if sys.platform.startswith("linux") and "XDG_RUNTIME_DIR" not in subp_env:
+ # Qt prints warnings to stderr when XDG_RUNTIME_DIR is unset or has wrong
+ # permissions (e.g. in CI environments without a desktop session), which
+ # would cause tests to fail due to unexpected stderr output. The two
+ # warnings are:
+ # "QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-root'"
+ # "QStandardPaths: wrong permissions on runtime directory /path, 0755 instead of 0700"
+ # Use a dedicated subdirectory with the required 0700 permissions.
+ xdg_runtime_dir = self.datadir_path / "xdg_runtime"
+ xdg_runtime_dir.mkdir(mode=0o700, exist_ok=True)
+ subp_env["XDG_RUNTIME_DIR"] = str(xdg_runtime_dir)
if env is not None:
subp_env.update(env)
diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py
index 7d064d68..24b31928 100644
--- a/test/functional/test_framework/util.py
+++ b/test/functional/test_framework/util.py
@@ -305,11 +305,11 @@ class Binaries:
"Return argv array that should be used to invoke bitcoin-chainstate"
return self._argv("chainstate", self.paths.bitcoinchainstate)
- def _argv(self, command, bin_path, need_ipc=False):
+ def _argv(self, command, bin_path, *, need_ipc=False, use_gui=False):
"""Return argv array that should be used to invoke the command.
- It either uses the bitcoin wrapper executable (if BITCOIN_CMD is set or
- need_ipc is True), or the direct binary path (bitcoind, etc). When
+ It either uses the bitcoin wrapper executable (if BITCOIN_CMD, need_ipc,
+ or use_gui are set), or the direct binary path (bitcoind, etc). When
bin_dir is set (by tests calling binaries from previous releases) it
always uses the direct path.
@@ -319,11 +319,12 @@ class Binaries:
"""
if self.bin_dir is not None:
return [os.path.join(self.bin_dir, os.path.basename(bin_path))]
- elif self.paths.bitcoin_cmd is not None or need_ipc:
- # If the current test needs IPC functionality, use the bitcoin
- # wrapper binary and append -m so it calls multiprocess binaries.
+ elif self.paths.bitcoin_cmd is not None or need_ipc or use_gui:
+ # If the current test needs IPC or GUI functionality, use the
+ # bitcoin wrapper binary and add appropriate options.
bitcoin_cmd = self.paths.bitcoin_cmd or [self.paths.bitcoin_bin]
- return self.valgrind_cmd + bitcoin_cmd + (["-m"] if need_ipc else []) + [command]
+ subcommand = "gui" if use_gui and command == "node" else command
+ return self.valgrind_cmd + bitcoin_cmd + (["-m"] if need_ipc else []) + [subcommand]
else:
return self.valgrind_cmd + [bin_path]
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 5bfc7d86..1f7416d6 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -361,6 +361,7 @@ BASE_SCRIPTS = [
'feature_logging.py',
'interface_ipc.py',
'interface_ipc_mining.py',
+ 'interface_gui.py',
'feature_anchors.py',
'mempool_datacarrier.py',
'feature_coinstatsindex.py',
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.