Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/mss/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ def _validate(retval: int, func: Any, args: tuple[Any, Any], /) -> tuple[Any, An
"XGetWindowAttributes": ("xlib", [POINTER(Display), POINTER(XWindowAttributes), POINTER(XWindowAttributes)], c_int),
"XOpenDisplay": ("xlib", [c_char_p], POINTER(Display)),
"XQueryExtension": ("xlib", [POINTER(Display), c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)], c_uint),
"XRRQueryVersion": ("xrandr", [POINTER(Display), POINTER(c_int), POINTER(c_int)], c_int),
"XRRFreeCrtcInfo": ("xrandr", [POINTER(XRRCrtcInfo)], c_void_p),
"XRRFreeScreenResources": ("xrandr", [POINTER(XRRScreenResources)], c_void_p),
"XRRGetCrtcInfo": ("xrandr", [POINTER(Display), POINTER(XRRScreenResources), c_long], POINTER(XRRCrtcInfo)),
Expand Down Expand Up @@ -388,6 +389,10 @@ def _monitors_impl(self) -> None:
int_ = int
xrandr = self.xrandr

xrandr_major = c_int(0)
xrandr_minor = c_int(0)
xrandr.XRRQueryVersion(display, xrandr_major, xrandr_minor)

# All monitors
gwa = XWindowAttributes()
self.xlib.XGetWindowAttributes(display, self._handles.root, byref(gwa))
Expand All @@ -400,9 +405,11 @@ def _monitors_impl(self) -> None:
# XRRGetScreenResources(): 0.1755971429956844 s
# XRRGetScreenResourcesCurrent(): 0.0039125580078689 s
# The second is faster by a factor of 44! So try to use it first.
try:
# It doesn't query the monitors for updated information, but it does require the server to support
# RANDR 1.3. We also make sure the client supports 1.3, by checking for the presence of the function.
if hasattr(xrandr, "XRRGetScreenResourcesCurrent") and (xrandr_major.value, xrandr_minor.value) >= (1, 3):
mon = xrandr.XRRGetScreenResourcesCurrent(display, self._handles.drawable).contents
except AttributeError:
else:
mon = xrandr.XRRGetScreenResources(display, self._handles.drawable).contents

crtcs = mon.crtcs
Expand Down
66 changes: 63 additions & 3 deletions src/tests/test_gnu_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

import platform
from collections.abc import Generator
from unittest.mock import Mock, patch
from ctypes import CFUNCTYPE, POINTER, _Pointer, c_int
from typing import Any
from unittest.mock import Mock, NonCallableMock, patch

import pytest

Expand All @@ -22,6 +24,14 @@
DEPTH = 24


def spy_and_patch(monkeypatch: pytest.MonkeyPatch, obj: Any, name: str) -> Mock:
"""Replace obj.name with a call-through mock and return the mock."""
real = getattr(obj, name)
spy = Mock(wraps=real)
monkeypatch.setattr(obj, name, spy, raising=False)
return spy


@pytest.fixture
def display() -> Generator:
with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH) as vdisplay:
Expand Down Expand Up @@ -133,20 +143,70 @@ def test__is_extension_enabled_unknown_name(display: str) -> None:
assert not sct._is_extension_enabled("NOEXT")


def test_missing_fast_function_for_monitor_details_retrieval(display: str) -> None:
def test_fast_function_for_monitor_details_retrieval(display: str, monkeypatch: pytest.MonkeyPatch) -> None:
with mss.mss(display=display) as sct:
assert isinstance(sct, mss.linux.MSS) # For Mypy
assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent")
fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent")
slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources")
screenshot_with_fast_fn = sct.grab(sct.monitors[1])

fast_spy.assert_called()
slow_spy.assert_not_called()

assert set(screenshot_with_fast_fn.rgb) == {0}


def test_client_missing_fast_function_for_monitor_details_retrieval(
display: str, monkeypatch: pytest.MonkeyPatch
) -> None:
with mss.mss(display=display) as sct:
assert isinstance(sct, mss.linux.MSS) # For Mypy
assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent")
del sct.xrandr.XRRGetScreenResourcesCurrent
# Even though we're going to delete it, we'll still create a fast spy, to make sure that it isn't somehow
# getting accessed through a path we hadn't considered.
fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent")
slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources")
# If we just delete sct.xrandr.XRRGetScreenResourcesCurrent, it will get recreated automatically by ctypes
# the next time it's accessed. A Mock will remember that the attribute was explicitly deleted and hide it.
mock_xrandr = NonCallableMock(wraps=sct.xrandr)
del mock_xrandr.XRRGetScreenResourcesCurrent
monkeypatch.setattr(sct, "xrandr", mock_xrandr)
assert not hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent")
screenshot_with_slow_fn = sct.grab(sct.monitors[1])

fast_spy.assert_not_called()
slow_spy.assert_called()

assert set(screenshot_with_slow_fn.rgb) == {0}


def test_server_missing_fast_function_for_monitor_details_retrieval(
display: str, monkeypatch: pytest.MonkeyPatch
) -> None:
fake_xrrqueryversion_type = CFUNCTYPE(
c_int, # Status
POINTER(mss.linux.Display), # Display*
POINTER(c_int), # int* major
POINTER(c_int), # int* minor
)

@fake_xrrqueryversion_type
def fake_xrrqueryversion(_dpy: _Pointer, major_p: _Pointer, minor_p: _Pointer) -> int:
major_p[0] = 1
minor_p[0] = 2
return 1

with mss.mss(display=display) as sct:
assert isinstance(sct, mss.linux.MSS) # For Mypy
monkeypatch.setattr(sct.xrandr, "XRRQueryVersion", fake_xrrqueryversion)
fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent")
slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources")
screenshot_with_slow_fn = sct.grab(sct.monitors[1])

fast_spy.assert_not_called()
slow_spy.assert_called()

assert set(screenshot_with_slow_fn.rgb) == {0}


Expand Down
Loading