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:
parent
7664fdbb34
commit
e7af54898e
|
|
@ -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)
|
||||
-------------------
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue