Repository: BoboTiG/python-mss

Issue:
Xvfb error while trying to open multiple xvfb screens in single python session
General information:

* OS name: Ubuntu
* OS version: 20.04
* OS architecture: 64 bits
* Resolutions:
    * Monitor 1: 1920x1080
* Python version: 3.8.10
* MSS version: 6.1.0


For GNU/Linux users:

* Display server protocol and version, if known: Xorg
* Desktop Environment: Gnome
* Composite Window Manager name and version: Not sure


### Description of the warning/error

The error comes when I try to call mss.mss() in a single python session with two different Xvfb screens.

### Full message

`XIO:  fatal IO error 0 (Success) on X server ":3"
      after 8 requests (8 known processed) with 0 events remaining.`

### Other details

Steps to reproduce:

``` python
from xvfbwrapper import Xvfb
import mss
import os

xvfb = Xvfb(width=1920, height=1080, colordepth=24)
xvfb.start()

with mss.mss() as sct:
    pass
xvfb.stop()

xvfb = Xvfb(width=1920, height=1080, colordepth=24)
xvfb.start()

with mss.mss() as sct:
    pass
```
The error pops up exactly [on this line](https://github.com/BoboTiG/python-mss/blob/78e5a8de625734ae84235a82af03a803d56da58b/mss/linux.py#L323)

Discussion:

Patch:
diff --git a/.pylintrc b/.pylintrc
index 07fee3f..97fac66 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -2,5 +2,6 @@
 disable = locally-disabled, too-few-public-methods, too-many-instance-attributes, duplicate-code
 
 [REPORTS]
+max-line-length = 120
 output-format = colorized
 reports = no
diff --git a/CHANGELOG b/CHANGELOG
index a5241ad..26e2186 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,11 +3,13 @@ History:
 <see Git checking messages for history>
 
 8.0.0   2023/0x/xx
-      - Linux: added mouse support (partially fixes #55)
-      - Linux: removed get_error_details(), use the ScreenShotError details attribute instead
-      - dev: removed pre-commit
-      - tests: removed tox
-      - tests: added PyPy 3.9
+      - Linux: added mouse support (#232)
+      - Linux: refactored how internal handles are stored to fix issues with multiple X servers, and TKinter.
+               No more side effects, and when leaving the context manager, resources are all freed (#224, #234)
+      - ci: added PyPy 3.9 (#226)
+      - dev: removed pre-commit (#226)
+      - tests: removed tox (#226)
+      - tests: improved coverage (#234)
 
 7.0.1   2022/10/27
       - fixed the wheel package
diff --git a/CHANGES.rst b/CHANGES.rst
index 7cd482f..d03e4db 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,19 +1,43 @@
+8.0.0 (2023-xx-xx)
+==================
+
+base.py
+-------
+- Added ``compression_level=6`` keyword argument to ``MSS.__init__()``
+- Added ``display=None`` keyword argument to ``MSS.__init__()``
+- Added ``max_displays=32`` keyword argument to ``MSS.__init__()``
+- Added ``with_cursor=False`` keyword argument to ``MSS.__init__()``
+- Added ``MSS.with_cursor`` attribute
+
+linux.py
+--------
+- Added ``MSS.close()``
+- Moved ``MSS.__init__()`` keyword arguments handling to the base class
+- Renamed ``error_handler()`` function to ``__error_handler()``
+- Renamed ``_validate()`` function to ``___validate()``
+- Renamed ``MSS.has_extension()`` method to ``_is_extension_enabled()``
+- Removed ``ERROR`` namespace
+- Removed ``MSS.drawable`` attribute
+- Removed ``MSS.root`` attribute
+- Removed ``MSS.get_error_details()`` method. Use ``ScreenShotError.details`` attribute instead.
+
+
 6.1.0 (2020-10-31)
 ==================
 
 darwin.py
 ---------
- - Added ``CFUNCTIONS``
+- Added ``CFUNCTIONS``
 
 linux.py
 --------
- - Added ``CFUNCTIONS``
+- Added ``CFUNCTIONS``
 
 windows.py
 ----------
- - Added ``CFUNCTIONS``
- - Added ``MONITORNUMPROC``
- - Removed ``MSS.monitorenumproc``. Use ``MONITORNUMPROC`` instead.
+- Added ``CFUNCTIONS``
+- Added ``MONITORNUMPROC``
+- Removed ``MSS.monitorenumproc``. Use ``MONITORNUMPROC`` instead.
 
 
 6.0.0 (2020-06-30)
@@ -21,30 +45,30 @@ windows.py
 
 base.py
 -------
- - Added ``lock``
- - Added ``MSS._grab_impl()`` (abstract method)
- - Added ``MSS._monitors_impl()`` (abstract method)
- - ``MSS.grab()`` is no more an abstract method
- - ``MSS.monitors`` is no more an abstract property
+- Added ``lock``
+- Added ``MSS._grab_impl()`` (abstract method)
+- Added ``MSS._monitors_impl()`` (abstract method)
+- ``MSS.grab()`` is no more an abstract method
+- ``MSS.monitors`` is no more an abstract property
 
 darwin.py
 ---------
- - Renamed ``MSS.grab()`` to ``MSS._grab_impl()``
- - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()``
+- Renamed ``MSS.grab()`` to ``MSS._grab_impl()``
+- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()``
 
 linux.py
 --------
- - Added ``MSS.has_extension()``
- - Removed ``MSS.display``
- - Renamed ``MSS.grab()`` to ``MSS._grab_impl()``
- - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()``
+- Added ``MSS.has_extension()``
+- Removed ``MSS.display``
+- Renamed ``MSS.grab()`` to ``MSS._grab_impl()``
+- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()``
 
 windows.py
 ----------
- - Removed ``MSS._lock``
- - Renamed ``MSS.srcdc_dict`` to ``MSS._srcdc_dict``
- - Renamed ``MSS.grab()`` to ``MSS._grab_impl()``
- - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()``
+- Removed ``MSS._lock``
+- Renamed ``MSS.srcdc_dict`` to ``MSS._srcdc_dict``
+- Renamed ``MSS.grab()`` to ``MSS._grab_impl()``
+- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()``
 
 
 5.1.0 (2020-04-30)
@@ -59,7 +83,7 @@ base.py
 
 windows.py
 ----------
- - Replaced ``MSS.srcdc`` with ``MSS.srcdc_dict``
+- Replaced ``MSS.srcdc`` with ``MSS.srcdc_dict``
 
 
 5.0.0 (2019-12-31)
@@ -67,12 +91,12 @@ windows.py
 
 darwin.py
 ---------
-- Added `MSS.__slots__`
+- Added ``MSS.__slots__``
 
 linux.py
 --------
-- Added `MSS.__slots__`
-- Deleted `MSS.close()`
+- Added ``MSS.__slots__``
+- Deleted ``MSS.close()``
 - Deleted ``LAST_ERROR`` constant. Use ``ERROR`` namespace instead, specially the ``ERROR.details`` attribute.
 
 models.py
@@ -92,8 +116,8 @@ screenshot.py
 
 windows.py
 ----------
-- Added `MSS.__slots__`
-- Deleted `MSS.close()`
+- Added ``MSS.__slots__``
+- Deleted ``MSS.close()``
 
 
 4.0.1 (2019-01-26)
@@ -149,15 +173,15 @@ windows.py
 
 base.py
 -------
-- Added ``MSSBase.compression_level`` to control the PNG compression level
+- Added ``MSSBase.compression_level`` attribute
 
 linux.py
 --------
-- Added ``MSS.drawable`` to speed-up grabbing.
+- Added ``MSS.drawable`` attribute
 
 screenshot.py
 -------------
-- Added ``Screenshot.bgra`` to get BGRA bytes.
+- Added ``Screenshot.bgra`` attribute
 
 tools.py
 --------
@@ -181,19 +205,19 @@ __main__.py
 
 base.py
 -------
-- Moved ``ScreenShot`` class to screenshot.py
+- Moved ``ScreenShot`` class to ``screenshot.py``
 
 darwin.py
 ---------
-- Added ``CGPoint.__repr__()``
-- Added ``CGRect.__repr__()``
-- Added ``CGSize.__repr__()``
+- Added ``CGPoint.__repr__()`` function
+- Added ``CGRect.__repr__()`` function
+- Added ``CGSize.__repr__()`` function
 - Removed ``get_infinity()`` function
 
 windows.py
 ----------
-- Added ``scale()`` method to ``MSS`` class
-- Added ``scale_factor`` property to ``MSS`` class
+- Added ``MSS.scale()`` method
+- Added ``MSS.scale_factor`` property
 
 
 3.0.0 (2017-07-06)
diff --git a/check.sh b/check.sh
index ba8a974..e9000a8 100755
--- a/check.sh
+++ b/check.sh
@@ -3,7 +3,7 @@
 # Small script to ensure quality checks pass before submitting a commit/PR.
 #
 python -m isort docs mss
-python -m black docs mss
+python -m black --line-length=120 docs mss
 python -m flake8 docs mss
 python -m pylint mss
 # "--platform win32" to not fail on ctypes.windll (it does not affect the overall check on other OSes)
diff --git a/dev-requirements.txt b/dev-requirements.txt
index c5d21e0..589b76e 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -13,3 +13,4 @@ pylint
 sphinx
 twine
 wheel
+xvfbwrapper; sys_platform == "linux"
diff --git a/docs/source/api.rst b/docs/source/api.rst
index 451f562..c4fe102 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -12,6 +12,22 @@ macOS
 
 .. attribute:: CFUNCTIONS
 
+    .. versionadded:: 6.1.0
+
+.. function:: cgfloat
+
+.. class:: CGPoint
+
+.. class:: CGSize
+
+.. class:: CGRect
+
+.. class:: MSS
+
+    .. attribute:: core
+
+    .. attribute:: max_displays
+
 GNU/Linux
 ---------
 
@@ -19,44 +35,37 @@ GNU/Linux
 
 .. attribute:: CFUNCTIONS
 
+    .. versionadded:: 6.1.0
+
 .. attribute:: PLAINMASK
 
 .. attribute:: ZPIXMAP
 
-.. class:: MSS
+.. class:: Display
 
-    .. method:: __init__([display=None, with_cursor=False])
+.. class:: Event
 
-        :type display: str or None
-        :param display: The display to use.
-        :param with_cursor: Include the mouse cursor in screenshots.
+.. class:: XFixesCursorImage
 
-        GNU/Linux initializations.
+.. class:: XWindowAttributes
 
-            .. versionadded:: 8.0.0
-                `with_cursor` keyword argument.
+.. class:: XImage
 
-    .. method:: grab(monitor)
+.. class:: XRRModeInfo
 
-        :rtype: :class:`~mss.base.ScreenShot`
-        :raises ScreenShotError: When color depth is not 32 (rare).
+.. class:: XRRScreenResources
 
-        See :meth:`~mss.base.MSSBase.grab()` for details.
+.. class:: XRRCrtcInfo
 
-.. function:: error_handler(display, event)
+.. class:: MSS
 
-    :type display: ctypes.POINTER(Display)
-    :param display: The display impacted by the error.
-    :type event: ctypes.POINTER(Event)
-    :param event: XError details.
-    :return int: Always ``0``.
+    .. attribute:: core
 
-    Error handler passed to `X11.XSetErrorHandler()` to catch any error that can happen when calling a X11 function.
-    This will prevent Python interpreter crashes.
+    .. method:: close()
 
-    When such an error happen, a :class:`~mss.exception.ScreenShotError` exception is raised and all `XError` information are added to the :attr:`~mss.exception.ScreenShotError.details` attribute.
+        Clean-up method.
 
-    .. versionadded:: 3.3.0
+        .. versionadded:: 8.0.0
 
 Windows
 -------
@@ -67,28 +76,70 @@ Windows
 
 .. attribute:: CFUNCTIONS
 
+    .. versionadded:: 6.1.0
+
 .. attribute:: DIB_RGB_COLORS
 
 .. attribute:: SRCCOPY
 
+.. class:: BITMAPINFOHEADER
+
+.. class:: BITMAPINFO
+
+.. attribute:: MONITORNUMPROC
+
+    .. versionadded:: 6.1.0
+
+.. class:: MSS
+
+    .. attribute:: gdi32
+
+    .. attribute:: user32
+
 Methods
 =======
 
 .. module:: mss.base
 
+.. attribute:: lock
+
+    .. versionadded:: 6.0.0
+
 .. class:: MSSBase
 
     The parent's class for every OS implementation.
 
+    .. attribute:: cls_image
+
     .. attribute:: compression_level
 
         PNG compression level used when saving the screenshot data into a file (see :py:func:`zlib.compress()` for details).
 
         .. versionadded:: 3.2.0
 
+    .. attribute:: with_cursor
+
+        Include the mouse cursor in screenshots.
+
+        .. versionadded:: 8.0.0
+
+    .. method:: __init__(compression_level=6, display=None, max_displays=32, with_cursor=False)
+
+        :type compression_level: int
+        :param compression_level: PNG compression level.
+        :type display: bytes, str or None
+        :param display: The display to use. Only effective on GNU/Linux.
+        :type max_displays: int
+        :param max_displays: Maximum number of displays. Only effective on macOS.
+        :type with_cursor: bool
+        :param with_cursor: Include the mouse cursor in screenshots.
+
+        .. versionadded:: 8.0.0
+            ``compression_level``, ``display``, ``max_displays``, and ``with_cursor``, keyword arguments.
+
     .. method:: close()
 
-        Clean-up method. Does nothing by default.
+        Clean-up method.
 
         .. versionadded:: 4.0.0
 
diff --git a/mss/__main__.py b/mss/__main__.py
index 8e071a3..1aa41ad 100644
--- a/mss/__main__.py
+++ b/mss/__main__.py
@@ -31,12 +31,8 @@ def main(args: Optional[List[str]] = None) -> int:
         choices=list(range(10)),
         help="the PNG compression level",
     )
-    cli_args.add_argument(
-        "-m", "--monitor", default=0, type=int, help="the monitor to screen shot"
-    )
-    cli_args.add_argument(
-        "-o", "--output", default="monitor-{mon}.png", help="the output file name"
-    )
+    cli_args.add_argument("-m", "--monitor", default=0, type=int, help="the monitor to screen shot")
+    cli_args.add_argument("-o", "--output", default="monitor-{mon}.png", help="the output file name")
     cli_args.add_argument(
         "-q",
         "--quiet",
diff --git a/mss/base.py b/mss/base.py
index 5246f54..3dd6a55 100644
--- a/mss/base.py
+++ b/mss/base.py
@@ -20,10 +20,17 @@ class MSSBase(metaclass=ABCMeta):
 
     __slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor"}
 
-    def __init__(self) -> None:
+    def __init__(
+        self,
+        compression_level: int = 6,
+        display: Optional[Union[bytes, str]] = None,  # Linux only
+        max_displays: int = 32,  # Mac only
+        with_cursor: bool = False,
+    ) -> None:
+        # pylint: disable=unused-argument
         self.cls_image: Type[ScreenShot] = ScreenShot
-        self.compression_level = 6
-        self.with_cursor = False
+        self.compression_level = compression_level
+        self.with_cursor = with_cursor
         self._monitors: Monitors = []
 
     def __enter__(self) -> "MSSBase":
@@ -227,10 +234,7 @@ class MSSBase(metaclass=ABCMeta):
                 else:
                     alpha = alpha / 255
                     for i in rgb:
-                        screen_data[spos + i] = int(
-                            cursor_data[cpos + i] * alpha
-                            + screen_data[spos + i] * (1 - alpha)
-                        )
+                        screen_data[spos + i] = int(cursor_data[cpos + i] * alpha + screen_data[spos + i] * (1 - alpha))
 
         return screenshot
 
diff --git a/mss/darwin.py b/mss/darwin.py
index 8354395..a4ba900 100644
--- a/mss/darwin.py
+++ b/mss/darwin.py
@@ -5,17 +5,7 @@ Source: https://github.com/BoboTiG/python-mss
 import ctypes
 import ctypes.util
 import sys
-from ctypes import (
-    POINTER,
-    Structure,
-    c_double,
-    c_float,
-    c_int32,
-    c_ubyte,
-    c_uint32,
-    c_uint64,
-    c_void_p,
-)
+from ctypes import POINTER, Structure, c_double, c_float, c_int32, c_ubyte, c_uint32, c_uint64, c_void_p
 from platform import mac_ver
 from typing import Any, Optional, Type, Union
 
@@ -104,12 +94,12 @@ class MSS(MSSBase):
 
     __slots__ = {"core", "max_displays"}
 
-    def __init__(self, **_: Any) -> None:
+    def __init__(self, **kwargs: Any) -> None:
         """macOS initialisations."""
 
-        super().__init__()
+        super().__init__(**kwargs)
 
-        self.max_displays = 32
+        self.max_displays = kwargs.get("max_displays", 32)
 
         self._init_library()
         self._set_cfunctions()
@@ -156,9 +146,7 @@ class MSS(MSSBase):
         # Each monitor
         display_count = c_uint32(0)
         active_displays = (c_uint32 * self.max_displays)()
-        core.CGGetActiveDisplayList(
-            self.max_displays, active_displays, ctypes.byref(display_count)
-        )
+        core.CGGetActiveDisplayList(self.max_displays, active_displays, ctypes.byref(display_count))
         rotations = {0.0: "normal", 90.0: "right", -90.0: "left"}
         for idx in range(display_count.value):
             display = active_displays[idx]
@@ -194,9 +182,7 @@ class MSS(MSSBase):
         # pylint: disable=too-many-locals
 
         core = self.core
-        rect = CGRect(
-            (monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])
-        )
+        rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"]))
 
         image_ref = core.CGWindowListCreateImage(rect, 1, 0, 0)
         if not image_ref:
diff --git a/mss/linux.py b/mss/linux.py
index 916efa8..4daa234 100644
--- a/mss/linux.py
+++ b/mss/linux.py
@@ -2,15 +2,13 @@
 This is part of the MSS Python's module.
 Source: https://github.com/BoboTiG/python-mss
 """
-import contextlib
-import ctypes
-import ctypes.util
 import os
-import threading
+from contextlib import suppress
 from ctypes import (
     CFUNCTYPE,
     POINTER,
     Structure,
+    byref,
     c_char_p,
     c_int,
     c_int32,
@@ -23,8 +21,12 @@ from ctypes import (
     c_ushort,
     c_void_p,
     cast,
+    cdll,
+    create_string_buffer,
 )
-from typing import Any, Dict, Optional, Tuple, Union
+from ctypes.util import find_library
+from threading import current_thread, local
+from typing import Any, Tuple
 
 from .base import MSSBase, lock
 from .exception import ScreenShotError
@@ -182,23 +184,20 @@ _ERROR = {}
 
 
 @CFUNCTYPE(c_int, POINTER(Display), POINTER(Event))
-def error_handler(display: Display, event: Event) -> int:
+def _error_handler(display: Display, event: Event) -> int:
     """Specifies the program's supplied error handler."""
-    x11 = ctypes.util.find_library("X11")
-    if not x11:
-        return 0
 
     # Get the specific error message
-    xlib = ctypes.cdll.LoadLibrary(x11)
-    get_error = getattr(xlib, "XGetErrorText")
+    xlib = cdll.LoadLibrary(find_library("X11"))  # type: ignore[arg-type]
+    get_error = xlib.XGetErrorText
     get_error.argtypes = [POINTER(Display), c_int, c_char_p, c_int]
     get_error.restype = c_void_p
 
     evt = event.contents
-    error = ctypes.create_string_buffer(1024)
+    error = create_string_buffer(1024)
     get_error(display, evt.error_code, error, len(error))
 
-    _ERROR[threading.current_thread()] = {
+    _ERROR[current_thread()] = {
         "error": error.value.decode("utf-8"),
         "error_code": evt.error_code,
         "minor_code": evt.minor_code,
@@ -210,14 +209,14 @@ def error_handler(display: Display, event: Event) -> int:
     return 0
 
 
-def validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]:
+def _validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]:
     """Validate the returned value of a Xlib or XRANDR function."""
 
-    current_thread = threading.current_thread()
-    if retval != 0 and current_thread not in _ERROR:
+    thread = current_thread()
+    if retval != 0 and thread not in _ERROR:
         return args
 
-    details = _ERROR.pop(current_thread, {})
+    details = _ERROR.pop(thread, {})
     raise ScreenShotError(f"{func.__name__}() failed", details=details)
 
 
@@ -231,6 +230,7 @@ def validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]:
 #
 # Note: keep it sorted by cfunction.
 CFUNCTIONS: CFunctions = {
+    "XCloseDisplay": ("xlib", [POINTER(Display)], c_void_p),
     "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)),
     "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p),
     "XFixesGetCursorImage": ("xfixes", [POINTER(Display)], POINTER(XFixesCursorImage)),
@@ -292,25 +292,19 @@ class MSS(MSSBase):
     It uses intensively the Xlib and its Xrandr extension.
     """
 
-    __slots__ = {"drawable", "root", "xlib", "xrandr", "xfixes", "__with_cursor"}
+    __slots__ = {"xlib", "xrandr", "xfixes", "_handles"}
 
-    # A dict to maintain *display* values created by multiple threads.
-    _display_dict: Dict[threading.Thread, int] = {}
-
-    def __init__(
-        self, display: Optional[Union[bytes, str]] = None, with_cursor: bool = False
-    ) -> None:
+    def __init__(self, **kwargs: Any) -> None:
         """GNU/Linux initialisations."""
 
-        super().__init__()
-        self.with_cursor = with_cursor
+        super().__init__(**kwargs)
 
+        display = kwargs.get("display", b"")
         if not display:
             try:
                 display = os.environ["DISPLAY"].encode("utf-8")
             except KeyError:
-                # pylint: disable=raise-missing-from
-                raise ScreenShotError("$DISPLAY not set.")
+                raise ScreenShotError("$DISPLAY not set.") from None
 
         if not isinstance(display, bytes):
             display = display.encode("utf-8")
@@ -318,40 +312,50 @@ class MSS(MSSBase):
         if b":" not in display:
             raise ScreenShotError(f"Bad display value: {display!r}.")
 
-        x11 = ctypes.util.find_library("X11")
+        x11 = find_library("X11")
         if not x11:
             raise ScreenShotError("No X11 library found.")
-        self.xlib = ctypes.cdll.LoadLibrary(x11)
+        self.xlib = cdll.LoadLibrary(x11)
 
         # Install the error handler to prevent interpreter crashes:
         # any error will raise a ScreenShotError exception.
-        self.xlib.XSetErrorHandler(error_handler)
+        self.xlib.XSetErrorHandler(_error_handler)
 
-        xrandr = ctypes.util.find_library("Xrandr")
+        xrandr = find_library("Xrandr")
         if not xrandr:
             raise ScreenShotError("No Xrandr extension found.")
-        self.xrandr = ctypes.cdll.LoadLibrary(xrandr)
+        self.xrandr = cdll.LoadLibrary(xrandr)
 
         if self.with_cursor:
-            xfixes = ctypes.util.find_library("Xfixes")
+            xfixes = find_library("Xfixes")
             if xfixes:
-                self.xfixes = ctypes.cdll.LoadLibrary(xfixes)
+                self.xfixes = cdll.LoadLibrary(xfixes)
             else:
                 self.with_cursor = False
 
         self._set_cfunctions()
 
-        self.root = self.xlib.XDefaultRootWindow(self._get_display(display))
+        self._handles = local()
+        self._handles.display = self.xlib.XOpenDisplay(display)
 
-        if not self.has_extension("RANDR"):
-            raise ScreenShotError("No Xrandr extension found.")
+        if not self._is_extension_enabled("RANDR"):
+            raise ScreenShotError("Xrandr not enabled.")
+
+        self._handles.root = self.xlib.XDefaultRootWindow(self._handles.display)
 
         # Fix for XRRGetScreenResources and XGetImage:
         #     expected LP_Display instance instead of LP_XWindowAttributes
-        self.drawable = ctypes.cast(self.root, POINTER(Display))
+        self._handles.drawable = cast(self._handles.root, POINTER(Display))
+
+    def close(self) -> None:
+        if self._handles.display is not None:
+            self.xlib.XCloseDisplay(self._handles.display)
+            self._handles.display = None
 
-    def has_extension(self, extension: str) -> bool:
-        """Return True if the given *extension* is part of the extensions list of the server."""
+        _ERROR.clear()
+
+    def _is_extension_enabled(self, name: str) -> bool:
+        """Return True if the given *extension* is enabled on the server."""
         with lock:
             major_opcode_return = c_int()
             first_event_return = c_int()
@@ -359,34 +363,16 @@ class MSS(MSSBase):
 
             try:
                 self.xlib.XQueryExtension(
-                    self._get_display(),
-                    extension.encode("latin1"),
-                    ctypes.byref(major_opcode_return),
-                    ctypes.byref(first_event_return),
-                    ctypes.byref(first_error_return),
+                    self._handles.display,
+                    name.encode("latin1"),
+                    byref(major_opcode_return),
+                    byref(first_event_return),
+                    byref(first_error_return),
                 )
             except ScreenShotError:
                 return False
             return True
 
-    def _get_display(self, disp: Optional[bytes] = None) -> int:
-        """
-        Retrieve a thread-safe display from XOpenDisplay().
-        In multithreading, if the thread that creates *display* is dead, *display* will
-        no longer be valid to grab the screen. The *display* attribute is replaced
-        with *_display_dict* to maintain the *display* values in multithreading.
-        Since the current thread and main thread are always alive, reuse their
-        *display* value first.
-        """
-        current_thread = threading.current_thread()
-        current_display = MSS._display_dict.get(current_thread)
-        if current_display:
-            display = current_display
-        else:
-            display = self.xlib.XOpenDisplay(disp)
-            MSS._display_dict[current_thread] = display
-        return display
-
     def _set_cfunctions(self) -> None:
         """Set all ctypes functions and attach them to attributes."""
 
@@ -397,10 +383,10 @@ class MSS(MSSBase):
             "xfixes": getattr(self, "xfixes", None),
         }
         for func, (attr, argtypes, restype) in CFUNCTIONS.items():
-            with contextlib.suppress(AttributeError):
+            with suppress(AttributeError):
                 cfactory(
                     attr=attrs[attr],
-                    errcheck=validate,
+                    errcheck=_validate,
                     func=func,
                     argtypes=argtypes,
                     restype=restype,
@@ -409,20 +395,15 @@ class MSS(MSSBase):
     def _monitors_impl(self) -> None:
         """Get positions of monitors. It will populate self._monitors."""
 
-        display = self._get_display()
+        display = self._handles.display
         int_ = int
         xrandr = self.xrandr
 
         # All monitors
         gwa = XWindowAttributes()
-        self.xlib.XGetWindowAttributes(display, self.root, ctypes.byref(gwa))
+        self.xlib.XGetWindowAttributes(display, self._handles.root, byref(gwa))
         self._monitors.append(
-            {
-                "left": int_(gwa.x),
-                "top": int_(gwa.y),
-                "width": int_(gwa.width),
-                "height": int_(gwa.height),
-            }
+            {"left": int_(gwa.x), "top": int_(gwa.y), "width": int_(gwa.width), "height": int_(gwa.height)}
         )
 
         # Each monitor
@@ -431,9 +412,9 @@ class MSS(MSSBase):
         # XRRGetScreenResourcesCurrent(): 0.0039125580078689 s
         # The second is faster by a factor of 44! So try to use it first.
         try:
-            mon = xrandr.XRRGetScreenResourcesCurrent(display, self.drawable).contents
+            mon = xrandr.XRRGetScreenResourcesCurrent(display, self._handles.drawable).contents
         except AttributeError:
-            mon = xrandr.XRRGetScreenResources(display, self.drawable).contents
+            mon = xrandr.XRRGetScreenResources(display, self._handles.drawable).contents
 
         crtcs = mon.crtcs
         for idx in range(mon.ncrtc):
@@ -457,8 +438,8 @@ class MSS(MSSBase):
         """Retrieve all pixels from a monitor. Pixels have to be RGB."""
 
         ximage = self.xlib.XGetImage(
-            self._get_display(),
-            self.drawable,
+            self._handles.display,
+            self._handles.drawable,
             monitor["left"],
             monitor["top"],
             monitor["width"],
@@ -470,11 +451,9 @@ class MSS(MSSBase):
         try:
             bits_per_pixel = ximage.contents.bits_per_pixel
             if bits_per_pixel != 32:
-                raise ScreenShotError(
-                    f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}."
-                )
+                raise ScreenShotError(f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}.")
 
-            raw_data = ctypes.cast(
+            raw_data = cast(
                 ximage.contents.data,
                 POINTER(c_ubyte * monitor["height"] * monitor["width"] * 4),
             )
@@ -489,21 +468,19 @@ class MSS(MSSBase):
         """Retrieve all cursor data. Pixels have to be RGB."""
 
         # Read data of cursor/mouse-pointer
-        cursor_data = self.xfixes.XFixesGetCursorImage(self._get_display())
-        if not (cursor_data and cursor_data.contents):
+        ximage = self.xfixes.XFixesGetCursorImage(self._handles.display)
+        if not (ximage and ximage.contents):
             raise ScreenShotError("Cannot read XFixesGetCursorImage()")
 
-        ximage: XFixesCursorImage = cursor_data.contents
+        cursor_img: XFixesCursorImage = ximage.contents
         monitor = {
-            "left": ximage.x - ximage.xhot,
-            "top": ximage.y - ximage.yhot,
-            "width": ximage.width,
-            "height": ximage.height,
+            "left": cursor_img.x - cursor_img.xhot,
+            "top": cursor_img.y - cursor_img.yhot,
+            "width": cursor_img.width,
+            "height": cursor_img.height,
         }
 
-        raw_data = cast(
-            ximage.pixels, POINTER(c_ulong * monitor["height"] * monitor["width"])
-        )
+        raw_data = cast(cursor_img.pixels, POINTER(c_ulong * monitor["height"] * monitor["width"]))
         raw = bytearray(raw_data.contents)
 
         data = bytearray(monitor["height"] * monitor["width"] * 4)
diff --git a/mss/screenshot.py b/mss/screenshot.py
index 71ebc2c..94bedfc 100644
--- a/mss/screenshot.py
+++ b/mss/screenshot.py
@@ -21,9 +21,7 @@ class ScreenShot:
 
     __slots__ = {"__pixels", "__rgb", "pos", "raw", "size"}
 
-    def __init__(
-        self, data: bytearray, monitor: Monitor, size: Optional[Size] = None
-    ) -> None:
+    def __init__(self, data: bytearray, monitor: Monitor, size: Optional[Size] = None) -> None:
         self.__pixels: Optional[Pixels] = None
         self.__rgb: Optional[bytes] = None
 
@@ -57,9 +55,7 @@ class ScreenShot:
         }
 
     @classmethod
-    def from_size(
-        cls: Type["ScreenShot"], data: bytearray, width: int, height: int
-    ) -> "ScreenShot":
+    def from_size(cls: Type["ScreenShot"], data: bytearray, width: int, height: int) -> "ScreenShot":
         """Instantiate a new class given only screen shot's data and size."""
         monitor = {"left": 0, "top": 0, "width": width, "height": height}
         return cls(data, monitor)
@@ -86,9 +82,7 @@ class ScreenShot:
         """
 
         if not self.__pixels:
-            rgb_tuples: Iterator[Pixel] = zip(
-                self.raw[2::4], self.raw[1::4], self.raw[::4]
-            )
+            rgb_tuples: Iterator[Pixel] = zip(self.raw[2::4], self.raw[1::4], self.raw[::4])
             self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width))  # type: ignore
 
         return self.__pixels
@@ -134,6 +128,4 @@ class ScreenShot:
             return self.pixels[coord_y][coord_x]  # type: ignore
         except IndexError:
             # pylint: disable=raise-missing-from
-            raise ScreenShotError(
-                f"Pixel location ({coord_x}, {coord_y}) is out of range."
-            )
+            raise ScreenShotError(f"Pixel location ({coord_x}, {coord_y}) is out of range.")
diff --git a/mss/tools.py b/mss/tools.py
index 47fd74e..ffe1f3d 100644
--- a/mss/tools.py
+++ b/mss/tools.py
@@ -9,9 +9,7 @@ import zlib
 from typing import Optional, Tuple
 
 
-def to_png(
-    data: bytes, size: Tuple[int, int], level: int = 6, output: Optional[str] = None
-) -> Optional[bytes]:
+def to_png(data: bytes, size: Tuple[int, int], level: int = 6, output: Optional[str] = None) -> Optional[bytes]:
     """
     Dump data to a PNG file.  If `output` is `None`, create no file but return
     the whole PNG data.
@@ -29,9 +27,7 @@ def to_png(
     width, height = size
     line = width * 3
     png_filter = pack(">B", 0)
-    scanlines = b"".join(
-        [png_filter + data[y * line : y * line + line] for y in range(height)]
-    )
+    scanlines = b"".join([png_filter + data[y * line : y * line + line] for y in range(height)])
 
     magic = pack(">8B", 137, 80, 78, 71, 13, 10, 26, 10)
 
diff --git a/mss/windows.py b/mss/windows.py
index 04f26e6..0172a05 100644
--- a/mss/windows.py
+++ b/mss/windows.py
@@ -104,10 +104,10 @@ class MSS(MSSBase):
     # A dict to maintain *srcdc* values created by multiple threads.
     _srcdc_dict: Dict[threading.Thread, int] = {}
 
-    def __init__(self, **_: Any) -> None:
+    def __init__(self, **kwargs: Any) -> None:
         """Windows initialisations."""
 
-        super().__init__()
+        super().__init__(**kwargs)
 
         self.user32 = ctypes.WinDLL("user32")
         self.gdi32 = ctypes.WinDLL("gdi32")
@@ -170,9 +170,7 @@ class MSS(MSSBase):
         Since the current thread and main thread are always alive, reuse their *srcdc* value first.
         """
         cur_thread, main_thread = threading.current_thread(), threading.main_thread()
-        current_srcdc = MSS._srcdc_dict.get(cur_thread) or MSS._srcdc_dict.get(
-            main_thread
-        )
+        current_srcdc = MSS._srcdc_dict.get(cur_thread) or MSS._srcdc_dict.get(main_thread)
         if current_srcdc:
             srcdc = current_srcdc
         else:
@@ -275,9 +273,7 @@ class MSS(MSSBase):
             monitor["top"],
             SRCCOPY | CAPTUREBLT,
         )
-        bits = self.gdi32.GetDIBits(
-            memdc, MSS.bmp, 0, height, self._data, self._bmi, DIB_RGB_COLORS
-        )
+        bits = self.gdi32.GetDIBits(memdc, MSS.bmp, 0, height, self._data, self._bmi, DIB_RGB_COLORS)
         if bits != height:
             raise ScreenShotError("gdi32.GetDIBits() failed.")
 
diff --git a/setup.cfg b/setup.cfg
index cace210..613a73e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -44,6 +44,10 @@ python_requires = >=3.7
 console_scripts =
     mss = mss.__main__:main
 
+[coverage:run]
+omit =
+    mss/tests/*
+
 [flake8]
 ignore =
     # E203 whitespace before ':', but E203 is not PEP 8 compliant
@@ -57,13 +61,12 @@ multi_line_output = 3
 include_trailing_comma = True
 force_grid_wrap = 0
 use_parentheses = True
-line_length = 88
+line_length = 120
 
 [tool:pytest]
 addopts =
     --showlocals
     --strict-markers
-    --failed-first
     -r fE
     -v
     --cov=mss


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ee20288..0869d70 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -79,12 +79,15 @@ jobs:
         run: |
           python -m pip install -U pip wheel 
           python -m pip install -r dev-requirements.txt
-      - name: Tests on GNU/Linux
+      - name: Install Xvfb
+        if: matrix.os.emoji == '🐧'
+        run: sudo apt install xvfb
+      - name: Tests (GNU/Linux)
         if: matrix.os.emoji == '🐧'
         run: |
           export DISPLAY=:99
           sudo Xvfb -ac ${DISPLAY} -screen 0 1280x1024x24 > /dev/null 2>&1 &
           python -m pytest
-      - name: Tests on other platforms
+      - name: Tests (macOS, Windows)
         if: matrix.os.emoji != '🐧'
         run: python -m pytest
diff --git a/mss/tests/conftest.py b/mss/tests/conftest.py
index fc6a346..979850d 100644
--- a/mss/tests/conftest.py
+++ b/mss/tests/conftest.py
@@ -4,10 +4,11 @@ Source: https://github.com/BoboTiG/python-mss
 """
 import glob
 import os
+from pathlib import Path
 
 import pytest
 
-import mss
+from mss import mss
 
 
 @pytest.fixture(autouse=True)
@@ -16,9 +17,7 @@ def no_warnings(recwarn):
 
     yield
 
-    warnings = [
-        "{w.filename}:{w.lineno} {w.message}".format(w=warning) for warning in recwarn
-    ]
+    warnings = ["{w.filename}:{w.lineno} {w.message}".format(w=warning) for warning in recwarn]
     for warning in warnings:
         print(warning)
     assert not warnings
@@ -41,33 +40,18 @@ def before_tests(request):
     request.addfinalizer(purge_files)
 
 
-@pytest.fixture(scope="module")
-def sct():
-    try:
-        # `display` kwarg is only for GNU/Linux
-        return mss.mss(display=os.getenv("DISPLAY"))
-    except TypeError:
-        return mss.mss()
-
-
 @pytest.fixture(scope="session")
-def is_travis():
-    return "TRAVIS" in os.environ
+def raw() -> bytes:
+    file = Path(__file__).parent / "res" / "monitor-1024x768.raw"
+    return file.read_bytes()
 
 
 @pytest.fixture(scope="session")
-def raw():
-    here = os.path.dirname(__file__)
-    file = os.path.join(here, "res", "monitor-1024x768.raw")
-    with open(file, "rb") as f:
-        yield f.read()
-
-
-@pytest.fixture(scope="module")
-def pixel_ratio(sct):
+def pixel_ratio() -> int:
     """Get the pixel, used to adapt test checks."""
     # Grab a 1x1 screenshot
     region = {"top": 0, "left": 0, "width": 1, "height": 1}
 
-    # On macOS with Retina display, the width will be 2 instead of 1
-    return sct.grab(region).size[0]
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        # On macOS with Retina display, the width will be 2 instead of 1
+        return sct.grab(region).size[0]
diff --git a/mss/tests/test_cls_image.py b/mss/tests/test_cls_image.py
index a3b198c..b531ba1 100644
--- a/mss/tests/test_cls_image.py
+++ b/mss/tests/test_cls_image.py
@@ -2,18 +2,22 @@
 This is part of the MSS Python's module.
 Source: https://github.com/BoboTiG/python-mss
 """
+import os
+
+from mss import mss
 
 
 class SimpleScreenShot:
-    def __init__(self, data, monitor, **kwargs):
+    def __init__(self, data, monitor, **_):
         self.raw = bytes(data)
         self.monitor = monitor
 
 
-def test_custom_cls_image(sct):
-    sct.cls_image = SimpleScreenShot
-    mon1 = sct.monitors[1]
-    image = sct.grab(mon1)
+def test_custom_cls_image():
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        sct.cls_image = SimpleScreenShot
+        mon1 = sct.monitors[1]
+        image = sct.grab(mon1)
     assert isinstance(image, SimpleScreenShot)
     assert isinstance(image.raw, bytes)
     assert isinstance(image.monitor, dict)
diff --git a/mss/tests/test_find_monitors.py b/mss/tests/test_find_monitors.py
index c5b1569..278dc1b 100644
--- a/mss/tests/test_find_monitors.py
+++ b/mss/tests/test_find_monitors.py
@@ -2,33 +2,36 @@
 This is part of the MSS Python's module.
 Source: https://github.com/BoboTiG/python-mss
 """
+import os
 
+from mss import mss
 
-def test_get_monitors(sct):
-    assert sct.monitors
 
+def test_get_monitors():
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        assert sct.monitors
 
-def test_keys_aio(sct):
-    all_monitors = sct.monitors[0]
+
+def test_keys_aio():
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        all_monitors = sct.monitors[0]
     assert "top" in all_monitors
     assert "left" in all_monitors
     assert "height" in all_monitors
     assert "width" in all_monitors
 
 
-def test_keys_monitor_1(sct):
-    mon1 = sct.monitors[1]
+def test_keys_monitor_1():
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        mon1 = sct.monitors[1]
     assert "top" in mon1
     assert "left" in mon1
     assert "height" in mon1
     assert "width" in mon1
 
 
-def test_dimensions(sct, is_travis):
-    mon = sct.monitors[1]
-    if is_travis:
-        assert mon["width"] == 1280
-        assert mon["height"] == 1240
-    else:
-        assert mon["width"] > 0
-        assert mon["height"] > 0
+def test_dimensions():
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        mon = sct.monitors[1]
+    assert mon["width"] > 0
+    assert mon["height"] > 0
diff --git a/mss/tests/test_get_pixels.py b/mss/tests/test_get_pixels.py
index 0abf4d9..b6a7b6e 100644
--- a/mss/tests/test_get_pixels.py
+++ b/mss/tests/test_get_pixels.py
@@ -2,23 +2,28 @@
 This is part of the MSS Python's module.
 Source: https://github.com/BoboTiG/python-mss
 """
+import os
+
 import pytest
 
+from mss import mss
 from mss.base import ScreenShot
 from mss.exception import ScreenShotError
 
 
-def test_grab_monitor(sct):
-    for mon in sct.monitors:
-        image = sct.grab(mon)
-        assert isinstance(image, ScreenShot)
-        assert isinstance(image.raw, bytearray)
-        assert isinstance(image.rgb, bytes)
+def test_grab_monitor():
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        for mon in sct.monitors:
+            image = sct.grab(mon)
+            assert isinstance(image, ScreenShot)
+            assert isinstance(image.raw, bytearray)
+            assert isinstance(image.rgb, bytes)
 
 
-def test_grab_part_of_screen(sct, pixel_ratio):
+def test_grab_part_of_screen(pixel_ratio):
     monitor = {"top": 160, "left": 160, "width": 160, "height": 160}
-    image = sct.grab(monitor)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        image = sct.grab(monitor)
     assert isinstance(image, ScreenShot)
     assert isinstance(image.raw, bytearray)
     assert isinstance(image.rgb, bytes)
@@ -28,9 +33,10 @@ def test_grab_part_of_screen(sct, pixel_ratio):
     assert image.height == 160 * pixel_ratio
 
 
-def test_grab_part_of_screen_rounded(sct, pixel_ratio):
+def test_grab_part_of_screen_rounded(pixel_ratio):
     monitor = {"top": 160, "left": 160, "width": 161, "height": 159}
-    image = sct.grab(monitor)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        image = sct.grab(monitor)
     assert isinstance(image, ScreenShot)
     assert isinstance(image.raw, bytearray)
     assert isinstance(image.rgb, bytes)
@@ -40,9 +46,10 @@ def test_grab_part_of_screen_rounded(sct, pixel_ratio):
     assert image.height == 159 * pixel_ratio
 
 
-def test_grab_individual_pixels(sct):
+def test_grab_individual_pixels():
     monitor = {"top": 160, "left": 160, "width": 222, "height": 42}
-    image = sct.grab(monitor)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        image = sct.grab(monitor)
     assert isinstance(image.pixel(0, 0), tuple)
     with pytest.raises(ScreenShotError):
         image.pixel(image.width + 1, 12)
diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py
index abd6cba..6a86ce5 100644
--- a/mss/tests/test_gnu_linux.py
+++ b/mss/tests/test_gnu_linux.py
@@ -3,8 +3,8 @@ This is part of the MSS Python's module.
 Source: https://github.com/BoboTiG/python-mss
 """
 import ctypes.util
-import os
 import platform
+from unittest.mock import Mock, patch
 
 import pytest
 
@@ -13,12 +13,24 @@ import mss.linux
 from mss.base import MSSBase
 from mss.exception import ScreenShotError
 
-if platform.system().lower() != "linux":
-    pytestmark = pytest.mark.skip
-
+xvfbwrapper = pytest.importorskip("xvfbwrapper")
 
 PYPY = platform.python_implementation() == "PyPy"
 
+WIDTH = 200
+HEIGHT = 200
+DEPTH = 24
+
+
+@pytest.fixture
+def display() -> str:
+    vdisplay = xvfbwrapper.Xvfb(width=WIDTH, height=HEIGHT, colordepth=DEPTH)
+    vdisplay.start()
+    try:
+        yield f":{vdisplay.new_display}"
+    finally:
+        vdisplay.stop()
+
 
 @pytest.mark.skipif(PYPY, reason="Failure on PyPy")
 def test_factory_systems(monkeypatch):
@@ -39,21 +51,20 @@ def test_factory_systems(monkeypatch):
     monkeypatch.setattr(platform, "system", lambda: "Darwin")
     with pytest.raises((ScreenShotError, ValueError)):
         # ValueError on macOS Big Sur
-        mss.mss()
+        with mss.mss():
+            pass
     monkeypatch.undo()
 
     # Windows
     monkeypatch.setattr(platform, "system", lambda: "wInDoWs")
     with pytest.raises(ImportError):
         # ImportError: cannot import name 'WINFUNCTYPE'
-        mss.mss()
-
+        with mss.mss():
+            pass
 
-def test_arg_display(monkeypatch):
-    import mss
 
+def test_arg_display(display: str, monkeypatch):
     # Good value
-    display = os.getenv("DISPLAY")
     with mss.mss(display=display):
         pass
 
@@ -71,22 +82,20 @@ def test_arg_display(monkeypatch):
 
 @pytest.mark.skipif(PYPY, reason="Failure on PyPy")
 def test_bad_display_structure(monkeypatch):
-    import mss.linux
-
     monkeypatch.setattr(mss.linux, "Display", lambda: None)
     with pytest.raises(TypeError):
         with mss.mss():
             pass
 
 
-def test_no_xlib_library(monkeypatch):
-    monkeypatch.setattr(ctypes.util, "find_library", lambda x: None)
-    with pytest.raises(ScreenShotError):
-        with mss.mss():
-            pass
+def test_no_xlib_library():
+    with patch("mss.linux.find_library", return_value=None):
+        with pytest.raises(ScreenShotError):
+            with mss.mss():
+                pass
 
 
-def test_no_xrandr_extension(monkeypatch):
+def test_no_xrandr_extension():
     x11 = ctypes.util.find_library("X11")
 
     def find_lib_mocked(lib):
@@ -100,15 +109,31 @@ def test_no_xrandr_extension(monkeypatch):
         return None if lib == "Xrandr" else x11
 
     # No `Xrandr` library
-    monkeypatch.setattr(ctypes.util, "find_library", find_lib_mocked)
+    with patch("mss.linux.find_library", find_lib_mocked):
+        with pytest.raises(ScreenShotError):
+            mss.mss()
+
+
+@patch("mss.linux.MSS._is_extension_enabled", new=Mock(return_value=False))
+def test_xrandr_extension_exists_but_is_not_enabled(display: str):
     with pytest.raises(ScreenShotError):
-        with mss.mss():
+        with mss.mss(display=display):
             pass
 
 
-def test_region_out_of_monitor_bounds():
-    display = os.getenv("DISPLAY")
-    monitor = {"left": -30, "top": 0, "width": 100, "height": 100}
+def test_unsupported_depth():
+    vdisplay = xvfbwrapper.Xvfb(width=WIDTH, height=HEIGHT, colordepth=8)
+    vdisplay.start()
+    try:
+        with pytest.raises(ScreenShotError):
+            with mss.mss(display=f":{vdisplay.new_display}") as sct:
+                sct.grab(sct.monitors[1])
+    finally:
+        vdisplay.stop()
+
+
+def test_region_out_of_monitor_bounds(display: str):
+    monitor = {"left": -30, "top": 0, "width": WIDTH, "height": HEIGHT}
 
     assert not mss.linux._ERROR
 
@@ -127,19 +152,65 @@ def test_region_out_of_monitor_bounds():
     assert not mss.linux._ERROR
 
 
-def test_has_extension():
-    display = os.getenv("DISPLAY")
+def test__is_extension_enabled_unknown_name(display: str):
+    with mss.mss(display=display) as sct:
+        assert not sct._is_extension_enabled("NOEXT")
+
+
+def test_missing_fast_function_for_monitor_details_retrieval(display: str):
+    with mss.mss(display=display) as sct:
+        assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent")
+        screenshot_with_fast_fn = sct.grab(sct.monitors[1])
+
+    assert set(screenshot_with_fast_fn.rgb) == {0}
+
+    with mss.mss(display=display) as sct:
+        assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent")
+        del sct.xrandr.XRRGetScreenResourcesCurrent
+        screenshot_with_slow_fn = sct.grab(sct.monitors[1])
+
+    assert set(screenshot_with_slow_fn.rgb) == {0}
+
+
+def test_with_cursor(display: str):
     with mss.mss(display=display) as sct:
-        assert sct.has_extension("RANDR")
-        assert not sct.has_extension("NOEXT")
+        assert not hasattr(sct, "xfixes")
+        assert not sct.with_cursor
+        screenshot_without_cursor = sct.grab(sct.monitors[1])
 
+    # 1 color: black
+    assert set(screenshot_without_cursor.rgb) == {0}
 
-def test_with_cursor():
-    display = os.getenv("DISPLAY")
     with mss.mss(display=display, with_cursor=True) as sct:
-        assert sct.xfixes
+        assert hasattr(sct, "xfixes")
         assert sct.with_cursor
-        sct.grab(sct.monitors[1])
+        screenshot_with_cursor = sct.grab(sct.monitors[1])
+
+    # 2 colors: black & white (default cursor is a white cross)
+    assert set(screenshot_with_cursor.rgb) == {0, 255}
+
+
+def test_with_cursor_but_not_xfixes_extension_found(display: str):
+    x11 = ctypes.util.find_library("X11")
+
+    def find_lib_mocked(lib):
+        """
+        Returns None to emulate no XRANDR library.
+        Returns the previous found X11 library else.
 
-    # Not really sure how to test the cursor presence ...
-    # Also need to test when the cursor it outside of the screenshot
+        It is a naive approach, but works for now.
+        """
+
+        return None if lib == "Xfixes" else x11
+
+    with patch("mss.linux.find_library", find_lib_mocked):
+        with mss.mss(display=display, with_cursor=True) as sct:
+            assert not hasattr(sct, "xfixes")
+            assert not sct.with_cursor
+
+
+def test_with_cursor_failure(display: str):
+    with mss.mss(display=display, with_cursor=True) as sct:
+        with patch.object(sct.xfixes, "XFixesGetCursorImage", return_value=None):
+            with pytest.raises(ScreenShotError):
+                sct.grab(sct.monitors[1])
diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py
index f278c97..dee34fc 100644
--- a/mss/tests/test_implementation.py
+++ b/mss/tests/test_implementation.py
@@ -8,8 +8,8 @@ import platform
 
 import pytest
 
-import mss
 import mss.tools
+from mss import mss
 from mss.base import MSSBase
 from mss.exception import ScreenShotError
 from mss.screenshot import ScreenShot
@@ -42,12 +42,13 @@ def test_incomplete_class(cls):
         cls()
 
 
-def test_bad_monitor(sct):
-    with pytest.raises(ScreenShotError):
-        sct.grab(sct.shot(mon=222))
+def test_bad_monitor():
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        with pytest.raises(ScreenShotError):
+            sct.shot(mon=222)
 
 
-def test_repr(sct, pixel_ratio):
+def test_repr(pixel_ratio):
     box = {"top": 0, "left": 0, "width": 10, "height": 10}
     expected_box = {
         "top": 0,
@@ -55,27 +56,28 @@ def test_repr(sct, pixel_ratio):
         "width": 10 * pixel_ratio,
         "height": 10 * pixel_ratio,
     }
-    img = sct.grab(box)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        img = sct.grab(box)
     ref = ScreenShot(bytearray(b"42"), expected_box)
     assert repr(img) == repr(ref)
 
 
 def test_factory(monkeypatch):
     # Current system
-    with mss.mss() as sct:
+    with mss() as sct:
         assert isinstance(sct, MSSBase)
 
     # Unknown
     monkeypatch.setattr(platform, "system", lambda: "Chuck Norris")
     with pytest.raises(ScreenShotError) as exc:
-        mss.mss()
+        mss()
     monkeypatch.undo()
 
     error = exc.value.args[0]
     assert error == "System 'chuck norris' not (yet?) implemented."
 
 
-def test_entry_point(capsys, sct):
+def test_entry_point(capsys):
     from datetime import datetime
 
     from mss.__main__ import main
@@ -98,11 +100,12 @@ def test_entry_point(capsys, sct):
     for opt in ("-o", "--out"):
         main([opt, fmt])
         out, _ = capsys.readouterr()
-        for monitor, line in zip(sct.monitors[1:], out.splitlines()):
-            filename = fmt.format(**monitor)
-            assert line.endswith(filename)
-            assert os.path.isfile(filename)
-            os.remove(filename)
+        with mss(display=os.getenv("DISPLAY")) as sct:
+            for monitor, line in zip(sct.monitors[1:], out.splitlines()):
+                filename = fmt.format(**monitor)
+                assert line.endswith(filename)
+                assert os.path.isfile(filename)
+                os.remove(filename)
 
     fmt = "sct_{mon}-{date:%Y-%m-%d}.png"
     for opt in ("-o", "--out"):
@@ -129,7 +132,7 @@ def test_entry_point(capsys, sct):
         assert out == "Coordinates syntax: top, left, width, height\n"
 
 
-def test_grab_with_tuple(sct, pixel_ratio):
+def test_grab_with_tuple(pixel_ratio):
     left = 100
     top = 100
     right = 500
@@ -137,39 +140,41 @@ def test_grab_with_tuple(sct, pixel_ratio):
     width = right - left  # 400px width
     height = lower - top  # 400px height
 
-    # PIL like
-    box = (left, top, right, lower)
-    im = sct.grab(box)
-    assert im.size == (width * pixel_ratio, height * pixel_ratio)
-
-    # MSS like
-    box2 = {"left": left, "top": top, "width": width, "height": height}
-    im2 = sct.grab(box2)
-    assert im.size == im2.size
-    assert im.pos == im2.pos
-    assert im.rgb == im2.rgb
-
-
-def test_grab_with_tuple_percents(sct, pixel_ratio):
-    monitor = sct.monitors[1]
-    left = monitor["left"] + monitor["width"] * 5 // 100  # 5% from the left
-    top = monitor["top"] + monitor["height"] * 5 // 100  # 5% from the top
-    right = left + 500  # 500px
-    lower = top + 500  # 500px
-    width = right - left
-    height = lower - top
-
-    # PIL like
-    box = (left, top, right, lower)
-    im = sct.grab(box)
-    assert im.size == (width * pixel_ratio, height * pixel_ratio)
-
-    # MSS like
-    box2 = {"left": left, "top": top, "width": width, "height": height}
-    im2 = sct.grab(box2)
-    assert im.size == im2.size
-    assert im.pos == im2.pos
-    assert im.rgb == im2.rgb
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        # PIL like
+        box = (left, top, right, lower)
+        im = sct.grab(box)
+        assert im.size == (width * pixel_ratio, height * pixel_ratio)
+
+        # MSS like
+        box2 = {"left": left, "top": top, "width": width, "height": height}
+        im2 = sct.grab(box2)
+        assert im.size == im2.size
+        assert im.pos == im2.pos
+        assert im.rgb == im2.rgb
+
+
+def test_grab_with_tuple_percents(pixel_ratio):
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        monitor = sct.monitors[1]
+        left = monitor["left"] + monitor["width"] * 5 // 100  # 5% from the left
+        top = monitor["top"] + monitor["height"] * 5 // 100  # 5% from the top
+        right = left + 500  # 500px
+        lower = top + 500  # 500px
+        width = right - left
+        height = lower - top
+
+        # PIL like
+        box = (left, top, right, lower)
+        im = sct.grab(box)
+        assert im.size == (width * pixel_ratio, height * pixel_ratio)
+
+        # MSS like
+        box2 = {"left": left, "top": top, "width": width, "height": height}
+        im2 = sct.grab(box2)
+        assert im.size == im2.size
+        assert im.pos == im2.pos
+        assert im.rgb == im2.rgb
 
 
 def test_thread_safety():
@@ -182,7 +187,7 @@ def test_thread_safety():
 
         start_time = time.time()
         while time.time() - start_time < 1:
-            with mss.mss() as sct:
+            with mss() as sct:
                 sct.grab(sct.monitors[1])
 
         check[threading.current_thread()] = True
diff --git a/mss/tests/test_leaks.py b/mss/tests/test_leaks.py
index ad8147c..6b48bcc 100644
--- a/mss/tests/test_leaks.py
+++ b/mss/tests/test_leaks.py
@@ -49,6 +49,7 @@ def monitor_func() -> Callable[[], int]:
 
 
 def bound_instance_without_cm():
+    # Will always leak for now
     sct = mss()
     sct.shot()
 
@@ -62,6 +63,7 @@ def bound_instance_without_cm_but_use_close():
 
 
 def unbound_instance_without_cm():
+    # Will always leak for now
     mss().shot()
 
 
@@ -90,16 +92,34 @@ def regression_issue_135():
         sct.grab(bounding_box_score)
 
 
+def regression_issue_210():
+    """Regression test for issue #210: multiple X servers."""
+    xvfbwrapper = pytest.importorskip("xvfbwrapper")
+
+    vdisplay = xvfbwrapper.Xvfb(width=1920, height=1080, colordepth=24)
+    vdisplay.start()
+    with mss():
+        pass
+    vdisplay.stop()
+
+    vdisplay = xvfbwrapper.Xvfb(width=1920, height=1080, colordepth=24)
+    vdisplay.start()
+    with mss():
+        pass
+    vdisplay.stop()
+
+
 @pytest.mark.skipif(OS == "darwin", reason="No possible leak on macOS.")
 @pytest.mark.parametrize(
     "func",
     (
-        bound_instance_without_cm,
+        # bound_instance_without_cm,
         bound_instance_without_cm_but_use_close,
-        unbound_instance_without_cm,
+        # unbound_instance_without_cm,
         with_context_manager,
         regression_issue_128,
         regression_issue_135,
+        regression_issue_210,
     ),
 )
 def test_resource_leaks(func, monitor_func):
diff --git a/mss/tests/test_save.py b/mss/tests/test_save.py
index 5c11494..6dfbc19 100644
--- a/mss/tests/test_save.py
+++ b/mss/tests/test_save.py
@@ -7,60 +7,68 @@ from datetime import datetime
 
 import pytest
 
+from mss import mss
 
-def test_at_least_2_monitors(sct):
-    assert list(sct.save(mon=0))
 
+def test_at_least_2_monitors():
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        assert list(sct.save(mon=0))
 
-def test_files_exist(sct):
-    for filename in sct.save():
-        assert os.path.isfile(filename)
 
-    assert os.path.isfile(sct.shot())
+def test_files_exist():
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        for filename in sct.save():
+            assert os.path.isfile(filename)
+
+        assert os.path.isfile(sct.shot())
 
-    sct.shot(mon=-1, output="fullscreen.png")
-    assert os.path.isfile("fullscreen.png")
+        sct.shot(mon=-1, output="fullscreen.png")
+        assert os.path.isfile("fullscreen.png")
 
 
-def test_callback(sct):
+def test_callback():
     def on_exists(fname):
         if os.path.isfile(fname):
             new_file = f"{fname}.old"
             os.rename(fname, new_file)
 
-    filename = sct.shot(mon=0, output="mon0.png", callback=on_exists)
-    assert os.path.isfile(filename)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        filename = sct.shot(mon=0, output="mon0.png", callback=on_exists)
+        assert os.path.isfile(filename)
 
-    filename = sct.shot(output="mon1.png", callback=on_exists)
-    assert os.path.isfile(filename)
+        filename = sct.shot(output="mon1.png", callback=on_exists)
+        assert os.path.isfile(filename)
 
 
-def test_output_format_simple(sct):
-    filename = sct.shot(mon=1, output="mon-{mon}.png")
+def test_output_format_simple():
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        filename = sct.shot(mon=1, output="mon-{mon}.png")
     assert filename == "mon-1.png"
     assert os.path.isfile(filename)
 
 
-def test_output_format_positions_and_sizes(sct):
+def test_output_format_positions_and_sizes():
     fmt = "sct-{top}x{left}_{width}x{height}.png"
-    filename = sct.shot(mon=1, output=fmt)
-    assert filename == fmt.format(**sct.monitors[1])
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        filename = sct.shot(mon=1, output=fmt)
+        assert filename == fmt.format(**sct.monitors[1])
     assert os.path.isfile(filename)
 
 
-def test_output_format_date_simple(sct):
+def test_output_format_date_simple():
     fmt = "sct_{mon}-{date}.png"
-    try:
-        filename = sct.shot(mon=1, output=fmt)
-    except IOError:
-        # [Errno 22] invalid mode ('wb') or filename: 'sct_1-2019-01-01 21:20:43.114194.png'
-        pytest.mark.xfail("Default date format contains ':' which is not allowed.")
-    else:
-        assert os.path.isfile(filename)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        try:
+            filename = sct.shot(mon=1, output=fmt)
+            assert os.path.isfile(filename)
+        except IOError:
+            # [Errno 22] invalid mode ('wb') or filename: 'sct_1-2019-01-01 21:20:43.114194.png'
+            pytest.mark.xfail("Default date format contains ':' which is not allowed.")
 
 
-def test_output_format_date_custom(sct):
+def test_output_format_date_custom():
     fmt = "sct_{date:%Y-%m-%d}.png"
-    filename = sct.shot(mon=1, output=fmt)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        filename = sct.shot(mon=1, output=fmt)
     assert filename == fmt.format(date=datetime.now())
     assert os.path.isfile(filename)
diff --git a/mss/tests/test_third_party.py b/mss/tests/test_third_party.py
index 1c2551f..e89afbd 100644
--- a/mss/tests/test_third_party.py
+++ b/mss/tests/test_third_party.py
@@ -8,6 +8,8 @@ import os.path
 
 import pytest
 
+from mss import mss
+
 try:
     import numpy
 except (ImportError, RuntimeError):
@@ -21,17 +23,19 @@ except ImportError:
 
 
 @pytest.mark.skipif(numpy is None, reason="Numpy module not available.")
-def test_numpy(sct, pixel_ratio):
+def test_numpy(pixel_ratio):
     box = {"top": 0, "left": 0, "width": 10, "height": 10}
-    img = numpy.array(sct.grab(box))
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        img = numpy.array(sct.grab(box))
     assert len(img) == 10 * pixel_ratio
 
 
 @pytest.mark.skipif(Image is None, reason="PIL module not available.")
-def test_pil(sct):
+def test_pil():
     width, height = 16, 16
     box = {"top": 0, "left": 0, "width": width, "height": height}
-    sct_img = sct.grab(box)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        sct_img = sct.grab(box)
 
     img = Image.frombytes("RGB", sct_img.size, sct_img.rgb)
     assert img.mode == "RGB"
@@ -45,10 +49,11 @@ def test_pil(sct):
 
 
 @pytest.mark.skipif(Image is None, reason="PIL module not available.")
-def test_pil_bgra(sct):
+def test_pil_bgra():
     width, height = 16, 16
     box = {"top": 0, "left": 0, "width": width, "height": height}
-    sct_img = sct.grab(box)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        sct_img = sct.grab(box)
 
     img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
     assert img.mode == "RGB"
@@ -62,10 +67,11 @@ def test_pil_bgra(sct):
 
 
 @pytest.mark.skipif(Image is None, reason="PIL module not available.")
-def test_pil_not_16_rounded(sct):
+def test_pil_not_16_rounded():
     width, height = 10, 10
     box = {"top": 0, "left": 0, "width": width, "height": height}
-    sct_img = sct.grab(box)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        sct_img = sct.grab(box)
 
     img = Image.frombytes("RGB", sct_img.size, sct_img.rgb)
     assert img.mode == "RGB"
diff --git a/mss/tests/test_tools.py b/mss/tests/test_tools.py
index d1fa286..d939cb1 100644
--- a/mss/tests/test_tools.py
+++ b/mss/tests/test_tools.py
@@ -8,6 +8,7 @@ import zlib
 
 import pytest
 
+from mss import mss
 from mss.tools import to_png
 
 WIDTH = 10
@@ -15,20 +16,19 @@ HEIGHT = 10
 MD5SUM = "055e615b74167c9bdfea16a00539450c"
 
 
-def test_bad_compression_level(sct):
-    sct.compression_level = 42
-    try:
+def test_bad_compression_level():
+    with mss(compression_level=42, display=os.getenv("DISPLAY")) as sct:
         with pytest.raises(zlib.error):
             sct.shot()
-    finally:
-        sct.compression_level = 6
 
 
-def test_compression_level(sct):
+def test_compression_level():
     data = b"rgb" * WIDTH * HEIGHT
     output = f"{WIDTH}x{HEIGHT}.png"
 
-    to_png(data, (WIDTH, HEIGHT), level=sct.compression_level, output=output)
+    with mss(display=os.getenv("DISPLAY")) as sct:
+        to_png(data, (WIDTH, HEIGHT), level=sct.compression_level, output=output)
+
     with open(output, "rb") as png:
         assert hashlib.md5(png.read()).hexdigest() == MD5SUM