Add X11/Wayland information to version info

Unfortunately there is no way to get this information from Qt, so I had to
resort to some funny low-level C-like Python programming to directly use
libwayland-client and Xlib. Fun was had! Hopefully this avoids having to ask
for this information every time someone shows a bug/crash report, as there
are various subtleties that can be specific to the Wayland compositor in use.
This commit is contained in:
Florian Bruhin 2025-06-24 18:20:58 +02:00
parent 7664fdbb34
commit e7af54898e
5 changed files with 738 additions and 8 deletions

View File

@ -15,6 +15,16 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
[[v3.6.0]]
v3.6.0 (unreleased)
-------------------
Added
~~~~~
- The `:version` info now shows the X11 window manager / Wayland compositor name
(mostly useful for bug/crash reports)
[[v3.5.2]]
v3.5.2 (unreleased)
-------------------

334
qutebrowser/misc/wmname.py Normal file
View File

@ -0,0 +1,334 @@
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""Utilities to get the name of the window manager (X11) / compositor (Wayland)."""
from typing import NewType
from collections.abc import Iterator
import ctypes
import socket
import struct
import pathlib
import dataclasses
import contextlib
import ctypes.util
class Error(Exception):
"""Base class for errors in this module."""
class _WaylandDisplayStruct(ctypes.Structure):
pass
_WaylandDisplay = NewType("_WaylandDisplay", "ctypes._Pointer[_WaylandDisplayStruct]")
def _load_libwayland_client() -> ctypes.CDLL:
"""Load the Wayland client library."""
try:
return ctypes.CDLL("libwayland-client.so")
except OSError as e:
raise Error(f"Failed to load libwayland-client: {e}")
def _pid_from_fd(fd: int) -> int:
"""Get the process ID from a file descriptor using SO_PEERCRED.
https://stackoverflow.com/a/35827184
"""
if not hasattr(socket, "SO_PEERCRED"):
raise Error("Missing socket.SO_PEERCRED")
# struct ucred {
# pid_t pid;
# uid_t uid;
#  gid_t gid;
# }; // where all of those are integers
ucred_format = "3i"
ucred_size = struct.calcsize(ucred_format)
try:
sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
except OSError as e:
raise Error(f"Error creating socket for fd {fd}: {e}")
try:
ucred = sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, ucred_size)
except OSError as e:
raise Error(f"Error getting SO_PEERCRED for fd {fd}: {e}")
finally:
sock.close()
pid, _uid, _gid = struct.unpack(ucred_format, ucred)
return pid
def _process_name_from_pid(pid: int) -> str:
"""Get the process name from a PID by reading /proc/[pid]/cmdline."""
proc_path = pathlib.Path(f"/proc/{pid}/cmdline")
try:
return proc_path.read_text(encoding="utf-8").replace("\0", " ").strip()
except OSError as e:
raise Error(f"Error opening {proc_path}: {e}")
@contextlib.contextmanager
def _wayland_display(wayland_client: ctypes.CDLL) -> Iterator[_WaylandDisplay]:
"""Context manager to connect to a Wayland display."""
wayland_client.wl_display_connect.argtypes = [ctypes.c_char_p] # name
wayland_client.wl_display_connect.restype = ctypes.POINTER(_WaylandDisplayStruct)
wayland_client.wl_display_disconnect.argtypes = [
ctypes.POINTER(_WaylandDisplayStruct)
]
wayland_client.wl_display_disconnect.restype = None
display = wayland_client.wl_display_connect(None)
if not display:
raise Error("Can't connect to display")
try:
yield display
finally:
wayland_client.wl_display_disconnect(display)
def _wayland_get_fd(wayland_client: ctypes.CDLL, display: _WaylandDisplay) -> int:
"""Get the file descriptor for the Wayland display."""
wayland_client.wl_display_get_fd.argtypes = [ctypes.POINTER(_WaylandDisplayStruct)]
wayland_client.wl_display_get_fd.restype = ctypes.c_int
fd = wayland_client.wl_display_get_fd(display)
if fd < 0:
raise Error(f"Failed to get Wayland display file descriptor: {fd}")
return fd
def wayland_compositor_name() -> str:
"""Get the name of the running Wayland compositor.
Approach based on:
https://stackoverflow.com/questions/69302630/wayland-client-get-compositor-name
"""
wayland_client = _load_libwayland_client()
with _wayland_display(wayland_client) as display:
fd = _wayland_get_fd(wayland_client, display)
pid = _pid_from_fd(fd)
process_name = _process_name_from_pid(pid)
return process_name
@dataclasses.dataclass
class _X11Atoms:
NET_SUPPORTING_WM_CHECK: int
NET_WM_NAME: int
UTF8_STRING: int
class _X11DisplayStruct(ctypes.Structure):
pass
_X11Display = NewType("_X11Display", "ctypes._Pointer[_X11DisplayStruct]")
_X11Window = NewType("_X11Window", int)
def _x11_load_lib() -> ctypes.CDLL:
"""Load the X11 library."""
lib = ctypes.util.find_library("X11")
if lib is None:
raise Error("X11 library not found")
try:
return ctypes.CDLL(lib)
except OSError as e:
raise Error(f"Failed to load X11 library: {e}")
@contextlib.contextmanager
def _x11_open_display(xlib: ctypes.CDLL) -> Iterator[_X11Display]:
"""Open a connection to the X11 display."""
xlib.XOpenDisplay.argtypes = [ctypes.c_char_p]
xlib.XOpenDisplay.restype = ctypes.POINTER(_X11DisplayStruct)
xlib.XCloseDisplay.argtypes = [ctypes.POINTER(_X11DisplayStruct)]
xlib.XCloseDisplay.restype = None
display = xlib.XOpenDisplay(None)
if not display:
raise Error("Cannot open display")
try:
yield display
finally:
xlib.XCloseDisplay(display)
def _x11_intern_atom(
xlib: ctypes.CDLL, display: _X11Display, name: bytes, only_if_exists: bool = True
) -> int:
"""Call xlib's XInternAtom function."""
xlib.XInternAtom.argtypes = [
ctypes.POINTER(_X11DisplayStruct), # Display
ctypes.c_char_p, # Atom name
ctypes.c_int, # Only if exists (bool)
]
xlib.XInternAtom.restype = ctypes.c_ulong
atom = xlib.XInternAtom(display, name, only_if_exists)
if atom == 0:
raise Error(f"Failed to intern atom: {name!r}")
return atom
@contextlib.contextmanager
def _x11_get_window_property(
xlib: ctypes.CDLL,
display: _X11Display,
*,
window: _X11Window,
prop: int,
req_type: int,
length: int,
offset: int = 0,
delete: bool = False,
) -> Iterator[tuple["ctypes._Pointer[ctypes.c_ubyte]", ctypes.c_ulong]]:
"""Call xlib's XGetWindowProperty function."""
ret_actual_type = ctypes.c_ulong()
ret_actual_format = ctypes.c_int()
ret_nitems = ctypes.c_ulong()
ret_bytes_after = ctypes.c_ulong()
ret_prop = ctypes.POINTER(ctypes.c_ubyte)()
xlib.XGetWindowProperty.argtypes = [
ctypes.POINTER(_X11DisplayStruct), # Display
ctypes.c_ulong, # Window
ctypes.c_ulong, # Property
ctypes.c_long, # Offset
ctypes.c_long, # Length
ctypes.c_int, # Delete (bool)
ctypes.c_ulong, # Required type (Atom)
ctypes.POINTER(ctypes.c_ulong), # return: Actual type (Atom)
ctypes.POINTER(ctypes.c_int), # return: Actual format
ctypes.POINTER(ctypes.c_ulong), # return: Number of items
ctypes.POINTER(ctypes.c_ulong), # return: Bytes after
ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte)), # return: Property value
]
xlib.XGetWindowProperty.restype = ctypes.c_int
result = xlib.XGetWindowProperty(
display,
window,
prop,
offset,
length,
delete,
req_type,
ctypes.byref(ret_actual_type),
ctypes.byref(ret_actual_format),
ctypes.byref(ret_nitems),
ctypes.byref(ret_bytes_after),
ctypes.byref(ret_prop),
)
if result != 0:
raise Error(f"XGetWindowProperty for {prop} failed: {result}")
if not ret_prop:
raise Error(f"Property {prop} is NULL")
if ret_actual_type.value != req_type:
raise Error(
f"Expected type {req_type}, got {ret_actual_type.value} for property {prop}"
)
if ret_bytes_after.value != 0:
raise Error(
f"Expected no bytes after property {prop}, got {ret_bytes_after.value}"
)
try:
yield ret_prop, ret_nitems
finally:
xlib.XFree(ret_prop)
def _x11_get_wm_window(
xlib: ctypes.CDLL, display: _X11Display, *, atoms: _X11Atoms
) -> _X11Window:
"""Get the _NET_SUPPORTING_WM_CHECK window."""
xlib.XDefaultScreen.argtypes = [ctypes.POINTER(_X11DisplayStruct)]
xlib.XDefaultScreen.restype = ctypes.c_int
xlib.XRootWindow.argtypes = [
ctypes.POINTER(_X11DisplayStruct), # Display
ctypes.c_int, # Screen number
]
xlib.XRootWindow.restype = ctypes.c_ulong
screen = xlib.XDefaultScreen(display)
root_window = xlib.XRootWindow(display, screen)
with _x11_get_window_property(
xlib,
display,
window=root_window,
prop=atoms.NET_SUPPORTING_WM_CHECK,
req_type=33, # XA_WINDOW
length=1,
) as (prop, _nitems):
win = ctypes.cast(prop, ctypes.POINTER(ctypes.c_ulong)).contents.value
return _X11Window(win)
def _x11_get_wm_name(
xlib: ctypes.CDLL,
display: _X11Display,
*,
atoms: _X11Atoms,
wm_window: _X11Window,
) -> str:
"""Get the _NET_WM_NAME property of the window manager."""
with _x11_get_window_property(
xlib,
display,
window=wm_window,
prop=atoms.NET_WM_NAME,
req_type=atoms.UTF8_STRING,
length=1024, # somewhat arbitrary
) as (prop, nitems):
if nitems.value <= 0:
raise Error(f"{nitems.value} items found in _NET_WM_NAME property")
wm_name = ctypes.string_at(prop, nitems.value).decode("utf-8")
if not wm_name:
raise Error("Window manager name is empty")
return wm_name
def x11_wm_name() -> str:
"""Get the name of the running X11 window manager."""
xlib = _x11_load_lib()
with _x11_open_display(xlib) as display:
atoms = _X11Atoms(
NET_SUPPORTING_WM_CHECK=_x11_intern_atom(
xlib, display, b"_NET_SUPPORTING_WM_CHECK"
),
NET_WM_NAME=_x11_intern_atom(xlib, display, b"_NET_WM_NAME"),
UTF8_STRING=_x11_intern_atom(xlib, display, b"UTF8_STRING"),
)
wm_window = _x11_get_wm_window(xlib, display, atoms=atoms)
return _x11_get_wm_name(xlib, display, atoms=atoms, wm_window=wm_window)
if __name__ == "__main__":
try:
wayland_name = wayland_compositor_name()
print(f"Wayland compositor name: {wayland_name}")
except Error as e:
print(f"Wayland error: {e}")
try:
x11_name = x11_wm_name()
print(f"X11 window manager name: {x11_name}")
except Error as e:
print(f"X11 error: {e}")

View File

@ -44,7 +44,7 @@ except ImportError: # pragma: no cover
import qutebrowser
from qutebrowser.utils import (log, utils, standarddir, usertypes, message, resources,
qtutils)
from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin, elf
from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin, elf, wmname
from qutebrowser.browser import pdfjs
from qutebrowser.config import config
if TYPE_CHECKING:
@ -978,7 +978,7 @@ def version_info() -> str:
metaobj = style.metaObject()
assert metaobj is not None
lines.append('Style: {}'.format(metaobj.className()))
lines.append('Platform plugin: {}'.format(objects.qapp.platformName()))
lines.append('Qt Platform: {}'.format(gui_platform_info()))
lines.append('OpenGL: {}'.format(opengl_info()))
importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__))
@ -1147,6 +1147,19 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover
old_context.makeCurrent(old_surface)
def gui_platform_info() -> str:
"""Get the Qt GUI platform name, optionally with the WM/compositor name."""
info = objects.qapp.platformName()
try:
if info == "xcb":
info += f" ({wmname.x11_wm_name()})"
elif info in ["wayland", "wayland-egl"]:
info += f" ({wmname.wayland_compositor_name()})"
except wmname.Error as e:
info += f" (Error: {e})"
return info
def pastebin_version(pbclient: pastebin.PastebinClient = None) -> None:
"""Pastebin the version and log the url to messages."""
def _yank_url(url: str) -> None:

View File

@ -0,0 +1,312 @@
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import sys
import socket
import ctypes
import ctypes.util
import unittest.mock
from collections.abc import Iterator
import pytest
import pytest_mock
import pytestqt.qtbot
from qutebrowser.qt.widgets import QApplication, QWidget
from qutebrowser.misc import wmname
def test_load_libwayland_client():
"""Test loading the Wayland client library, which might or might not exist."""
try:
wmname._load_libwayland_client()
except wmname.Error:
pass
def test_load_libwayland_client_error(mocker: pytest_mock.MockerFixture):
"""Test that an error in loading the Wayland client library raises an error."""
mocker.patch("ctypes.CDLL", side_effect=OSError("Library not found"))
with pytest.raises(wmname.Error, match="Failed to load libwayland-client"):
wmname._load_libwayland_client()
@pytest.fixture
def sock() -> Iterator[socket.socket]:
"""Fixture to create a Unix domain socket."""
parent_sock, child_sock = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
yield parent_sock
parent_sock.close()
child_sock.close()
@pytest.mark.linux
def test_pid_from_fd(sock: socket.socket):
assert wmname._pid_from_fd(sock.fileno()) == os.getpid()
@pytest.mark.skipif(
not hasattr(socket, "SO_PEERCRED"), reason="socket.SO_PEERCRED not available"
)
def test_pid_from_fd_invalid():
"""Test that an invalid file descriptor raises an error."""
with pytest.raises(
wmname.Error,
match=r"Error creating socket for fd -1: \[Errno 9\] Bad file descriptor",
):
wmname._pid_from_fd(-1)
@pytest.mark.linux
def test_pid_from_fd_getsockopt_error(
sock: socket.socket, mocker: pytest_mock.MockerFixture
):
"""Test that an error in getsockopt raises an error."""
mocker.patch.object(
socket.socket, "getsockopt", side_effect=OSError("Mocked error")
)
with pytest.raises(wmname.Error, match="Error getting SO_PEERCRED for fd"):
wmname._pid_from_fd(sock.fileno())
def test_pid_from_fd_no_so_peercred(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delattr(socket, "SO_PEERCRED", raising=False)
with pytest.raises(wmname.Error, match=r"Missing socket\.SO_PEERCRED"):
wmname._pid_from_fd(-1)
@pytest.mark.linux
def test_process_name_from_pid():
"""Test getting the process name from a PID."""
pid = os.getpid()
name = wmname._process_name_from_pid(pid)
assert os.path.basename(name.split()[0]) == os.path.basename(sys.executable)
def test_process_name_from_pid_invalid():
"""Test that an invalid PID raises an error."""
with pytest.raises(wmname.Error, match=r"Error opening .proc.-1.cmdline"):
wmname._process_name_from_pid(-1)
@pytest.fixture
def libwayland_client_mock(mocker: pytest_mock.MockerFixture) -> None:
"""Mock the libwayland-client library."""
return mocker.Mock()
@pytest.fixture
def fake_wayland_display() -> wmname._WaylandDisplay:
return wmname._WaylandDisplay(ctypes.pointer(wmname._WaylandDisplayStruct()))
def test_wayland_display(
libwayland_client_mock: unittest.mock.Mock,
fake_wayland_display: wmname._WaylandDisplay,
):
"""Test getting the Wayland display."""
libwayland_client_mock.wl_display_connect.return_value = fake_wayland_display
with wmname._wayland_display(libwayland_client_mock):
pass
libwayland_client_mock.wl_display_connect.assert_called_once_with(None)
libwayland_client_mock.wl_display_disconnect.assert_called_once_with(
fake_wayland_display
)
def test_wayland_display_error(libwayland_client_mock: unittest.mock.Mock):
"""Test that an error in getting the Wayland display raises an error."""
libwayland_client_mock.wl_display_connect.return_value = ctypes.c_void_p(0)
with pytest.raises(wmname.Error, match="Can't connect to display"):
with wmname._wayland_display(libwayland_client_mock):
pass
libwayland_client_mock.wl_display_disconnect.assert_not_called() # Not called on error
def test_wayland_get_fd(
libwayland_client_mock: unittest.mock.Mock,
fake_wayland_display: wmname._WaylandDisplay,
):
"""Test getting the file descriptor from a Wayland display."""
libwayland_client_mock.wl_display_get_fd.return_value = 42
fd = wmname._wayland_get_fd(libwayland_client_mock, fake_wayland_display)
assert fd == 42
libwayland_client_mock.wl_display_get_fd.assert_called_once_with(
fake_wayland_display
)
def test_wayland_get_fd_error(
libwayland_client_mock: unittest.mock.Mock,
fake_wayland_display: wmname._WaylandDisplay,
):
"""Test that an error in getting the file descriptor raises an error."""
libwayland_client_mock.wl_display_get_fd.return_value = -1
with pytest.raises(
wmname.Error, match="Failed to get Wayland display file descriptor: -1"
):
wmname._wayland_get_fd(libwayland_client_mock, fake_wayland_display)
libwayland_client_mock.wl_display_get_fd.assert_called_once_with(
fake_wayland_display
)
def test_wayland_real():
"""Test getting the Wayland window manager name."""
try:
name = wmname.wayland_compositor_name()
except wmname.Error:
return
assert isinstance(name, str)
assert name
def test_load_xlib():
"""Test loading Xlib, which might or might not exist."""
try:
wmname._x11_load_lib()
except wmname.Error:
pass
def test_load_xlib_not_found(monkeypatch: pytest.MonkeyPatch):
"""Test loading Xlib simulating a missing library."""
monkeypatch.setattr(ctypes.util, "find_library", lambda x: None)
with pytest.raises(wmname.Error, match="X11 library not found"):
wmname._x11_load_lib()
def test_load_xlib_error(mocker: pytest_mock.MockerFixture):
"""Test that an error in loading Xlib raises an error."""
mocker.patch.object(ctypes.util, "find_library", return_value="libX11.so.6")
mocker.patch.object(ctypes, "CDLL", side_effect=OSError("Failed to load library"))
with pytest.raises(
wmname.Error, match="Failed to load X11 library: Failed to load library"
):
wmname._x11_load_lib()
@pytest.fixture
def xlib_mock(mocker: pytest_mock.MockerFixture) -> None:
"""Mock the XLib library."""
return mocker.Mock()
@pytest.fixture
def fake_x11_display() -> wmname._X11Display:
return wmname._X11Display(ctypes.pointer(wmname._X11DisplayStruct()))
def test_x11_display(
xlib_mock: unittest.mock.Mock,
fake_x11_display: wmname._X11Display,
):
"""Test getting the X11 display."""
xlib_mock.XOpenDisplay.return_value = fake_x11_display
with wmname._x11_open_display(xlib_mock):
pass
xlib_mock.XOpenDisplay.assert_called_once_with(None)
xlib_mock.XCloseDisplay.assert_called_once_with(fake_x11_display)
def test_x11_display_error(xlib_mock: unittest.mock.Mock):
"""Test that an error in getting the X11 display raises an error."""
xlib_mock.XOpenDisplay.return_value = ctypes.c_void_p(0)
with pytest.raises(wmname.Error, match="Cannot open display"):
with wmname._x11_open_display(xlib_mock):
pass
xlib_mock.XCloseDisplay.assert_not_called() # Not called on error
def test_x11_intern_atom(
xlib_mock: unittest.mock.Mock,
fake_x11_display: wmname._X11Display,
):
"""Test getting an interned atom from X11."""
atom_name = b"_NET_WM_NAME"
atom = 12345
xlib_mock.XInternAtom.return_value = atom
result = wmname._x11_intern_atom(xlib_mock, fake_x11_display, atom_name)
assert result == atom
xlib_mock.XInternAtom.assert_called_once_with(
fake_x11_display,
atom_name,
True, # don't create if not found
)
def test_x11_intern_atom_error(
xlib_mock: unittest.mock.Mock,
fake_x11_display: wmname._X11Display,
):
"""Test that an error in getting an interned atom raises an error."""
xlib_mock.XInternAtom.return_value = 0
with pytest.raises(wmname.Error, match="Failed to intern atom: b'_NET_WM_NAME'"):
wmname._x11_intern_atom(xlib_mock, fake_x11_display, b"_NET_WM_NAME")
xlib_mock.XInternAtom.assert_called_once_with(
fake_x11_display,
b"_NET_WM_NAME",
True, # don't create if not found
)
def test_x11_get_wm_name(
qapp: QApplication,
qtbot: pytestqt.qtbot.QtBot,
) -> None:
"""Test getting a property from X11.
This is difficult to mock (as it involves a C layer via ctypes with return
arguments), so we instead try getting data from a real window.
"""
if qapp.platformName() != "xcb":
pytest.skip("This test only works on X11 (xcb) platforms")
w = QWidget()
qtbot.add_widget(w)
w.setWindowTitle("Test Window")
xlib = wmname._x11_load_lib()
with wmname._x11_open_display(xlib) as display:
atoms = wmname._X11Atoms(
NET_SUPPORTING_WM_CHECK=-1,
NET_WM_NAME=wmname._x11_intern_atom(xlib, display, b"_NET_WM_NAME"),
UTF8_STRING=wmname._x11_intern_atom(xlib, display, b"UTF8_STRING"),
)
window = wmname._X11Window(int(w.winId()))
name = wmname._x11_get_wm_name(xlib, display, atoms=atoms, wm_window=window)
assert name == "Test Window"
def test_x11_real():
try:
name = wmname.x11_wm_name()
except wmname.Error:
return
assert isinstance(name, str)
assert name

View File

@ -16,6 +16,7 @@ import datetime
import dataclasses
import importlib.metadata
import unittest.mock
from typing import Any
import pytest
import pytest_mock
@ -27,7 +28,7 @@ from qutebrowser.qt.core import PYQT_VERSION_STR
import qutebrowser
from qutebrowser.config import config, websettings
from qutebrowser.utils import version, usertypes, utils, standarddir
from qutebrowser.misc import pastebin, objects, elf
from qutebrowser.misc import pastebin, objects, elf, wmname
from qutebrowser.browser import pdfjs
try:
@ -1283,6 +1284,7 @@ class TestChromiumVersion:
class VersionParams:
name: str
gui_platform: str = 'GUI_PLATFORM'
git_commit: bool = True
frozen: bool = False
qapp: bool = True
@ -1303,6 +1305,8 @@ class VersionParams:
VersionParams('no-ssl', ssl_support=False),
VersionParams('no-autoconfig-loaded', autoconfig_loaded=False),
VersionParams('no-config-py-loaded', config_py_loaded=False),
VersionParams('xcb-platform', gui_platform='xcb'),
VersionParams('wayland-platform', gui_platform='wayland'),
], ids=lambda param: param.name)
def test_version_info(params, stubs, monkeypatch, config_stub):
"""Test version.version_info()."""
@ -1323,16 +1327,21 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
'QSslSocket': FakeQSslSocket('SSL VERSION', params.ssl_support),
'platform.platform': lambda: 'PLATFORM',
'platform.architecture': lambda: ('ARCHITECTURE', ''),
'wmname.x11_wm_name': lambda: 'X11 WM NAME',
'wmname.wayland_compositor_name': lambda: 'WAYLAND COMPOSITOR NAME',
'_os_info': lambda: ['OS INFO 1', 'OS INFO 2'],
'_path_info': lambda: {'PATH DESC': 'PATH NAME'},
'objects.qapp': (stubs.FakeQApplication(style='STYLE', platform_name='PLATFORM')
if params.qapp else None),
'objects.qapp': (
stubs.FakeQApplication(style='STYLE', platform_name=params.gui_platform)
if params.qapp
else None
),
'qtutils.library_path': (lambda _loc: 'QT PATH'),
'sql.version': lambda: 'SQLITE VERSION',
'_uptime': lambda: datetime.timedelta(hours=1, minutes=23, seconds=45),
'config.instance.yaml_loaded': params.autoconfig_loaded,
'machinery.INFO': machinery.SelectionInfo(
wrapper="QT WRAPPER",
wrapper='QT WRAPPER',
reason=machinery.SelectionReason.fake
),
}
@ -1340,11 +1349,23 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
version.opengl_info.cache_clear()
monkeypatch.setenv('QUTE_FAKE_OPENGL', 'VENDOR, 1.0 VERSION')
if not params.qapp:
expected_gui_platform = None
elif params.gui_platform == 'GUI_PLATFORM':
expected_gui_platform = 'GUI_PLATFORM'
elif params.gui_platform == 'xcb':
expected_gui_platform = 'xcb (X11 WM NAME)'
elif params.gui_platform == 'wayland':
expected_gui_platform = 'wayland (WAYLAND COMPOSITOR NAME)'
else:
raise utils.Unreachable(params.gui_platform)
substitutions = {
'git_commit': '\nGit commit: GIT COMMIT' if params.git_commit else '',
'style': '\nStyle: STYLE' if params.qapp else '',
'platform_plugin': ('\nPlatform plugin: PLATFORM' if params.qapp
else ''),
'platform_plugin': (
f'\nQt Platform: {expected_gui_platform}' if params.qapp else ''
),
'opengl': '\nOpenGL: VENDOR, 1.0 VERSION' if params.qapp else '',
'qt': 'QT VERSION',
'frozen': str(params.frozen),
@ -1552,6 +1573,46 @@ def test_pastebin_version_error(pbclient, caplog, message_mock, monkeypatch):
assert msg.text == "Failed to pastebin version info: test"
@pytest.mark.parametrize("platform, expected", [
("windows", "windows"),
("xcb", "xcb (X11 WM NAME)"),
("wayland", "wayland (WAYLAND COMPOSITOR NAME)"),
("wayland-egl", "wayland-egl (WAYLAND COMPOSITOR NAME)"),
])
def test_gui_platform_info(
platform: str, expected: str, monkeypatch: pytest.MonkeyPatch, stubs: Any
) -> None:
monkeypatch.setattr(
version.objects,
"qapp",
stubs.FakeQApplication(platform_name=platform, style="STYLE"),
)
monkeypatch.setattr(version.wmname, "x11_wm_name", lambda: "X11 WM NAME")
monkeypatch.setattr(
version.wmname, "wayland_compositor_name", lambda: "WAYLAND COMPOSITOR NAME"
)
assert version.gui_platform_info() == expected
@pytest.mark.parametrize("platform", ["xcb", "wayland", "wayland-egl"])
def test_gui_platform_info_error(
platform: str,
monkeypatch: pytest.MonkeyPatch,
mocker: pytest_mock.MockerFixture,
stubs: Any,
) -> None:
monkeypatch.setattr(
version.objects,
"qapp",
stubs.FakeQApplication(platform_name=platform, style="STYLE"),
)
mocker.patch.object(wmname, "x11_wm_name", side_effect=wmname.Error("fake error"))
mocker.patch.object(
wmname, "wayland_compositor_name", side_effect=wmname.Error("fake error")
)
assert version.gui_platform_info() == f"{platform} (Error: fake error)"
def test_uptime(monkeypatch, qapp):
"""Test _uptime runs and check if microseconds are dropped."""
monkeypatch.setattr(objects, 'qapp', qapp)