Skip to content

Commit 163ec2d

Browse files
committed
Improve testing
Test the cases of missing libraries when using XCB. Add fixtures to reset the XCB library loader after each testcase. Add fixtures to check for Xlib errors after each testcase.
1 parent 606badb commit 163ec2d

File tree

4 files changed

+97
-23
lines changed

4 files changed

+97
-23
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ strict_equality = true
134134

135135
[tool.pytest.ini_options]
136136
pythonpath = "src"
137+
markers = ["without_libraries"]
137138
addopts = """
138139
--showlocals
139140
--strict-markers

src/mss/linux/xcb.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import ctypes.util
6+
from contextlib import suppress
57
from copy import copy
68
from ctypes import (
79
CDLL,
@@ -22,7 +24,6 @@
2224
cast,
2325
cdll,
2426
)
25-
from ctypes.util import find_library
2627
from threading import Lock
2728
from typing import TYPE_CHECKING
2829
from weakref import finalize
@@ -857,6 +858,13 @@ def __init__(self) -> None:
857858
self._lock = Lock()
858859
self._initialized = False
859860

861+
def reset(self) -> None:
862+
with self._lock:
863+
self._initialized = False
864+
for name in self._EXPOSED_NAMES:
865+
with suppress(AttributeError):
866+
delattr(self, name)
867+
860868
def __getattr__(self, name: str) -> CDLL:
861869
# In normal use, this will only be called once (for a library). After that, all the names will be populated in
862870
# __dict__, and this fallback won't be used.
@@ -872,6 +880,7 @@ def __getattr__(self, name: str) -> CDLL:
872880
def load(self) -> None:
873881
with self._lock:
874882
if self._initialized:
883+
# Something else initialized this object while we were waiting for the lock.
875884
return
876885

877886
# We don't use the cached versions that ctypes.cdll exposes as attributes, since other libraries may be
@@ -886,9 +895,14 @@ def load(self) -> None:
886895
# Alternatively, we might define the functions to be initialized with a decorator on the functions that use
887896
# them.
888897

889-
self.xcb = cdll.LoadLibrary(find_library("xcb"))
898+
libxcb_so = ctypes.util.find_library("xcb")
899+
if libxcb_so is None:
900+
msg = "Library libxcb.so not found"
901+
raise ScreenShotError(msg)
902+
self.xcb = cdll.LoadLibrary(libxcb_so)
890903

891904
# Ordered as <xcb/xcb.h>
905+
892906
self.xcb.xcb_request_check.argtypes = [POINTER(XcbConnection), XcbVoidCookie]
893907
self.xcb.xcb_request_check.restype = POINTER(XcbGenericErrorStructure)
894908
self.xcb.xcb_discard_reply.argtypes = [POINTER(XcbConnection), c_uint]
@@ -908,6 +922,7 @@ def load(self) -> None:
908922
self.xcb.xcb_disconnect.restype = None
909923

910924
# Ordered as <xcb/xproto.h>
925+
911926
self.xcb.xcb_depth_visuals.argtypes = [POINTER(XcbDepth)]
912927
self.xcb.xcb_depth_visuals.restype = POINTER(XcbVisualtype)
913928
self.xcb.xcb_depth_visuals_length.argtypes = [POINTER(XcbDepth)]
@@ -957,7 +972,12 @@ def load(self) -> None:
957972
initialize_xcb_void_func(self.xcb, "xcb_no_operation_checked", [POINTER(XcbConnection)])
958973

959974
# Ordered as <xcb/randr.h>
960-
self.randr = cdll.LoadLibrary(find_library("xcb-randr"))
975+
976+
libxcb_randr_so = ctypes.util.find_library("xcb-randr")
977+
if libxcb_randr_so is None:
978+
msg = "Library libxcb-randr.so not found"
979+
raise ScreenShotError(msg)
980+
self.randr = cdll.LoadLibrary(libxcb_randr_so)
961981
self.randr_id = XcbExtension.in_dll(self.randr, "xcb_randr_id")
962982
initialize_xcb_typed_func(
963983
self.randr,
@@ -997,7 +1017,12 @@ def load(self) -> None:
9971017
self.randr.xcb_randr_get_screen_resources_current_crtcs_length.restype = c_int
9981018

9991019
# Ordered as <xcb/render.h>
1000-
self.render = cdll.LoadLibrary(find_library("xcb-render"))
1020+
1021+
libxcb_render_so = ctypes.util.find_library("xcb-render")
1022+
if libxcb_render_so is None:
1023+
msg = "Library libxcb-render.so not found"
1024+
raise ScreenShotError(msg)
1025+
self.render = cdll.LoadLibrary(libxcb_render_so)
10011026
self.render_id = XcbExtension.in_dll(self.render, "xcb_render_id")
10021027

10031028
self.render.xcb_render_pictdepth_visuals.argtypes = [POINTER(XcbRenderPictdepth)]
@@ -1035,7 +1060,11 @@ def load(self) -> None:
10351060

10361061
# Ordered as <xcb/xfixes.h>
10371062

1038-
self.xfixes = cdll.LoadLibrary(find_library("xcb-xfixes"))
1063+
libxcb_xfixes_so = ctypes.util.find_library("xcb-xfixes")
1064+
if libxcb_xfixes_so is None:
1065+
msg = "Library libxcb-xfixes.so not found"
1066+
raise ScreenShotError(msg)
1067+
self.xfixes = cdll.LoadLibrary(libxcb_xfixes_so)
10391068
self.xfixes_id = XcbExtension.in_dll(self.xfixes, "xcb_xfixes_id")
10401069

10411070
initialize_xcb_typed_func(

src/tests/conftest.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import os
66
from collections.abc import Callable, Generator
7-
from functools import partial
87
from hashlib import sha256
98
from pathlib import Path
109
from platform import system
@@ -14,6 +13,7 @@
1413

1514
from mss import mss
1615
from mss.base import MSSBase
16+
from mss.linux import xcb, xlib
1717

1818

1919
@pytest.fixture(autouse=True)
@@ -43,6 +43,23 @@ def _before_tests() -> None:
4343
purge_files()
4444

4545

46+
@pytest.fixture(autouse=True)
47+
def no_xlib_errors(request: pytest.FixtureRequest) -> None:
48+
system() == "Linux" and ("backend" not in request.fixturenames or request.getfixturevalue("backend") == "xlib")
49+
assert not xlib._ERROR
50+
51+
52+
@pytest.fixture(autouse=True)
53+
def reset_xcb_libraries(request: pytest.FixtureRequest) -> Generator[None]:
54+
# We need to test this before we yield, since the backend isn't available afterwards.
55+
xcb_should_reset = system() == "Linux" and (
56+
"backend" not in request.fixturenames or request.getfixturevalue("backend") == "xcb"
57+
)
58+
yield None
59+
if xcb_should_reset:
60+
xcb.LIB.reset()
61+
62+
4663
@pytest.fixture(scope="session")
4764
def raw() -> bytes:
4865
file = Path(__file__).parent / "res" / "monitor-1024x768.raw.zip"
@@ -60,4 +77,6 @@ def backend(request: pytest.FixtureRequest) -> str:
6077

6178
@pytest.fixture
6279
def mss_impl(backend: str) -> Callable[..., MSSBase]:
63-
return partial(mss, display=os.getenv("DISPLAY"), backend=backend)
80+
# We can't just use partial here, since it will read $DISPLAY at the wrong time. This can cause problems,
81+
# depending on just how the fixtures get run.
82+
return lambda *args, **kwargs: mss(*args, display=os.getenv("DISPLAY"), backend=backend, **kwargs)

src/tests/test_gnu_linux.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,26 @@
22
Source: https://github.com/BoboTiG/python-mss.
33
"""
44

5+
from __future__ import annotations
6+
7+
import ctypes.util
58
import platform
6-
from collections.abc import Generator
79
from ctypes import CFUNCTYPE, POINTER, _Pointer, c_int
8-
from typing import Any
10+
from typing import TYPE_CHECKING, Any
911
from unittest.mock import Mock, NonCallableMock, patch
1012

1113
import pytest
1214

1315
import mss
1416
import mss.linux
17+
import mss.linux.xcb
1518
import mss.linux.xlib
1619
from mss.base import MSSBase
1720
from mss.exception import ScreenShotError
1821

22+
if TYPE_CHECKING:
23+
from collections.abc import Generator
24+
1925
pyvirtualdisplay = pytest.importorskip("pyvirtualdisplay")
2026

2127
PYPY = platform.python_implementation() == "PyPy"
@@ -33,6 +39,26 @@ def spy_and_patch(monkeypatch: pytest.MonkeyPatch, obj: Any, name: str) -> Mock:
3339
return spy
3440

3541

42+
@pytest.fixture(autouse=True)
43+
def without_libraries(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest) -> Generator[None]:
44+
marker = request.node.get_closest_marker("without_libraries")
45+
if marker is None:
46+
yield None
47+
return
48+
skip_find = frozenset(marker.args)
49+
old_find_library = ctypes.util.find_library
50+
51+
def new_find_library(name: str, *args: list, **kwargs: dict[str, Any]) -> str | None:
52+
if name in skip_find:
53+
return None
54+
return old_find_library(name, *args, **kwargs)
55+
56+
# We use a context here so other fixtures or the test itself can use .undo.
57+
with monkeypatch.context() as mp:
58+
mp.setattr(ctypes.util, "find_library", new_find_library)
59+
yield None
60+
61+
3662
@pytest.fixture
3763
def display() -> Generator:
3864
with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH) as vdisplay:
@@ -84,15 +110,18 @@ def test_arg_display(display: str, backend: str, monkeypatch: pytest.MonkeyPatch
84110
pass
85111

86112
# No `DISPLAY` in envars
87-
monkeypatch.delenv("DISPLAY")
88-
with pytest.raises(ScreenShotError), mss.mss(backend=backend):
89-
pass
113+
# The monkeypatch implementation of delenv seems to interact badly with some other uses of setenv, so we use a
114+
# monkeypatch context to isolate it a bit.
115+
with monkeypatch.context() as mp:
116+
mp.delenv("DISPLAY")
117+
with pytest.raises(ScreenShotError), mss.mss(backend=backend):
118+
pass
90119

91120

92121
def test_xerror_without_details() -> None:
93122
# Opening an invalid display with the Xlib backend will create an XError instance, but since there was no
94123
# XErrorEvent, then the details won't be filled in. Generate one.
95-
with pytest.raises(ScreenShotError) as excinfo, mss.mss(display=":INVALID", backend="xlib"):
124+
with pytest.raises(ScreenShotError) as excinfo, mss.mss(display=":INVALID"):
96125
pass
97126

98127
exc = excinfo.value
@@ -102,15 +131,17 @@ def test_xerror_without_details() -> None:
102131
str(exc)
103132

104133

134+
@pytest.mark.without_libraries("xcb")
105135
@patch("mss.linux.xlib._X11", new=None)
106-
def test_no_xlib_library() -> None:
107-
with pytest.raises(ScreenShotError), mss.mss(backend="xlib"):
136+
def test_no_xlib_library(backend: str) -> None:
137+
with pytest.raises(ScreenShotError), mss.mss(backend=backend):
108138
pass
109139

110140

141+
@pytest.mark.without_libraries("xcb-randr")
111142
@patch("mss.linux.xlib._XRANDR", new=None)
112-
def test_no_xrandr_extension() -> None:
113-
with pytest.raises(ScreenShotError), mss.mss(backend="xlib"):
143+
def test_no_xrandr_extension(backend: str) -> None:
144+
with pytest.raises(ScreenShotError), mss.mss(backend=backend):
114145
pass
115146

116147

@@ -132,9 +163,6 @@ def test_unsupported_depth(backend: str) -> None:
132163
def test_region_out_of_monitor_bounds(display: str, backend: str) -> None:
133164
monitor = {"left": -30, "top": 0, "width": WIDTH, "height": HEIGHT}
134165

135-
if backend == "xlib":
136-
assert not mss.linux.xlib._ERROR
137-
138166
with mss.mss(display=display, backend=backend, with_cursor=True) as sct:
139167
# At one point, I had accidentally been reporting the resource ID as a CData object instead of the contained
140168
# int. This is to make sure I don't repeat that mistake. That said, change this error regex if needed to keep
@@ -164,9 +192,6 @@ def test_region_out_of_monitor_bounds(display: str, backend: str) -> None:
164192
if backend == "xlib":
165193
assert not mss.linux.xlib._ERROR
166194

167-
if backend == "xlib":
168-
assert not mss.linux.xlib._ERROR
169-
170195

171196
def test__is_extension_enabled_unknown_name(display: str) -> None:
172197
with mss.mss(display=display, backend="xlib") as sct:

0 commit comments

Comments
 (0)