mirror of
https://gitlab.freedesktop.org/xorg/xserver.git
synced 2026-06-13 15:18:23 +02:00
pyxtest: add --display for running a test against a manually started server
This makes it much easier to debug an individual test since we can now start an X server via valgrind/gdb/whatever and have the test client connect to that server. Assisted-by: Claude:claude-claude-opus-4-6 Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2187>
This commit is contained in:
parent
b9ed4bd4c0
commit
4d79ddd0b4
3 changed files with 142 additions and 3 deletions
|
|
@ -42,6 +42,14 @@ pytest test/pyxtest/ -v
|
||||||
|
|
||||||
The normal pytest options work as expected (`-k` for test selection, etc.)
|
The normal pytest options work as expected (`-k` for test selection, etc.)
|
||||||
|
|
||||||
|
Tests can be run against a manually-started server using the `--display`
|
||||||
|
option:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./build/hw/vfb/Xvfb :2
|
||||||
|
pytest test/pyxtest --display :2
|
||||||
|
```
|
||||||
|
|
||||||
### Running with AddressSanitizer (ASAN)
|
### Running with AddressSanitizer (ASAN)
|
||||||
|
|
||||||
ASAN is a compile-time instrumentation that detects memory errors such as
|
ASAN is a compile-time instrumentation that detects memory errors such as
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import shutil
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from xserver import XServerProcess
|
from xserver import ExternalXServer, XServerProcess
|
||||||
from xclient import RawX11Connection, X11ConnectionError, XlibConnection
|
from xclient import RawX11Connection, X11ConnectionError, XlibConnection
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -33,6 +33,14 @@ def pytest_addoption(parser):
|
||||||
parser.addoption(
|
parser.addoption(
|
||||||
"--server-path", default=None, help="Explicit path to the X server binary"
|
"--server-path", default=None, help="Explicit path to the X server binary"
|
||||||
)
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--display",
|
||||||
|
default=None,
|
||||||
|
help="Connect to an existing X server instead of starting one. "
|
||||||
|
"Value is a display number or :N string (e.g. '42' or ':42'). "
|
||||||
|
"Optionally combine with --server-type to declare the server type "
|
||||||
|
"for marker-based test filtering.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
|
|
@ -53,10 +61,36 @@ def pytest_configure(config):
|
||||||
"markers", "swapped_client: mark test as requiring a byte-swapped client"
|
"markers", "swapped_client: mark test as requiring a byte-swapped client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate --display against conflicting options
|
||||||
|
display = config.getoption("--display", default=None)
|
||||||
|
if display is not None:
|
||||||
|
if config.getoption("--valgrind"):
|
||||||
|
raise pytest.UsageError("--display and --valgrind are mutually exclusive")
|
||||||
|
if config.getoption("--server-path"):
|
||||||
|
raise pytest.UsageError(
|
||||||
|
"--display and --server-path are mutually exclusive"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_display(value):
|
||||||
|
"""Parse a display string like ':42' or '42' into an integer."""
|
||||||
|
value = value.strip().lstrip(":")
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise pytest.UsageError(f"Invalid display value: {value!r}")
|
||||||
|
|
||||||
|
|
||||||
def get_server_types(config) -> list[str]:
|
def get_server_types(config) -> list[str]:
|
||||||
"""Get the list of server types to test, default to xvfb."""
|
"""Get the list of server types to test.
|
||||||
return config.getoption("--server-type") or ["xvfb"]
|
|
||||||
|
With ``--display``, defaults to ``["external"]`` if no explicit
|
||||||
|
``--server-type`` is given. Otherwise defaults to ``["xvfb"]``.
|
||||||
|
"""
|
||||||
|
types = config.getoption("--server-type") or []
|
||||||
|
if config.getoption("--display", default=None) is not None:
|
||||||
|
return types or ["external"]
|
||||||
|
return types or ["xvfb"]
|
||||||
|
|
||||||
|
|
||||||
def get_valgrind_suppressions(config) -> Path | None:
|
def get_valgrind_suppressions(config) -> Path | None:
|
||||||
|
|
@ -209,6 +243,9 @@ def xserver(request, tmp_path):
|
||||||
A fresh server per test, killed afterward. With --valgrind,
|
A fresh server per test, killed afterward. With --valgrind,
|
||||||
valgrind memory errors cause test failure during teardown.
|
valgrind memory errors cause test failure during teardown.
|
||||||
|
|
||||||
|
When ``--display`` is given, no server is started; an
|
||||||
|
:class:`ExternalXServer` proxy is yielded instead.
|
||||||
|
|
||||||
For a fixture that targets a specific server type use the xvfb,
|
For a fixture that targets a specific server type use the xvfb,
|
||||||
xwayland, or xorg fixtures instead.
|
xwayland, or xorg fixtures instead.
|
||||||
"""
|
"""
|
||||||
|
|
@ -220,6 +257,13 @@ def xserver(request, tmp_path):
|
||||||
if request.node.get_closest_marker("xorg_only") and server_type != "xorg":
|
if request.node.get_closest_marker("xorg_only") and server_type != "xorg":
|
||||||
pytest.skip("Test only applies to Xorg")
|
pytest.skip("Test only applies to Xorg")
|
||||||
|
|
||||||
|
# External server mode: no server lifecycle management
|
||||||
|
display = request.config.getoption("--display")
|
||||||
|
if display is not None:
|
||||||
|
display_num = _parse_display(display)
|
||||||
|
yield ExternalXServer(display_num, server_type=server_type)
|
||||||
|
return
|
||||||
|
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if server_type == "xorg":
|
if server_type == "xorg":
|
||||||
kwargs["log_file"] = tmp_path / f"{server_type}.log"
|
kwargs["log_file"] = tmp_path / f"{server_type}.log"
|
||||||
|
|
@ -230,6 +274,10 @@ def xserver(request, tmp_path):
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def xvfb(request, tmp_path):
|
def xvfb(request, tmp_path):
|
||||||
"""Start an Xvfb server for this test."""
|
"""Start an Xvfb server for this test."""
|
||||||
|
display = request.config.getoption("--display")
|
||||||
|
if display is not None:
|
||||||
|
yield ExternalXServer(_parse_display(display), server_type="xvfb")
|
||||||
|
return
|
||||||
if "xvfb" not in get_server_types(request.config):
|
if "xvfb" not in get_server_types(request.config):
|
||||||
pytest.skip("Xvfb not in --server-type list")
|
pytest.skip("Xvfb not in --server-type list")
|
||||||
yield from _start_server(request, "xvfb")
|
yield from _start_server(request, "xvfb")
|
||||||
|
|
@ -238,6 +286,10 @@ def xvfb(request, tmp_path):
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def xwayland(request, tmp_path):
|
def xwayland(request, tmp_path):
|
||||||
"""Start an Xwayland server for this test."""
|
"""Start an Xwayland server for this test."""
|
||||||
|
display = request.config.getoption("--display")
|
||||||
|
if display is not None:
|
||||||
|
yield ExternalXServer(_parse_display(display), server_type="xwayland")
|
||||||
|
return
|
||||||
if "xwayland" not in get_server_types(request.config):
|
if "xwayland" not in get_server_types(request.config):
|
||||||
pytest.skip("Xwayland not in --server-type list")
|
pytest.skip("Xwayland not in --server-type list")
|
||||||
yield from _start_server(request, "xwayland")
|
yield from _start_server(request, "xwayland")
|
||||||
|
|
@ -246,6 +298,10 @@ def xwayland(request, tmp_path):
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def xorg(request, tmp_path):
|
def xorg(request, tmp_path):
|
||||||
"""Start an Xorg server for this test."""
|
"""Start an Xorg server for this test."""
|
||||||
|
display = request.config.getoption("--display")
|
||||||
|
if display is not None:
|
||||||
|
yield ExternalXServer(_parse_display(display), server_type="xorg")
|
||||||
|
return
|
||||||
if "xorg" not in get_server_types(request.config):
|
if "xorg" not in get_server_types(request.config):
|
||||||
pytest.skip("Xorg not in --server-type list")
|
pytest.skip("Xorg not in --server-type list")
|
||||||
yield from _start_server(request, "xorg", log_file=tmp_path / "xorg.log")
|
yield from _start_server(request, "xorg", log_file=tmp_path / "xorg.log")
|
||||||
|
|
|
||||||
|
|
@ -427,3 +427,78 @@ class XServerProcess:
|
||||||
if self.asan:
|
if self.asan:
|
||||||
flags += " +asan"
|
flags += " +asan"
|
||||||
return f"<XServerProcess {self.server_type} {display} [{state}]{flags}>"
|
return f"<XServerProcess {self.server_type} {display} [{state}]{flags}>"
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalXServer:
|
||||||
|
"""Proxy for an externally-managed X server (``--display`` mode).
|
||||||
|
|
||||||
|
Used when the user passes ``--display`` to connect to an already-running
|
||||||
|
server instead of launching one per test. The server's PID is discovered
|
||||||
|
via ``SO_PEERCRED`` on the Unix socket so that :attr:`is_alive` can
|
||||||
|
report whether the process is still running.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, display_num, server_type="external"):
|
||||||
|
self._display_num = display_num
|
||||||
|
self.server_type = server_type
|
||||||
|
self._pid = self._get_server_pid()
|
||||||
|
|
||||||
|
def _get_server_pid(self):
|
||||||
|
"""Get the PID of the X server via SO_PEERCRED on the Unix socket."""
|
||||||
|
import socket as _socket
|
||||||
|
import struct as _struct
|
||||||
|
|
||||||
|
path = f"/tmp/.X11-unix/X{self._display_num}"
|
||||||
|
try:
|
||||||
|
sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM)
|
||||||
|
sock.connect(path)
|
||||||
|
cred = sock.getsockopt(
|
||||||
|
_socket.SOL_SOCKET, _socket.SO_PEERCRED, _struct.calcsize("iii")
|
||||||
|
)
|
||||||
|
pid, _, _ = _struct.unpack("iii", cred)
|
||||||
|
sock.close()
|
||||||
|
return pid
|
||||||
|
except (OSError, _struct.error):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_num(self):
|
||||||
|
return self._display_num
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display(self):
|
||||||
|
return f":{self._display_num}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_alive(self) -> bool:
|
||||||
|
"""Check if the external server process is still running."""
|
||||||
|
if self._pid is None:
|
||||||
|
return True # can't check, assume alive
|
||||||
|
try:
|
||||||
|
os.kill(self._pid, 0)
|
||||||
|
return True
|
||||||
|
except ProcessLookupError:
|
||||||
|
return False
|
||||||
|
except PermissionError:
|
||||||
|
return True # process exists but we can't signal it
|
||||||
|
|
||||||
|
@property
|
||||||
|
def crash_signal(self) -> int | None:
|
||||||
|
"""Always None -- we cannot determine this for external servers."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""No-op -- never stop an external server."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_asan_errors(self):
|
||||||
|
"""No ASAN log access for external servers."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def check_valgrind_errors(self):
|
||||||
|
"""No valgrind access for external servers."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
state = "running" if self.is_alive else "stopped"
|
||||||
|
return f"<ExternalXServer :{self._display_num} [{state}] pid={self._pid}>"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue