├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── pyproject.toml ├── pytest.ini ├── pytest_xvfb.py ├── setup.py ├── tests ├── conftest.py ├── test_qtwe_xio_error.py ├── test_xvfb.py └── test_xvfb_windows.py └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: [push, pull_request] 3 | env: 4 | FORCE_COLOR: "1" 5 | PY_COLORS: "1" 6 | 7 | jobs: 8 | tests: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 13 | ubuntu-version: [ubuntu-latest, ubuntu-24.04] 14 | runs-on: ${{ matrix.ubuntu-version }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - run: | 21 | sudo apt-get update 22 | sudo apt-get install --no-install-recommends libyaml-dev libegl1 libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 xserver-xephyr xvfb 23 | - run: pip install tox 24 | - run: tox -e py 25 | 26 | windows: 27 | runs-on: windows-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: actions/setup-python@v5 31 | with: 32 | python-version: '3.x' 33 | - run: pip install tox 34 | - run: tox -e py -- tests/test_xvfb_windows.py 35 | 36 | lint: 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: 40 | env: [format, mypy] 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: actions/setup-python@v5 44 | with: 45 | python-version: '3.x' 46 | - run: pip install tox 47 | - run: tox -e ${{ matrix.env }} 48 | 49 | deploy: 50 | runs-on: ubuntu-latest 51 | needs: [tests, windows, lint] 52 | environment: pypi 53 | permissions: 54 | id-token: write 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: actions/setup-python@v5 58 | with: 59 | python-version: '3.x' 60 | - name: Build package 61 | run: | 62 | python -m pip install --upgrade pip setuptools 63 | pip install build 64 | python -m build 65 | - name: Publish package to PyPI 66 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 67 | uses: pypa/gh-action-pypi-publish@release/v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache 2 | /*.egg-info 3 | /__pycache__ 4 | /.tox 5 | /dist 6 | /htmlcov 7 | /coverage.xml 8 | /.coverage 9 | *.pyc 10 | .mypy_cache 11 | /.venv 12 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | pytest-xvfb changelog 2 | ===================== 3 | 4 | v3.1.1 5 | ------ 6 | 7 | - Same as v3.1.0, but with proper version number and fully released. 8 | 9 | v3.1.0 10 | ------ 11 | 12 | - Support for Python 3.7 and 3.8 is now dropped, while official support for 13 | 3.12 and 3.13 was added (with no code changes required). 14 | - ``pytest.ini`` (required to run self-tests) is now included in sdists (#39). 15 | - New ``pytest_xvfb_disable`` hook to dynamically disable pytest-xvfb. 16 | 17 | v3.0.0 18 | ------ 19 | 20 | - New ``--xvfb-backend`` argument, which can be used to run Xephyr or Xvnc in 21 | place of Xvfb (e.g. for visual inspection but on a remote system or a 22 | consistent screen size needed). 23 | - Support for Python 3.5 and 3.6 is now dropped, while official support for 3.9, 24 | 3.10 and 3.11 was added (with no code changes required). 25 | - The ``Xvfb`` instance is now no longer saved in pytest's ``config`` object as 26 | ``config.xvfb`` anymore, and only available via the ``xvfb`` fixture. 27 | - Xvfb is now shut down as late as possible (via an ``atexit`` hook registered 28 | at import time), seemingly avoiding errors such as 29 | "XIO: fatal IO error 0 (Success)". 30 | - Code reformatting using black/shed. 31 | - Packaging refresh using ``pyproject.toml``. 32 | 33 | v2.0.0 34 | ------ 35 | 36 | - PyVirtualDisplay 1.3 and newer is now supported, support for older versions 37 | was dropped. 38 | - Support for Python 2.7, 3.3 and 3.4 is now dropped. 39 | - Support for Python 3.6, 3.7 and 3.8 was added (no code changes required). 40 | - Xvfb is now not started anymore in the xdist master process. 41 | 42 | v1.2.0 43 | ------ 44 | 45 | - ``Item.get_closest_marker`` is now used, which restores compatibility with 46 | pytest 4.1.0 and requires pytest 3.6.0 or newer. 47 | 48 | v1.1.0 49 | ------ 50 | 51 | - The ``xvfb_args`` option is now a single line parsed with ``shlex.split``. 52 | - The ``XvfbExitedError`` exception now includes stdout and stderr. 53 | 54 | v1.0.0 55 | ------ 56 | 57 | - Use `PyVirtualDisplay`_ to start/stop Xvfb 58 | - Show a warning on Linux if Xvfb is unavailable 59 | 60 | .. _PyVirtualDisplay: https://pypi.python.org/pypi/PyVirtualDisplay 61 | 62 | v0.3.0 63 | ------ 64 | 65 | - Add a new ``xvfb_xauth`` setting which creates an ``XAUTHORITY`` file. 66 | 67 | v0.2.1 68 | ------ 69 | 70 | - The temporary directory searched for logfiles is now hardcoded to /tmp 71 | as that's what X11 does as well. 72 | 73 | v0.2.0 74 | ------ 75 | 76 | - The ``no_xvfb``-marker is now registered automatically so pytest doesn't fail 77 | when run with ``--strict``. 78 | - The ``xvfb`` fixture is now session-scoped. 79 | 80 | v0.1.0 81 | ------ 82 | 83 | - Initial release 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Florian Bruhin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.rst 2 | include LICENSE 3 | include pytest.ini 4 | recursive-include tests *.py 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pytest-xvfb 2 | =================================== 3 | 4 | A pytest plugin to run `Xvfb`_ (or `Xephyr`_/`Xvnc`_) for tests. 5 | 6 | ---- 7 | 8 | Installation 9 | ------------ 10 | 11 | You can install "`pytest-xvfb`_" via `pip`_ from `PyPI`_:: 12 | 13 | $ pip install pytest-xvfb 14 | 15 | 16 | Usage 17 | ----- 18 | 19 | With Xvfb and the plugin installed, your testsuite automatically runs with `Xvfb`_. This allows tests to be run without windows popping up during GUI tests or on systems without a display (like a CI). 20 | 21 | The plugin sees Xvfb being installed as "optional", since the tests can still 22 | run without it installed. If it's unavailable, it will show an informational 23 | message, if on Linux and a ``DISPLAY`` is available. When using 24 | ``--xvfb-backend xvfb``, this message will turn into a hard error instead. 25 | 26 | If you're currently using ``xvfb-run`` in something like a GitHub Actions YAML 27 | file simply remove the wrapper and install this plugin instead - then you'll 28 | also have the benefits of Xvfb locally. 29 | 30 | Features 31 | -------- 32 | 33 | You can pass ``--no-xvfb`` to explicitly turn off Xvfb (e.g. to visually 34 | inspect a failure). 35 | 36 | With ``--xvfb-backend xephyr`` or ``--xvfb-backend xvnc``, you can use Xephyr 37 | or Xvnc in place of Xvfb, e.g. to visually inspect failures. 38 | 39 | **NOTE:** Support for ``xvnc`` is currently experimental and not tested on CI, 40 | due to incompatibilities with PyVirtualDisplay and Ubuntu 22.04's tightvncserver. 41 | 42 | You can mark tests with ``@pytest.mark.no_xvfb`` to skip them when they're 43 | running with Xvfb. 44 | 45 | By implementing the `pytest_xvfb_disable(config: pytest.Config)` pytest hook, 46 | you can dynamically decide whether pytest-xvfb should be disabled (by returning 47 | `True` from such a hook). 48 | 49 | A ``xvfb`` fixture is available with the following attributes: 50 | 51 | - ``width``: The configured width of the screen. 52 | - ``height``: The configured height of the screen. 53 | - ``colordepth``: The configured colordepth of the screen. 54 | - ``args``: The arguments to be passed to Xvfb. 55 | - ``display``: The display number (as int) which is used. 56 | - ``backend``: Either ``None`` (Xvfb), ``"xvfb"``, ``"xephyr"``, or ``"xvnc"``. 57 | 58 | In a pytest.ini, ``xvfb_width``, ``xvfb_height``, ``xvfb_colordepth`` and 59 | ``xvfb_args`` can be used to configure the respective values. In addition, 60 | ``xvfb_xauth`` can be set to ``true`` to generate an ``Xauthority`` token. 61 | 62 | Contributing 63 | ------------ 64 | 65 | Contributions are very welcome. Tests can be run with `tox`_, please ensure 66 | the coverage at least stays the same before you submit a pull request. 67 | 68 | License 69 | ------- 70 | 71 | Distributed under the terms of the `MIT`_ license, "pytest-xvfb" is free and open source software 72 | 73 | Thanks 74 | ------ 75 | 76 | This `pytest`_ plugin was generated with `Cookiecutter`_ along with 77 | `@hackebrot`_'s `Cookiecutter-pytest-plugin`_ template. 78 | 79 | Thanks to `@cgoldberg`_ for `xvfbwrapper`_ which was the inspiration for this 80 | project. 81 | 82 | Issues 83 | ------ 84 | 85 | If you encounter any problems, please `file an issue`_ along with a detailed description. 86 | 87 | .. _`pytest-xvfb`: https://pypi.python.org/pypi/pytest-xvfb/ 88 | .. _`Cookiecutter`: https://github.com/audreyr/cookiecutter 89 | .. _`@hackebrot`: https://github.com/hackebrot 90 | .. _`@cgoldberg`: https://github.com/cgoldberg 91 | .. _`xvfbwrapper`: https://github.com/cgoldberg/xvfbwrapper 92 | .. _`MIT`: http://opensource.org/licenses/MIT 93 | .. _`cookiecutter-pytest-plugin`: https://github.com/pytest-dev/cookiecutter-pytest-plugin 94 | .. _`file an issue`: https://github.com/The-Compiler/pytest-xvfb/issues 95 | .. _`pytest`: https://github.com/pytest-dev/pytest 96 | .. _`tox`: https://tox.readthedocs.org/en/latest/ 97 | .. _`pip`: https://pypi.python.org/pypi/pip/ 98 | .. _`PyPI`: https://pypi.python.org/pypi 99 | .. _`Xvfb`: https://en.wikipedia.org/wiki/Xvfb 100 | .. _`Xephyr`: https://www.freedesktop.org/wiki/Software/Xephyr/ 101 | .. _`Xvnc`: https://tigervnc.org/doc/Xvnc.html 102 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pytest-xvfb" 7 | version = "3.1.1" 8 | authors = [{name = "Florian Bruhin", email = "me@the-compiler.org"}] 9 | maintainers = [{name = "Florian Bruhin", email = "me@the-compiler.org"}] 10 | license = {text = "MIT"} 11 | description = "A pytest plugin to run Xvfb (or Xephyr/Xvnc) for tests." 12 | readme = "README.rst" 13 | classifiers = [ 14 | "Development Status :: 4 - Beta", 15 | "Intended Audience :: Developers", 16 | "Topic :: Software Development :: Testing", 17 | "Programming Language :: Python", 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Programming Language :: Python :: 3.13", 25 | "Programming Language :: Python :: Implementation :: CPython", 26 | "Programming Language :: Python :: Implementation :: PyPy", 27 | "Operating System :: OS Independent", 28 | "License :: OSI Approved :: MIT License", 29 | ] 30 | requires-python = ">=3.9" 31 | dependencies = ["pytest>=2.8.1", "pyvirtualdisplay>=1.3"] 32 | 33 | [project.urls] 34 | Homepage = "https://github.com/The-Compiler/pytest-xvfb" 35 | 36 | [project.entry-points] 37 | pytest11 = {xvfb = "pytest_xvfb"} 38 | 39 | [tool.setuptools] 40 | py-modules = ["pytest_xvfb"] 41 | include-package-data = false 42 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --runpytest=subprocess 3 | -------------------------------------------------------------------------------- /pytest_xvfb.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import atexit 4 | import os 5 | import os.path 6 | import sys 7 | 8 | import pytest 9 | import pyvirtualdisplay 10 | import pyvirtualdisplay.display 11 | 12 | xvfb_instance = None 13 | 14 | 15 | def shutdown_xvfb() -> None: 16 | if xvfb_instance is not None: 17 | xvfb_instance.stop() 18 | 19 | 20 | # This needs to be done as early as possible (before importing QtWebEngine for 21 | # example), so that Xvfb gets shut down as late as possible. 22 | atexit.register(shutdown_xvfb) 23 | 24 | 25 | def is_xdist_master(config: pytest.Config) -> bool: 26 | return config.getoption("dist", "no") != "no" and not os.environ.get( 27 | "PYTEST_XDIST_WORKER" 28 | ) 29 | 30 | 31 | def has_executable(name: str) -> bool: 32 | # http://stackoverflow.com/a/28909933/2085149 33 | return any( 34 | os.access(os.path.join(path, name), os.X_OK) 35 | for path in os.environ["PATH"].split(os.pathsep) 36 | ) 37 | 38 | 39 | class XvfbExitedError(Exception): 40 | pass 41 | 42 | 43 | class Hookspec: 44 | def pytest_xvfb_disable(self, config: pytest.Config) -> bool: # type: ignore[empty-body] 45 | """Return bool from this hook to disable pytest-xvfb.""" 46 | ... 47 | 48 | 49 | class Xvfb: 50 | def __init__(self, config: pytest.Config) -> None: 51 | self.width = int(config.getini("xvfb_width")) 52 | self.height = int(config.getini("xvfb_height")) 53 | self.colordepth = int(config.getini("xvfb_colordepth")) 54 | self.args = config.getini("xvfb_args") or [] 55 | self.xauth = config.getini("xvfb_xauth") 56 | self.backend = config.getoption("--xvfb-backend") 57 | self.display: int | None = None 58 | self._virtual_display: pyvirtualdisplay.display.Display | None = None 59 | 60 | def start(self) -> None: 61 | self._virtual_display = pyvirtualdisplay.display.Display( 62 | backend=self.backend, 63 | size=(self.width, self.height), 64 | color_depth=self.colordepth, 65 | use_xauth=self.xauth, 66 | extra_args=self.args, 67 | ) 68 | self._virtual_display.start() 69 | self.display = self._virtual_display.display 70 | assert self._virtual_display.is_alive() 71 | 72 | def stop(self) -> None: 73 | if self.display is not None: # starting worked 74 | assert self._virtual_display is not None # mypy 75 | self._virtual_display.stop() 76 | 77 | 78 | def pytest_addoption(parser: pytest.Parser) -> None: 79 | group = parser.getgroup("xvfb") 80 | group.addoption("--no-xvfb", action="store_true", help="Disable Xvfb for tests.") 81 | group.addoption( 82 | "--xvfb-backend", 83 | action="store", 84 | choices=["xvfb", "xvnc", "xephyr"], 85 | help="Use Xephyr or Xvnc instead of Xvfb for tests. Will be ignored if --no-xvfb is given.", 86 | ) 87 | 88 | parser.addini("xvfb_width", "Width of the Xvfb display", default="800") 89 | parser.addini("xvfb_height", "Height of the Xvfb display", default="600") 90 | parser.addini("xvfb_colordepth", "Color depth of the Xvfb display", default="16") 91 | parser.addini("xvfb_args", "Additional arguments for Xvfb", type="args") 92 | parser.addini( 93 | "xvfb_xauth", 94 | "Generate an Xauthority token for Xvfb. Needs xauth.", 95 | default=False, 96 | type="bool", 97 | ) 98 | 99 | 100 | def pytest_addhooks(pluginmanager: pytest.PytestPluginManager) -> None: 101 | pluginmanager.add_hookspecs(Hookspec) 102 | 103 | 104 | def pytest_configure(config: pytest.Config) -> None: 105 | global xvfb_instance 106 | 107 | no_xvfb = ( 108 | config.getoption("--no-xvfb") 109 | or is_xdist_master(config) 110 | or any(config.pluginmanager.hook.pytest_xvfb_disable(config=config)) 111 | ) 112 | 113 | backend = config.getoption("--xvfb-backend") 114 | 115 | if no_xvfb: 116 | pass 117 | elif backend is None and not has_executable("Xvfb"): 118 | # soft fail 119 | if sys.platform.startswith("linux") and "DISPLAY" in os.environ: 120 | print( 121 | "pytest-xvfb could not find Xvfb. " 122 | "You can install it to prevent windows from being shown." 123 | ) 124 | elif ( 125 | backend == "xvfb" 126 | and not has_executable("Xvfb") 127 | or backend == "xvnc" 128 | and not has_executable("Xvnc") 129 | or backend == "xephyr" 130 | and not has_executable("Xephyr") 131 | ): 132 | raise pytest.UsageError(f"xvfb backend {backend} requested but not installed.") 133 | else: 134 | xvfb_instance = Xvfb(config) 135 | xvfb_instance.start() 136 | 137 | config.addinivalue_line("markers", "no_xvfb: Skip test when using Xvfb") 138 | 139 | 140 | def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: 141 | for item in items: 142 | if item.get_closest_marker("no_xvfb") and xvfb_instance is not None: 143 | skipif_marker = pytest.mark.skipif(True, reason="Skipped with Xvfb") 144 | item.add_marker(skipif_marker) 145 | 146 | 147 | @pytest.fixture(scope="session") 148 | def xvfb() -> Xvfb | None: 149 | return xvfb_instance 150 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = "pytester" 2 | -------------------------------------------------------------------------------- /tests/test_qtwe_xio_error.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import sys 5 | 6 | import pytest 7 | 8 | if sys.version_info == (3, 12, 0, "beta", 1) and "CI" in os.environ: 9 | pytest.skip( 10 | reason="Segfaults on GHA for unknown reasons", 11 | allow_module_level=True, 12 | ) 13 | 14 | pytest.importorskip("PyQt5.QtWebEngineWidgets") 15 | 16 | 17 | def test_qt_output(pytester: pytest.Pytester) -> None: 18 | pytester.makepyfile( 19 | """ 20 | import sys 21 | import PyQt5.QtWebEngineWidgets 22 | from PyQt5.QtWidgets import QWidget, QApplication 23 | 24 | app = QApplication(sys.argv) 25 | 26 | def test_widget(): 27 | widget = QWidget() 28 | widget.show() 29 | """ 30 | ) 31 | res = pytester.runpytest() 32 | assert res.ret == 0 33 | -------------------------------------------------------------------------------- /tests/test_xvfb.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from typing import Iterator 5 | 6 | import pytest 7 | import pyvirtualdisplay 8 | 9 | import pytest_xvfb 10 | 11 | xauth_available = any( 12 | os.access(os.path.join(path, "xauth"), os.X_OK) 13 | for path in os.environ.get("PATH", "").split(os.pathsep) 14 | ) 15 | 16 | 17 | @pytest.fixture(autouse=True, scope="session") 18 | def ensure_xvfb() -> None: 19 | if not pytest_xvfb.has_executable("Xvfb"): 20 | raise Exception("Tests need Xvfb to run.") 21 | 22 | 23 | needs_xephyr = pytest.mark.skipif( 24 | not pytest_xvfb.has_executable("Xephyr"), reason="Needs Xephyr" 25 | ) 26 | needs_xvnc = pytest.mark.skipif( 27 | not pytest_xvfb.has_executable("Xvnc"), reason="Needs Xvnc" 28 | ) 29 | 30 | 31 | @pytest.fixture( 32 | params=[ 33 | None, 34 | "xvfb", 35 | pytest.param("xephyr", marks=needs_xephyr), 36 | pytest.param("xvnc", marks=needs_xvnc), 37 | ] 38 | ) 39 | def backend_args( 40 | request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch 41 | ) -> Iterator[list[str]]: 42 | monkeypatch.delenv("DISPLAY") 43 | args = [] if request.param is None else ["--xvfb-backend", request.param] 44 | if request.param == "xephyr": 45 | # we need a host display for it... PyVirtualDisplay and Xvfb to the 46 | # rescue! 47 | display = pyvirtualdisplay.Display() # type: ignore[attr-defined] 48 | display.start() 49 | yield args 50 | display.stop() 51 | else: 52 | yield args 53 | 54 | 55 | def test_xvfb_available(pytester: pytest.Pytester, backend_args: list[str]) -> None: 56 | pytester.makepyfile( 57 | """ 58 | import os 59 | 60 | def test_display(): 61 | assert 'DISPLAY' in os.environ 62 | """ 63 | ) 64 | result = pytester.runpytest(*backend_args) 65 | assert result.ret == 0 66 | 67 | 68 | def test_empty_display( 69 | pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, backend_args: list[str] 70 | ) -> None: 71 | if backend_args == ["--xvfb-backend", "xephyr"]: 72 | pytest.skip("Xephyr needs a host display") 73 | 74 | monkeypatch.setenv("DISPLAY", "") 75 | pytester.makepyfile( 76 | """ 77 | import os 78 | 79 | def test_display(): 80 | assert 'DISPLAY' in os.environ 81 | """ 82 | ) 83 | result = pytester.runpytest(*backend_args) 84 | assert os.environ["DISPLAY"] == "" 85 | assert result.ret == 0 86 | 87 | 88 | def test_xvfb_unavailable( 89 | pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch 90 | ) -> None: 91 | monkeypatch.setenv("PATH", "") 92 | monkeypatch.setenv("DISPLAY", ":42") 93 | pytester.makepyfile( 94 | """ 95 | import os 96 | 97 | def test_display(): 98 | assert os.environ['DISPLAY'] == ':42' 99 | """ 100 | ) 101 | assert os.environ["DISPLAY"] == ":42" 102 | result = pytester.runpytest() 103 | result.stdout.fnmatch_lines("* could not find Xvfb.*") 104 | assert result.ret == 0 105 | 106 | 107 | def test_xvfb_unavailable_explicit( 108 | pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, backend_args: list[str] 109 | ) -> None: 110 | """If an explicitly chosen backend is unavailable: Hard error.""" 111 | if not backend_args: 112 | pytest.skip("Already tested above") 113 | 114 | monkeypatch.setenv("PATH", "") 115 | monkeypatch.setenv("DISPLAY", ":42") 116 | pytester.makepyfile( 117 | """ 118 | import os 119 | 120 | def test_display(): 121 | assert False # never run 122 | """ 123 | ) 124 | assert os.environ["DISPLAY"] == ":42" 125 | result = pytester.runpytest(*backend_args) 126 | result.stderr.fnmatch_lines("*xvfb backend * requested but not installed.") 127 | assert result.ret == pytest.ExitCode.USAGE_ERROR 128 | 129 | 130 | def test_no_xvfb_arg( 131 | pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, backend_args: list[str] 132 | ) -> None: 133 | monkeypatch.setenv("DISPLAY", ":42") 134 | pytester.makepyfile( 135 | """ 136 | import os 137 | 138 | def test_display(): 139 | assert os.environ['DISPLAY'] == ':42' 140 | """ 141 | ) 142 | assert os.environ["DISPLAY"] == ":42" 143 | result = pytester.runpytest("--no-xvfb", *backend_args) 144 | assert result.ret == 0 145 | 146 | 147 | def test_disable_hook( 148 | pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, backend_args: list[str] 149 | ) -> None: 150 | monkeypatch.setenv("DISPLAY", ":42") 151 | pytester.makepyfile( 152 | conftest=""" 153 | def pytest_xvfb_disable(config): 154 | return True 155 | """ 156 | ) 157 | pytester.makepyfile( 158 | """ 159 | import os 160 | 161 | def test_display(): 162 | assert os.environ['DISPLAY'] == ':42' 163 | """ 164 | ) 165 | assert os.environ["DISPLAY"] == ":42" 166 | result = pytester.runpytest(*backend_args) 167 | assert result.ret == 0 168 | 169 | 170 | @pytest.mark.parametrize("configured", [True, False]) 171 | def test_screen_size( 172 | pytester: pytest.Pytester, configured: bool, backend_args: list[str] 173 | ) -> None: 174 | if backend_args == ["--xvfb-backend", "xvnc"]: 175 | pytest.skip("Seems to be unsupported with Xvnc") 176 | 177 | try: 178 | import tkinter # noqa 179 | except ImportError: 180 | pytest.importorskip("Tkinter") 181 | 182 | if configured: 183 | pytester.makeini( 184 | """ 185 | [pytest] 186 | xvfb_width = 1024 187 | xvfb_height = 768 188 | xvfb_colordepth = 8 189 | """ 190 | ) 191 | expected_width = 1024 192 | expected_height = 768 193 | expected_depth = 8 194 | else: 195 | expected_width = 800 196 | expected_height = 600 197 | expected_depth = 16 198 | 199 | pytester.makepyfile( 200 | """ 201 | try: 202 | import tkinter as tk 203 | except ImportError: 204 | import Tkinter as tk 205 | 206 | def test_screen_size(): 207 | root = tk.Tk() 208 | assert root.winfo_screenwidth() == {width} 209 | assert root.winfo_screenheight() == {height} 210 | assert root.winfo_screendepth() == {depth} 211 | """.format(width=expected_width, height=expected_height, depth=expected_depth) 212 | ) 213 | result = pytester.runpytest(*backend_args) 214 | assert result.ret == 0 215 | 216 | 217 | def test_failing_start( 218 | pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, backend_args: list[str] 219 | ) -> None: 220 | pytester.makeini( 221 | """ 222 | [pytest] 223 | xvfb_args = -foo 224 | """ 225 | ) 226 | pytester.makepyfile( 227 | """ 228 | def test_none(): 229 | pass 230 | """ 231 | ) 232 | result = pytester.runpytest(*backend_args) 233 | result.stderr.fnmatch_lines( 234 | [ 235 | "INTERNALERROR> *.XStartError: X* program closed. *", 236 | ] 237 | ) 238 | assert "OSError" not in str(result.stderr) 239 | 240 | 241 | @pytest.mark.parametrize( 242 | "args, outcome", 243 | [ 244 | ([], "1 passed, 1 skipped"), 245 | (["--no-xvfb"], "2 passed"), 246 | ], 247 | ) 248 | def test_no_xvfb_marker( 249 | pytester: pytest.Pytester, args: list[str], outcome: str, backend_args: list[str] 250 | ) -> None: 251 | pytester.makepyfile( 252 | """ 253 | import pytest 254 | 255 | @pytest.mark.no_xvfb 256 | def test_marked(): 257 | pass 258 | 259 | def test_unmarked(): 260 | pass 261 | """ 262 | ) 263 | res = pytester.runpytest(*args, *backend_args) 264 | res.stdout.fnmatch_lines(f"*= {outcome}*") 265 | 266 | 267 | def test_xvfb_fixture(pytester: pytest.Pytester, backend_args: list[str]) -> None: 268 | pytester.makepyfile( 269 | """ 270 | import os 271 | 272 | def test_display(xvfb): 273 | assert ':{}'.format(xvfb.display) == os.environ['DISPLAY'] 274 | 275 | def test_screen(xvfb): 276 | assert xvfb.width == 800 277 | assert xvfb.height == 600 278 | assert xvfb.colordepth == 16 279 | 280 | def test_args(xvfb): 281 | assert xvfb.args == [] 282 | """ 283 | ) 284 | result = pytester.runpytest(*backend_args) 285 | assert result.ret == 0 286 | 287 | 288 | def test_early_display( 289 | monkeypatch: pytest.MonkeyPatch, pytester: pytest.Pytester, backend_args: list[str] 290 | ) -> None: 291 | """Make sure DISPLAY is set in a session-scoped fixture already.""" 292 | pytester.makepyfile( 293 | """ 294 | import os 295 | import pytest 296 | 297 | @pytest.yield_fixture(scope='session', autouse=True) 298 | def fixt(): 299 | assert 'DISPLAY' in os.environ 300 | yield 301 | 302 | def test_foo(): 303 | pass 304 | """ 305 | ) 306 | result = pytester.runpytest(*backend_args) 307 | assert result.ret == 0 308 | 309 | 310 | def test_strict_markers(pytester: pytest.Pytester) -> None: 311 | pytester.makepyfile( 312 | """ 313 | import pytest 314 | 315 | @pytest.mark.no_xvfb 316 | def test_marked(): 317 | pass 318 | """ 319 | ) 320 | result = pytester.runpytest("--strict") 321 | assert result.ret == 0 322 | 323 | 324 | def test_xvfb_session_fixture(pytester: pytest.Pytester) -> None: 325 | """Make sure the xvfb fixture can be used from a session-wide one.""" 326 | pytester.makepyfile( 327 | """ 328 | import pytest 329 | 330 | @pytest.fixture(scope='session') 331 | def fixt(xvfb): 332 | pass 333 | 334 | def test_fixt(fixt): 335 | pass 336 | """ 337 | ) 338 | result = pytester.runpytest() 339 | assert result.ret == 0 340 | 341 | 342 | @pytest.mark.skipif(not xauth_available, reason="no xauth") 343 | def test_xvfb_with_xauth(pytester: pytest.Pytester, backend_args: list[str]) -> None: 344 | original_auth = os.environ.get("XAUTHORITY") 345 | pytester.makeini( 346 | """ 347 | [pytest] 348 | xvfb_xauth = True 349 | """ 350 | ) 351 | pytester.makepyfile( 352 | """ 353 | import os 354 | 355 | def test_xauth(): 356 | print('\\nXAUTHORITY: ' + os.environ['XAUTHORITY']) 357 | assert os.path.isfile(os.environ['XAUTHORITY']) 358 | assert os.access(os.environ['XAUTHORITY'], os.R_OK) 359 | """ 360 | ) 361 | result = pytester.runpytest("-s", *backend_args) 362 | # Get and parse the XAUTHORITY: line 363 | authline = next(line for line in result.outlines if line.startswith("XAUTHORITY:")) 364 | authfile = authline.split(" ", 1)[1] 365 | 366 | assert result.ret == 0 367 | # Make sure the authfile is deleted 368 | assert not os.path.exists(authfile) 369 | assert os.environ.get("XAUTHORITY") == original_auth 370 | -------------------------------------------------------------------------------- /tests/test_xvfb_windows.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | 6 | def test_xvfb_windows(pytester: pytest.Pytester) -> None: 7 | """Make sure things don't break on Windows with no Xvfb available.""" 8 | pytester.makepyfile( 9 | """ 10 | def test_nothing(): 11 | pass 12 | """ 13 | ) 14 | result = pytester.runpytest() 15 | assert result.ret == 0 16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # For more information about tox, see https://tox.readthedocs.org/en/latest/ 2 | [tox] 3 | envlist = py39,py310,py311,py312,py313,pypy3 4 | 5 | [testenv] 6 | # for test_qtwe_xio_error.py 7 | deps = PyQtWebEngine 8 | passenv = CI 9 | commands = pytest {posargs:tests} 10 | 11 | [testenv:ruff] 12 | deps = ruff 13 | commands = 14 | ruff check --fix . 15 | ruff format . 16 | 17 | [testenv:format] 18 | deps = ruff 19 | commands = 20 | ruff check . 21 | ruff format --diff . 22 | 23 | [testenv:mypy] 24 | deps = 25 | mypy 26 | types-setuptools 27 | commands = mypy --strict . 28 | --------------------------------------------------------------------------------