diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index e0966b9c38e..6e5d1759acf 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 eac3ec5942f..91a6afb12c9 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 20fa36b94ef..45b69ded41b 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 00000000000..7f05d720291 --- /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 64dcbfd7ec5..8559a2ae611 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 a7c140a53bb..de96cc60446 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 7d064d68a9f..24b31928a24 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 5bfc7d86239..1f7416d611a 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',