├── pytest.ini ├── setup.cfg ├── .flake8 ├── .pre-commit-config.yaml ├── MANIFEST.in ├── testing ├── conftest.py └── test_basic.py ├── .gitignore ├── make-manifest ├── .github ├── dependabot.yml ├── local-problem-matchers.json └── workflows │ └── ci.yml ├── src └── pytest_twisted │ ├── two.py │ ├── three.py │ └── __init__.py ├── tox.ini ├── CONTRIBUTING.rst ├── LICENSE ├── setup.py └── README.rst /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --verbose 3 | filterwarnings = error 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [sdist] 2 | formats=zip 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | N802 4 | 5 | per-file-ignores = 6 | src/pytest_twisted/__init__.py: F401 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | args: [--line-length=79] 7 | language_version: python3.6 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include LICENSE 3 | include README.rst 4 | include pytest_twisted.py 5 | include setup.cfg 6 | include setup.py 7 | include testing/conftest.py 8 | include testing/test_basic.py 9 | include tox.ini 10 | -------------------------------------------------------------------------------- /testing/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pytest_twisted 3 | 4 | 5 | pytest_plugins = "pytester" 6 | 7 | 8 | @pytest.hookimpl(tryfirst=True) 9 | def pytest_configure(config): 10 | pytest_twisted._use_asyncio_selector_if_required(config=config) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.o 4 | *.so 5 | *.lo 6 | *.la 7 | *.os 8 | *.pyd 9 | *.elc 10 | *~ 11 | .*.swp 12 | .*.swo 13 | .*.swn 14 | .~ 15 | .DS_Store 16 | .ropeproject 17 | ID 18 | __pycache__/ 19 | /pytest_twisted.egg-info/ 20 | /dist/ 21 | /.tox/ 22 | /README.html 23 | .idea/ 24 | -------------------------------------------------------------------------------- /make-manifest: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import os 4 | 5 | 6 | def main(): 7 | files = sorted(set([x.strip() for x in os.popen("git ls-files")]) - 8 | {"make-manifest", ".gitignore"}) 9 | 10 | with open("MANIFEST.in", "w") as f: 11 | for x in files: 12 | f.write("include %s\n" % x) 13 | 14 | 15 | if __name__ == '__main__': 16 | main() 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | # Check for updates once a week 7 | schedule: 8 | interval: "weekly" 9 | day: "wednesday" 10 | target-branch: "main" 11 | open-pull-requests-limit: 10 12 | pull-request-branch-name: 13 | # Separate sections of the branch name with a hyphen 14 | # for example, `dependabot-npm_and_yarn-next_js-acorn-6.4.1` 15 | separator: "-" 16 | -------------------------------------------------------------------------------- /src/pytest_twisted/two.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import defer 2 | 3 | 4 | @defer.inlineCallbacks 5 | def _async_pytest_pyfunc_call(pyfuncitem, f, kwargs): 6 | """Run test function.""" 7 | from pytest_twisted import _get_mark 8 | 9 | fixture_kwargs = { 10 | name: value 11 | for name, value in pyfuncitem.funcargs.items() 12 | if name in pyfuncitem._fixtureinfo.argnames 13 | } 14 | kwargs.update(fixture_kwargs) 15 | 16 | maybe_mark = _get_mark(f) 17 | if maybe_mark == 'async_test': 18 | result = yield defer.ensureDeferred(f(**kwargs)) 19 | elif maybe_mark == 'inline_callbacks_test': 20 | result = yield f(**kwargs) 21 | else: 22 | # TODO: maybe deprecate this 23 | result = yield f(**kwargs) 24 | 25 | defer.returnValue(result) 26 | -------------------------------------------------------------------------------- /.github/local-problem-matchers.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "local-generic-warning", 5 | "severity": "warning", 6 | "pattern": [ 7 | { 8 | "regexp": "^(.*\\bWARNING:\\b.*)$", 9 | "message": 1 10 | } 11 | ] 12 | }, 13 | { 14 | "owner": "local-tox-not_in_env_warning", 15 | "severity": "error", 16 | "pattern": [ 17 | { 18 | "regexp": "^(\\s*WARNING: test command found but not installed.*)$" 19 | }, 20 | { 21 | "regexp": "^(\\s*(cmd:|env:|Maybe you forgot).*)$", 22 | "message": 1, 23 | "loop": true 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{27,py27,35,36,37,38,39,310,311,py37,py38,py39}-defaultreactor 4 | py{35,36,37,38,39,310,311,py37,py38,py39}-asyncioreactor 5 | py{35,36,37,38,39,310,311}-pyqt5reactor 6 | py{35,36,37,38,39,310,311}-pyside2reactor 7 | linting 8 | 9 | [testenv] 10 | deps= 11 | greenlet 12 | pytest 13 | twisted 14 | py37,py38,py39,pypy37,pypy38: hypothesis 15 | pyqt5reactor,pyside2reactor: pytest-qt 16 | pyqt5reactor,pyside2reactor: pytest-qt<4.5.0; python_version >= "3.9" 17 | pyqt5reactor,pyside2reactor: pytest-xvfb 18 | pyqt5reactor,pyside2reactor: pywin32; sys_platform == 'win32' 19 | extras= 20 | pyqt5reactor: pyqt5 21 | pyside2reactor: pyside2 22 | setenv= 23 | defaultreactor: REACTOR = default 24 | pyqt5reactor: REACTOR = qt5reactor 25 | pyside2reactor: REACTOR = qt5reactor 26 | asyncioreactor: REACTOR = asyncio 27 | PIP_PREFER_BINARY = 1 28 | commands= 29 | pytest --reactor={env:REACTOR} 30 | sitepackages=False 31 | download=true 32 | 33 | [testenv:linting] 34 | deps=flake8 35 | commands=flake8 setup.py src/pytest_twisted testing 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | What it takes to add a new reactor: 2 | ----------------------------------- 3 | 4 | * In ``pytest_twisted.py`` 5 | 6 | * Write an ``init_foo_reactor()`` function 7 | * Add ``'foo': init_foo_reactor,`` to ``reactor_installers`` where the key will be the string to be passed such as ``--reactor=foo``. 8 | 9 | * In ``testing/test_basic.py`` 10 | 11 | * Add ``test_blockon_in_hook_with_foo()`` with ``skip_if_reactor_not('foo')`` as the first line 12 | * Add ``test_wrong_reactor_with_foo()`` with ``skip_if_reactor_not('foo')`` as the first line 13 | 14 | * In ``tox.ini`` 15 | 16 | * Adjust ``envlist`` to include the ``fooreactor`` factor for the appropriate versions of Python 17 | * Add conditional ``deps`` for the new reactor such as ``foo: foobar`` to the appropriate test environments 18 | * Add ``fooreactor: pytest --reactor=foo`` to the commands list 19 | 20 | * In ``.github/workflows/ci.yml`` 21 | 22 | * Consider any extra system packages which may be required 23 | 24 | Reference reactor additions: 25 | * `asyncio`_ 26 | * `qt5reactor`_ 27 | 28 | .. _`asyncio`: https://github.com/pytest-dev/pytest-twisted/pull/63 29 | .. _`qt5reactor`: https://github.com/pytest-dev/pytest-twisted/pull/16 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2012, Ralf Schmitt 4 | Copyright (c) 2018, Victor Titor 5 | Copyright (c) 2019-2020, Kyle Altendorf 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /src/pytest_twisted/three.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import defer 2 | 3 | 4 | @defer.inlineCallbacks 5 | def _async_pytest_fixture_setup(fixturedef, request, mark): 6 | """Setup an async or async yield fixture.""" 7 | from pytest_twisted import ( 8 | UnrecognizedCoroutineMarkError, 9 | _create_async_yield_fixture_finalizer, 10 | ) 11 | 12 | fixture_function = fixturedef.func 13 | 14 | kwargs = { 15 | name: request.getfixturevalue(name) 16 | for name in fixturedef.argnames 17 | } 18 | 19 | if mark == 'async_fixture': 20 | arg_value = yield defer.ensureDeferred( 21 | fixture_function(**kwargs) 22 | ) 23 | elif mark == 'async_yield_fixture': 24 | coroutine = fixture_function(**kwargs) 25 | 26 | request.addfinalizer( 27 | _create_async_yield_fixture_finalizer(coroutine=coroutine), 28 | ) 29 | 30 | arg_value = yield defer.ensureDeferred(coroutine.__anext__()) 31 | else: 32 | raise UnrecognizedCoroutineMarkError.from_mark(mark=mark) 33 | 34 | fixturedef.cached_result = (arg_value, fixturedef.cache_key(request), None) 35 | 36 | return arg_value 37 | 38 | 39 | @defer.inlineCallbacks 40 | def _async_pytest_pyfunc_call(pyfuncitem, f, kwargs): 41 | """Run test function.""" 42 | from pytest_twisted import _get_mark 43 | 44 | fixture_kwargs = { 45 | name: value 46 | for name, value in pyfuncitem.funcargs.items() 47 | if name in pyfuncitem._fixtureinfo.argnames 48 | } 49 | kwargs.update(fixture_kwargs) 50 | 51 | maybe_mark = _get_mark(f) 52 | if maybe_mark == 'async_test': 53 | result = yield defer.ensureDeferred(f(**kwargs)) 54 | elif maybe_mark == 'inline_callbacks_test': 55 | result = yield f(**kwargs) 56 | else: 57 | # TODO: maybe deprecate this 58 | result = yield f(**kwargs) 59 | 60 | return result 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.rst") as f: 4 | long_description = f.read() 5 | 6 | setuptools.setup( 7 | name="pytest-twisted", 8 | version="1.14.3", 9 | description="A twisted plugin for pytest.", 10 | long_description=long_description, 11 | long_description_content_type="text/x-rst", 12 | author="Ralf Schmitt, Kyle Altendorf, Victor Titor", 13 | author_email="sda@fstab.net", 14 | url="https://github.com/pytest-dev/pytest-twisted", 15 | packages=setuptools.find_packages('src'), 16 | package_dir={'': 'src'}, 17 | python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*', 18 | install_requires=["greenlet", "pytest>=2.3", "decorator"], 19 | extras_require={ 20 | "dev": ["pre-commit", "black"], 21 | "pyside2": [ 22 | # >= 0.6.3 for PySide2 extra version constraints 23 | "qt5reactor[pyside2]>=0.6.3", 24 | ], 25 | "pyqt5": ["qt5reactor[pyqt5]>=0.6.2"], 26 | }, 27 | classifiers=[ 28 | "Development Status :: 5 - Production/Stable", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: BSD License", 31 | "Operating System :: OS Independent", 32 | "Programming Language :: Python", 33 | "Topic :: Software Development :: Testing", 34 | "Programming Language :: Python :: 2", 35 | "Programming Language :: Python :: 2.7", 36 | "Programming Language :: Python :: 3", 37 | "Programming Language :: Python :: 3.6", 38 | "Programming Language :: Python :: 3.7", 39 | "Programming Language :: Python :: 3.8", 40 | "Programming Language :: Python :: 3.9", 41 | "Programming Language :: Python :: 3.10", 42 | "Programming Language :: Python :: 3.11", 43 | "Programming Language :: Python :: 3.12", 44 | "Programming Language :: Python :: Implementation :: CPython", 45 | "Programming Language :: Python :: Implementation :: PyPy", 46 | ], 47 | entry_points={"pytest11": ["twisted = pytest_twisted"]}, 48 | ) 49 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; coding: utf-8 -*- 2 | 3 | ============================================================================== 4 | pytest-twisted - test twisted code with pytest 5 | ============================================================================== 6 | 7 | |PyPI| |Pythons| |Actions| |Black| 8 | 9 | :Authors: Ralf Schmitt, Kyle Altendorf, Victor Titor 10 | :Version: 1.14.3 11 | :Date: 2024-08-21 12 | :Download: https://pypi.org/project/pytest-twisted/#files 13 | :Code: https://github.com/pytest-dev/pytest-twisted 14 | 15 | 16 | pytest-twisted is a plugin for pytest, which allows to test code, 17 | which uses the twisted framework. test functions can return Deferred 18 | objects and pytest will wait for their completion with this plugin. 19 | 20 | 21 | NOTICE: Python 3.8 with asyncio support 22 | ======================================= 23 | 24 | In Python 3.8, asyncio changed the default loop implementation to use 25 | their proactor. The proactor does not implement some methods used by 26 | Twisted's asyncio support. The result is a ``NotImplementedError`` 27 | exception such as below. 28 | 29 | .. code-block:: pytb 30 | 31 | 32 | File "c:\projects\pytest-twisted\.tox\py38-asyncioreactor\lib\site-packages\twisted\internet\asyncioreactor.py", line 320, in install 33 | reactor = AsyncioSelectorReactor(eventloop) 34 | File "c:\projects\pytest-twisted\.tox\py38-asyncioreactor\lib\site-packages\twisted\internet\asyncioreactor.py", line 69, in __init__ 35 | super().__init__() 36 | File "c:\projects\pytest-twisted\.tox\py38-asyncioreactor\lib\site-packages\twisted\internet\base.py", line 571, in __init__ 37 | self.installWaker() 38 | File "c:\projects\pytest-twisted\.tox\py38-asyncioreactor\lib\site-packages\twisted\internet\posixbase.py", line 286, in installWaker 39 | self.addReader(self.waker) 40 | File "c:\projects\pytest-twisted\.tox\py38-asyncioreactor\lib\site-packages\twisted\internet\asyncioreactor.py", line 151, in addReader 41 | self._asyncioEventloop.add_reader(fd, callWithLogger, reader, 42 | File "C:\Python38-x64\Lib\asyncio\events.py", line 501, in add_reader 43 | raise NotImplementedError 44 | NotImplementedError 45 | 46 | The previous default, the selector loop, still works but you have to 47 | explicitly set it and do so early. The following ``conftest.py`` is provided 48 | for reference. 49 | 50 | .. code-block:: python3 51 | 52 | import sys 53 | 54 | import pytest 55 | import pytest_twisted 56 | 57 | 58 | @pytest.hookimpl(tryfirst=True) 59 | def pytest_configure(config): 60 | # https://twistedmatrix.com/trac/ticket/9766 61 | # https://github.com/pytest-dev/pytest-twisted/issues/80 62 | 63 | if ( 64 | config.getoption("reactor", "default") == "asyncio" 65 | and sys.platform == 'win32' 66 | and sys.version_info >= (3, 8) 67 | ): 68 | import asyncio 69 | 70 | selector_policy = asyncio.WindowsSelectorEventLoopPolicy() 71 | asyncio.set_event_loop_policy(selector_policy) 72 | 73 | 74 | Python 2 support plans 75 | ====================== 76 | 77 | At some point it may become impractical to retain Python 2 support. 78 | Given the small size and very low amount of development it seems 79 | likely that this will not be a near term issue. While I personally 80 | have no need for Python 2 support I try to err on the side of being 81 | helpful so support will not be explicitly removed just to not have to 82 | think about it. If major issues are reported and neither myself nor 83 | the community have time to resolve them then options will be 84 | considered. 85 | 86 | 87 | Installation 88 | ============ 89 | Install the plugin as below. 90 | 91 | .. code-block:: sh 92 | 93 | pip install pytest-twisted 94 | 95 | 96 | Using the plugin 97 | ================ 98 | 99 | The plugin is available after installation and can be disabled using 100 | ``-p no:twisted``. 101 | 102 | By default ``twisted.internet.default`` is used to install the reactor. 103 | This creates the same reactor that ``import twisted.internet.reactor`` 104 | would. Alternative reactors can be specified using the ``--reactor`` 105 | option. This presently supports ``qt5reactor`` for use with ``pyqt5`` 106 | and ``pytest-qt`` as well as ``asyncio``. This `guide`_ describes how to add 107 | support for a new reactor. 108 | 109 | The reactor is automatically created prior to the first test but can 110 | be explicitly installed earlier by calling 111 | ``pytest_twisted.init_default_reactor()`` or the corresponding function 112 | for the desired alternate reactor. 113 | 114 | 115 | inlineCallbacks 116 | =============== 117 | Using ``twisted.internet.defer.inlineCallbacks`` as a decorator for test 118 | functions, which use fixtures, does not work. Please use 119 | ``pytest_twisted.inlineCallbacks`` instead. 120 | 121 | .. code-block:: python 122 | 123 | @pytest_twisted.inlineCallbacks 124 | def test_some_stuff(tmpdir): 125 | res = yield threads.deferToThread(os.listdir, tmpdir.strpath) 126 | assert res == [] 127 | 128 | 129 | ensureDeferred 130 | ============== 131 | Using ``twisted.internet.defer.ensureDeferred`` as a decorator for test 132 | functions, which use fixtures, does not work. Please use 133 | ``pytest_twisted.ensureDeferred`` instead. 134 | 135 | .. code-block:: python 136 | 137 | @pytest_twisted.ensureDeferred 138 | async def test_some_stuff(tmpdir): 139 | res = await threads.deferToThread(os.listdir, tmpdir.strpath) 140 | assert res == [] 141 | 142 | 143 | Waiting for deferreds in fixtures 144 | ================================= 145 | ``pytest_twisted.blockon`` allows fixtures to wait for deferreds. 146 | 147 | .. code-block:: python 148 | 149 | @pytest.fixture 150 | def val(): 151 | d = defer.Deferred() 152 | reactor.callLater(1.0, d.callback, 10) 153 | return pytest_twisted.blockon(d) 154 | 155 | 156 | async/await fixtures 157 | ==================== 158 | ``async``/``await`` fixtures can be used along with ``yield`` for normal 159 | pytest fixture semantics of setup, value, and teardown. At present only 160 | function and module scope are supported. 161 | 162 | .. code-block:: python 163 | 164 | # No yield (coroutine function) 165 | # -> use pytest_twisted.async_fixture() 166 | @pytest_twisted.async_fixture() 167 | async def foo(): 168 | d = defer.Deferred() 169 | reactor.callLater(0.01, d.callback, 42) 170 | value = await d 171 | return value 172 | 173 | # With yield (asynchronous generator) 174 | # -> use pytest_twisted.async_yield_fixture() 175 | @pytest_twisted.async_yield_fixture() 176 | async def foo_with_teardown(): 177 | d1, d2 = defer.Deferred(), defer.Deferred() 178 | reactor.callLater(0.01, d1.callback, 42) 179 | reactor.callLater(0.02, d2.callback, 37) 180 | value = await d1 181 | yield value 182 | await d2 183 | 184 | 185 | Hypothesis 186 | ========== 187 | pytest-twisted can be used with Hypothesis. 188 | 189 | .. code-block:: python 190 | 191 | @hypothesis.given(x=hypothesis.strategies.integers()) 192 | @pytest_twisted.ensureDeferred 193 | async def test_async(x): 194 | assert isinstance(x, int) 195 | 196 | 197 | The twisted greenlet 198 | ==================== 199 | Some libraries (e.g. corotwine) need to know the greenlet, which is 200 | running the twisted reactor. It's available from the 201 | ``twisted_greenlet`` fixture. The following code can be used to make 202 | corotwine work with pytest-twisted. 203 | 204 | .. code-block:: python 205 | 206 | @pytest.fixture(scope="session", autouse=True) 207 | def set_MAIN(request, twisted_greenlet): 208 | from corotwine import protocol 209 | protocol.MAIN = twisted_greenlet 210 | 211 | 212 | That's (almost) all. 213 | 214 | 215 | Deprecations 216 | ============ 217 | 218 | ---- 219 | v1.9 220 | ---- 221 | 222 | ``pytest.blockon`` 223 | Use ``pytest_twisted.blockon`` 224 | ``pytest.inlineCallbacks`` 225 | Use ``pytest_twisted.inlineCallbacks`` 226 | 227 | 228 | .. |PyPI| image:: https://img.shields.io/pypi/v/pytest-twisted.svg 229 | :alt: PyPI version 230 | :target: https://pypi.org/project/pytest-twisted/ 231 | 232 | .. |Pythons| image:: https://img.shields.io/pypi/pyversions/pytest-twisted.svg 233 | :alt: Supported Python versions 234 | :target: https://pypi.org/project/pytest-twisted/ 235 | 236 | .. |Actions| image:: https://img.shields.io/github/workflow/status/pytest-dev/pytest-twisted/CI/master?logo=GitHub-Actions 237 | :alt: GitHub Actions build status 238 | :target: https://github.com/pytest-dev/pytest-twisted/actions?query=branch%3Amaster 239 | 240 | .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg 241 | :alt: Black code style 242 | :target: https://github.com/psf/black 243 | 244 | .. _guide: CONTRIBUTING.rst 245 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | pull_request: 10 | branches: 11 | - "*" 12 | release: 13 | types: 14 | - published 15 | schedule: 16 | # Daily at 05:47 17 | - cron: '47 5 * * *' 18 | 19 | concurrency: 20 | group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} 21 | cancel-in-progress: true 22 | 23 | env: 24 | PIP_NO_PYTHON_VERSION_WARNING: 1 25 | 26 | permissions: 27 | id-token: write 28 | 29 | jobs: 30 | build: 31 | name: Build 32 | runs-on: ubuntu-latest 33 | container: docker://python:3.11-buster 34 | steps: 35 | - uses: actions/checkout@v6 36 | - name: Build 37 | run: | 38 | python -m venv venv 39 | venv/bin/pip install --upgrade pip 40 | venv/bin/pip install build 41 | venv/bin/python -m build --outdir dist/ 42 | - uses: actions/upload-artifact@v6 43 | if: always() 44 | with: 45 | name: dist 46 | path: dist/* 47 | if-no-files-found: error 48 | 49 | test: 50 | name: ${{ matrix.os.name }} ${{ matrix.python.name }} ${{ matrix.reactor.name }} ${{ matrix.arch.name }} 51 | needs: build 52 | runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }} 53 | container: ${{ matrix.os.container[matrix.python.docker] }} 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | os: 58 | - name: 🐧 59 | matrix: linux 60 | runs-on: 61 | intel: ubuntu-latest 62 | python_platform: linux 63 | container: 64 | "2.7": docker://python:2.7-buster 65 | "3.6": docker://python:3.6-bullseye 66 | "3.7": docker://python:3.7-bookworm 67 | "3.8": docker://python:3.8-bookworm 68 | "3.9": docker://python:3.9-bookworm 69 | "3.10": docker://python:3.10-bookworm 70 | "3.11": docker://python:3.11-bookworm 71 | "3.12": docker://python:3.12-bookworm 72 | "3.13": docker://python:3.13-bookworm 73 | "pypy2.7": docker://pypy:2.7-bookworm 74 | "pypy3.7": docker://pypy:3.7-bullseye 75 | "pypy3.8": docker://pypy:3.8-bookworm 76 | "pypy3.9": docker://pypy:3.9-bookworm 77 | "pypy3.10": docker://pypy:3.10-bookworm 78 | - name: 🪟 79 | matrix: windows 80 | runs-on: 81 | intel: windows-latest 82 | python_platform: win32 83 | - name: 🍎 84 | matrix: macos 85 | runs-on: 86 | arm: macos-latest 87 | intel: macos-15-intel 88 | python_platform: darwin 89 | python: 90 | - name: CPython 2.7 91 | tox: py27 92 | major-dot-minor: 2.7 93 | action: 2.7 94 | docker: 2.7 95 | implementation: cpython 96 | major: 2 97 | - name: CPython 3.6 98 | tox: py36 99 | major-dot-minor: 3.6 100 | action: 3.6 101 | docker: 3.6 102 | implementation: cpython 103 | major: 3 104 | - name: CPython 3.7 105 | tox: py37 106 | major-dot-minor: 3.7 107 | action: 3.7 108 | docker: 3.7 109 | implementation: cpython 110 | major: 3 111 | - name: CPython 3.8 112 | tox: py38 113 | major-dot-minor: 3.8 114 | action: 3.8 115 | docker: 3.8 116 | implementation: cpython 117 | major: 3 118 | - name: CPython 3.9 119 | tox: py39 120 | major-dot-minor: 3.9 121 | action: 3.9 122 | docker: 3.9 123 | implementation: cpython 124 | major: 3 125 | - name: CPython 3.10 126 | tox: py310 127 | major-dot-minor: "3.10" 128 | action: "3.10" 129 | docker: "3.10" 130 | implementation: cpython 131 | major: 3 132 | - name: CPython 3.11 133 | tox: py311 134 | major-dot-minor: "3.11" 135 | action: "3.11" 136 | docker: "3.11" 137 | implementation: cpython 138 | major: 3 139 | - name: CPython 3.12 140 | tox: py312 141 | major-dot-minor: "3.12" 142 | action: "3.12" 143 | docker: "3.12" 144 | implementation: cpython 145 | major: 3 146 | - name: CPython 3.13 147 | tox: py313 148 | major-dot-minor: "3.13" 149 | action: "3.13" 150 | docker: "3.13" 151 | implementation: cpython 152 | major: 3 153 | # disabled due to installation failures 154 | # https://github.com/pytest-dev/pytest-twisted/pull/157 155 | # - name: PyPy 2.7 156 | # tox: pypy27 157 | # action: pypy-2.7 158 | # docker: pypy2.7 159 | # implementation: pypy 160 | # major: 2 161 | - name: PyPy 3.7 162 | tox: pypy37 163 | major-dot-minor: 3.7 164 | action: pypy-3.7 165 | docker: pypy3.7 166 | implementation: pypy 167 | major: 3 168 | - name: PyPy 3.8 169 | tox: pypy38 170 | major-dot-minor: 3.8 171 | action: pypy-3.8 172 | docker: pypy3.8 173 | implementation: pypy 174 | major: 3 175 | - name: PyPy 3.9 176 | tox: pypy39 177 | major-dot-minor: 3.9 178 | action: pypy-3.9 179 | docker: pypy3.9 180 | implementation: pypy 181 | major: 3 182 | - name: PyPy 3.10 183 | tox: pypy310 184 | major-dot-minor: "3.10" 185 | action: pypy-3.10 186 | docker: pypy3.10 187 | implementation: pypy 188 | major: 3 189 | reactor: 190 | - name: default 191 | tox: default 192 | dependencies: default 193 | - name: PyQt5 194 | tox: pyqt5 195 | dependencies: qt5 196 | - name: PySide2 197 | tox: pyside2 198 | dependencies: qt5 199 | - name: asyncio 200 | tox: asyncio 201 | dependencies: asyncio 202 | arch: 203 | - name: ARM 204 | matrix: arm 205 | setup-python: 206 | architecture: arm64 207 | - name: Intel 208 | matrix: intel 209 | setup-python: 210 | architecture: x64 211 | exclude: 212 | - python: 213 | major: 2 214 | os: 215 | python_platform: darwin 216 | - python: 217 | major: 2 218 | os: 219 | python_platform: win32 220 | - python: 221 | major: 2 222 | reactor: 223 | tox: pyqt5 224 | - python: 225 | major: 2 226 | reactor: 227 | tox: pyside2 228 | - python: 229 | major-dot-minor: "3.11" 230 | reactor: 231 | tox: pyside2 232 | - python: 233 | major-dot-minor: "3.12" 234 | reactor: 235 | tox: pyside2 236 | - python: 237 | major-dot-minor: "3.13" 238 | reactor: 239 | tox: pyside2 240 | - python: 241 | major: 2 242 | reactor: 243 | tox: asyncio 244 | - python: 245 | implementation: pypy 246 | reactor: 247 | tox: pyqt5 248 | - python: 249 | implementation: pypy 250 | reactor: 251 | tox: pyside2 252 | - os: 253 | matrix: linux 254 | arch: 255 | matrix: arm 256 | - os: 257 | matrix: windows 258 | arch: 259 | matrix: arm 260 | - os: 261 | matrix: macos 262 | python: 263 | major-dot-minor: "3.6" 264 | arch: 265 | matrix: arm 266 | - os: 267 | matrix: macos 268 | python: 269 | major-dot-minor: "3.7" 270 | arch: 271 | matrix: arm 272 | - arch: 273 | matrix: arm 274 | reactor: 275 | tox: pyside2 276 | steps: 277 | - uses: actions/checkout@v6 278 | - name: Enable Problem Matchers 279 | run: | 280 | echo "::add-matcher::.github/local-problem-matchers.json" 281 | - name: Set up ${{ matrix.python.name }} 282 | if: ${{ job.container == '' }} 283 | uses: actions/setup-python@v6 284 | with: 285 | # This allows the matrix to specify just the major.minor version while still 286 | # expanding it to get the latest patch version including alpha releases. 287 | # This avoids the need to update for each new alpha, beta, release candidate, 288 | # and then finally an actual release version. actions/setup-python doesn't 289 | # support this for PyPy presently so we get no help there. 290 | # 291 | # CPython -> 3.9.0-alpha - 3.9.X 292 | # PyPy -> pypy-3.7 293 | python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python.action), matrix.python.action))[startsWith(matrix.python.action, 'pypy')] }} 294 | architecture: ${{ matrix.arch.setup-python.architecture }} 295 | - name: Report Python information 296 | shell: bash 297 | run: | 298 | python -c 'import sys; print(sys.version)' 299 | echo 300 | echo " <=======>" 301 | echo 302 | pip --version 303 | echo 304 | echo " <=======>" 305 | echo 306 | pip list 307 | echo 308 | echo " <=======>" 309 | echo 310 | pip freeze --all 311 | - name: Install Linux Qt5 dependencies 312 | if: matrix.os.python_platform == 'linux' && matrix.reactor.dependencies == 'qt5' 313 | run: | 314 | apt-get update --yes 315 | apt-get install --yes libgl1 316 | - name: Install 317 | run: | 318 | pip install tox 319 | - uses: actions/download-artifact@v7 320 | with: 321 | name: dist 322 | path: dist/ 323 | - name: Test 324 | shell: bash 325 | run: | 326 | tox --installpkg dist/*.whl -v -e "${{ matrix.python.tox }}-${{ matrix.reactor.tox }}reactor" 327 | 328 | linting: 329 | name: Linting 330 | runs-on: ubuntu-latest 331 | strategy: 332 | matrix: 333 | python: 334 | - short: 311 335 | dotted: "3.11" 336 | steps: 337 | - uses: actions/checkout@v6 338 | - name: Set up Python ${{ matrix.python.dotted }} 339 | uses: actions/setup-python@v6 340 | with: 341 | python-version: ${{ matrix.python.dotted }} 342 | architecture: x64 343 | - name: Install 344 | run: | 345 | pip install tox 346 | - name: Test 347 | run: | 348 | tox -v -e linting 349 | 350 | publish: 351 | name: Publish 352 | runs-on: ubuntu-latest 353 | needs: 354 | - build 355 | - test 356 | - linting 357 | steps: 358 | - uses: actions/download-artifact@v7 359 | with: 360 | name: dist 361 | path: dist/ 362 | - uses: pypa/gh-action-pypi-publish@release/v1 363 | if: github.event_name == 'release' 364 | with: 365 | packages-dir: dist/ 366 | skip-existing: true 367 | 368 | all: 369 | name: All 370 | runs-on: ubuntu-latest 371 | needs: 372 | - build 373 | - test 374 | - linting 375 | - publish 376 | steps: 377 | - name: This 378 | shell: python 379 | run: | 380 | import this 381 | -------------------------------------------------------------------------------- /src/pytest_twisted/__init__.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | import itertools 4 | import signal 5 | import sys 6 | import threading 7 | import warnings 8 | 9 | import decorator 10 | import greenlet 11 | import pytest 12 | 13 | from twisted.internet import defer, error 14 | from twisted.internet.threads import blockingCallFromThread 15 | from twisted.python import failure 16 | 17 | if sys.version_info[0] == 3: 18 | from pytest_twisted.three import ( 19 | _async_pytest_fixture_setup, 20 | _async_pytest_pyfunc_call, 21 | ) 22 | elif sys.version_info[0] == 2: 23 | from pytest_twisted.two import _async_pytest_pyfunc_call 24 | 25 | 26 | class WrongReactorAlreadyInstalledError(Exception): 27 | pass 28 | 29 | 30 | class UnrecognizedCoroutineMarkError(Exception): 31 | @classmethod 32 | def from_mark(cls, mark): 33 | return cls( 34 | 'Coroutine wrapper mark not recognized: {}'.format(repr(mark)), 35 | ) 36 | 37 | 38 | class AsyncGeneratorFixtureDidNotStopError(Exception): 39 | @classmethod 40 | def from_generator(cls, generator): 41 | return cls( 42 | 'async fixture did not stop: {}'.format(generator), 43 | ) 44 | 45 | 46 | class AsyncFixtureUnsupportedScopeError(Exception): 47 | @classmethod 48 | def from_scope(cls, scope): 49 | return cls( 50 | 'Unsupported scope {0!r} used for async fixture'.format(scope) 51 | ) 52 | 53 | 54 | class _config: 55 | external_reactor = False 56 | 57 | 58 | class _instances: 59 | gr_twisted = None 60 | reactor = None 61 | 62 | 63 | def _deprecate(deprecated, recommended): 64 | def decorator(f): 65 | @functools.wraps(f) 66 | def wrapper(*args, **kwargs): 67 | warnings.warn( 68 | '{deprecated} has been deprecated, use {recommended}'.format( 69 | deprecated=deprecated, 70 | recommended=recommended, 71 | ), 72 | DeprecationWarning, 73 | stacklevel=2, 74 | ) 75 | return f(*args, **kwargs) 76 | 77 | return wrapper 78 | 79 | return decorator 80 | 81 | 82 | def blockon(d): 83 | if _config.external_reactor: 84 | return block_from_thread(d) 85 | 86 | return blockon_default(d) 87 | 88 | 89 | def blockon_default(d): 90 | current = greenlet.getcurrent() 91 | assert ( 92 | current is not _instances.gr_twisted 93 | ), "blockon cannot be called from the twisted greenlet" 94 | result = [] 95 | 96 | def cb(r): 97 | result.append(r) 98 | if greenlet.getcurrent() is not current: 99 | current.switch(result) 100 | 101 | d.addCallbacks(cb, cb) 102 | if not result: 103 | _result = _instances.gr_twisted.switch() 104 | assert _result is result, "illegal switch in blockon" 105 | 106 | if isinstance(result[0], failure.Failure): 107 | result[0].raiseException() 108 | 109 | return result[0] 110 | 111 | 112 | def block_from_thread(d): 113 | return blockingCallFromThread(_instances.reactor, lambda x: x, d) 114 | 115 | 116 | def decorator_apply(dec, func): 117 | """ 118 | Decorate a function by preserving the signature even if dec 119 | is not a signature-preserving decorator. 120 | 121 | https://github.com/micheles/decorator/blob/55a68b5ef1951614c5c37a6d201b1f3b804dbce6/docs/documentation.md#dealing-with-third-party-decorators 122 | """ 123 | return decorator.FunctionMaker.create( 124 | func, 'return decfunc(%(signature)s)', 125 | dict(decfunc=dec(func)), __wrapped__=func) 126 | 127 | 128 | class DecoratorArgumentsError(Exception): 129 | pass 130 | 131 | 132 | def repr_args_kwargs(*args, **kwargs): 133 | arguments = ', '.join(itertools.chain( 134 | (repr(x) for x in args), 135 | ('{}={}'.format(k, repr(v)) for k, v in kwargs.items()) 136 | )) 137 | 138 | return '({})'.format(arguments) 139 | 140 | 141 | def _positional_not_allowed_exception(*args, **kwargs): 142 | arguments = repr_args_kwargs(*args, **kwargs) 143 | 144 | return DecoratorArgumentsError( 145 | 'Positional decorator arguments not allowed: {}'.format(arguments), 146 | ) 147 | 148 | 149 | def _optional_arguments(): 150 | def decorator_decorator(d): 151 | # TODO: this should get the signature of d minus the f or something 152 | def decorator_wrapper(*args, **decorator_arguments): 153 | """this is decorator_wrapper""" 154 | if len(args) > 1: 155 | raise _positional_not_allowed_exception() 156 | 157 | if len(args) == 1: 158 | maybe_f = args[0] 159 | 160 | if len(decorator_arguments) > 0 or not callable(maybe_f): 161 | raise _positional_not_allowed_exception() 162 | 163 | f = maybe_f 164 | return d(f) 165 | 166 | # TODO: this should get the signature of d minus the kwargs 167 | def decorator_closure_on_arguments(f): 168 | return d(f, **decorator_arguments) 169 | 170 | return decorator_closure_on_arguments 171 | 172 | return decorator_wrapper 173 | 174 | return decorator_decorator 175 | 176 | 177 | @_optional_arguments() 178 | def inlineCallbacks(f): 179 | """ 180 | Mark as inline callbacks test for pytest-twisted processing and apply 181 | @inlineCallbacks. 182 | 183 | Unlike @ensureDeferred, @inlineCallbacks can be applied here because it 184 | does not call nor schedule the test function. Further, @inlineCallbacks 185 | must be applied here otherwise pytest identifies the test as a 'yield test' 186 | for which they dropped support in 4.0 and now they skip. 187 | """ 188 | decorated = decorator_apply(defer.inlineCallbacks, f) 189 | _set_mark(o=decorated, mark='inline_callbacks_test') 190 | 191 | return decorated 192 | 193 | 194 | @_optional_arguments() 195 | def ensureDeferred(f): 196 | """ 197 | Mark as async test for pytest-twisted processing. 198 | 199 | Unlike @inlineCallbacks, @ensureDeferred must not be applied here since it 200 | would call and schedule the test function. 201 | """ 202 | _set_mark(o=f, mark='async_test') 203 | 204 | return f 205 | 206 | 207 | def init_twisted_greenlet(): 208 | if _instances.reactor is None or _instances.gr_twisted: 209 | return 210 | 211 | if not _instances.reactor.running: 212 | if not isinstance(threading.current_thread(), threading._MainThread): 213 | warnings.warn( 214 | ( 215 | 'Will not attempt to block Twisted signal configuration' 216 | ' since we are not running in the main thread. See' 217 | ' https://github.com/pytest-dev/pytest-twisted/issues/153.' 218 | ), 219 | RuntimeWarning, 220 | ) 221 | elif signal.getsignal(signal.SIGINT) == signal.default_int_handler: 222 | signal.signal( 223 | signal.SIGINT, 224 | functools.partial(signal.default_int_handler), 225 | ) 226 | _instances.gr_twisted = greenlet.greenlet(_instances.reactor.run) 227 | # give me better tracebacks: 228 | failure.Failure.cleanFailure = lambda self: None 229 | else: 230 | _config.external_reactor = True 231 | 232 | 233 | def stop_twisted_greenlet(): 234 | if _instances.gr_twisted: 235 | try: 236 | _instances.reactor.stop() 237 | except error.ReactorNotRunning: 238 | # Sometimes the reactor is stopped before we get here. For 239 | # example, this can happen in response to a SIGINT in some cases. 240 | pass 241 | _instances.gr_twisted.switch() 242 | 243 | 244 | def _get_mark(o, default=None): 245 | """Get the pytest-twisted test or fixture mark.""" 246 | return getattr(o, _mark_attribute_name, default) 247 | 248 | 249 | def _set_mark(o, mark): 250 | """Set the pytest-twisted test or fixture mark.""" 251 | setattr(o, _mark_attribute_name, mark) 252 | 253 | 254 | def _marked_async_fixture(mark): 255 | @functools.wraps(pytest.fixture) 256 | @_optional_arguments() 257 | def fixture(f, *args, **kwargs): 258 | try: 259 | scope = args[0] 260 | except IndexError: 261 | scope = kwargs.get('scope', 'function') 262 | 263 | if scope not in ['function', 'module']: 264 | # TODO: handle... 265 | # - class 266 | # - package 267 | # - session 268 | # - dynamic 269 | # 270 | # https://docs.pytest.org/en/latest/reference.html#pytest-fixture-api 271 | # then remove this and update docs, or maybe keep it around 272 | # in case new options come in without support? 273 | # 274 | # https://github.com/pytest-dev/pytest-twisted/issues/56 275 | raise AsyncFixtureUnsupportedScopeError.from_scope(scope=scope) 276 | 277 | _set_mark(f, mark) 278 | result = pytest.fixture(*args, **kwargs)(f) 279 | 280 | return result 281 | 282 | return fixture 283 | 284 | 285 | _mark_attribute_name = '_pytest_twisted_mark' 286 | async_fixture = _marked_async_fixture('async_fixture') 287 | async_yield_fixture = _marked_async_fixture('async_yield_fixture') 288 | 289 | 290 | def pytest_fixture_setup(fixturedef, request): 291 | """Interface pytest to async for async and async yield fixtures.""" 292 | # TODO: what about _adding_ inlineCallbacks fixture support? 293 | maybe_mark = _get_mark(fixturedef.func) 294 | if maybe_mark is None: 295 | return None 296 | 297 | mark = maybe_mark 298 | 299 | _run_inline_callbacks( 300 | _async_pytest_fixture_setup, 301 | fixturedef, 302 | request, 303 | mark, 304 | ) 305 | 306 | return not None 307 | 308 | 309 | def _create_async_yield_fixture_finalizer(coroutine): 310 | def finalizer(): 311 | _run_inline_callbacks( 312 | _tear_it_down, 313 | defer.ensureDeferred(coroutine.__anext__()), 314 | ) 315 | 316 | return finalizer 317 | 318 | 319 | @defer.inlineCallbacks 320 | def _tear_it_down(deferred): 321 | """Tear down a specific async yield fixture.""" 322 | try: 323 | yield deferred 324 | except StopAsyncIteration: 325 | return 326 | 327 | # TODO: six.raise_from() 328 | raise AsyncGeneratorFixtureDidNotStopError.from_generator( 329 | generator=deferred, 330 | ) 331 | 332 | 333 | def _run_inline_callbacks(f, *args): 334 | """Interface into Twisted greenlet to run and wait for a deferred.""" 335 | if _instances.gr_twisted is not None: 336 | if _instances.gr_twisted.dead: 337 | raise RuntimeError("twisted reactor has stopped") 338 | 339 | def in_reactor(d, f, *args): 340 | return defer.maybeDeferred(f, *args).chainDeferred(d) 341 | 342 | d = defer.Deferred() 343 | _instances.reactor.callLater(0.0, in_reactor, d, f, *args) 344 | blockon_default(d) 345 | else: 346 | if not _instances.reactor.running: 347 | raise RuntimeError("twisted reactor is not running") 348 | blockingCallFromThread(_instances.reactor, f, *args) 349 | 350 | 351 | def pytest_pyfunc_call(pyfuncitem): 352 | """Interface to async test call handler.""" 353 | # TODO: only handle 'our' tests? what is the point of handling others? 354 | # well, because our interface allowed people to return deferreds 355 | # from arbitrary tests so we kinda have to keep this up for now 356 | maybe_hypothesis = getattr(pyfuncitem.obj, "hypothesis", None) 357 | if maybe_hypothesis is None: 358 | _run_inline_callbacks( 359 | _async_pytest_pyfunc_call, 360 | pyfuncitem, 361 | pyfuncitem.obj, 362 | {} 363 | ) 364 | result = not None 365 | else: 366 | hypothesis = maybe_hypothesis 367 | f = hypothesis.inner_test 368 | 369 | def inner_test(**kwargs): 370 | return _run_inline_callbacks( 371 | _async_pytest_pyfunc_call, 372 | pyfuncitem, 373 | f, 374 | kwargs, 375 | ) 376 | 377 | pyfuncitem.obj.hypothesis.inner_test = inner_test 378 | result = None 379 | 380 | return result 381 | 382 | 383 | @pytest.fixture(scope="session", autouse=True) 384 | def twisted_greenlet(): 385 | """Provide the twisted greenlet in fixture form.""" 386 | return _instances.gr_twisted 387 | 388 | 389 | def init_default_reactor(): 390 | """Install the default Twisted reactor.""" 391 | import twisted.internet.default 392 | 393 | module = inspect.getmodule(twisted.internet.default.install) 394 | 395 | module_name = module.__name__.split(".")[-1] 396 | reactor_type_name, = (x for x in dir(module) if x.lower() == module_name) 397 | reactor_type = getattr(module, reactor_type_name) 398 | 399 | _install_reactor( 400 | reactor_installer=twisted.internet.default.install, 401 | reactor_type=reactor_type, 402 | ) 403 | 404 | 405 | def init_qt5_reactor(): 406 | """Install the qt5reactor... reactor.""" 407 | import qt5reactor 408 | 409 | _install_reactor( 410 | reactor_installer=qt5reactor.install, reactor_type=qt5reactor.QtReactor 411 | ) 412 | 413 | 414 | def init_asyncio_reactor(): 415 | """Install the Twisted reactor for asyncio.""" 416 | from twisted.internet import asyncioreactor 417 | 418 | _install_reactor( 419 | reactor_installer=asyncioreactor.install, 420 | reactor_type=asyncioreactor.AsyncioSelectorReactor, 421 | ) 422 | 423 | 424 | reactor_installers = { 425 | "default": init_default_reactor, 426 | "qt5reactor": init_qt5_reactor, 427 | "asyncio": init_asyncio_reactor, 428 | } 429 | 430 | 431 | def _install_reactor(reactor_installer, reactor_type): 432 | """Install the specified reactor and create the greenlet.""" 433 | try: 434 | reactor_installer() 435 | except error.ReactorAlreadyInstalledError: 436 | import twisted.internet.reactor 437 | 438 | if not isinstance(twisted.internet.reactor, reactor_type): 439 | raise WrongReactorAlreadyInstalledError( 440 | "expected {} but found {}".format( 441 | reactor_type, type(twisted.internet.reactor) 442 | ) 443 | ) 444 | 445 | import twisted.internet.reactor 446 | 447 | _instances.reactor = twisted.internet.reactor 448 | init_twisted_greenlet() 449 | 450 | 451 | def pytest_addoption(parser): 452 | """Add options into the pytest CLI.""" 453 | group = parser.getgroup("twisted") 454 | group.addoption( 455 | "--reactor", 456 | default="default", 457 | choices=tuple(reactor_installers.keys()), 458 | ) 459 | 460 | 461 | def pytest_configure(config): 462 | """Identify and install chosen reactor.""" 463 | pytest.inlineCallbacks = _deprecate( 464 | deprecated='pytest.inlineCallbacks', 465 | recommended='pytest_twisted.inlineCallbacks', 466 | )(inlineCallbacks) 467 | pytest.blockon = _deprecate( 468 | deprecated='pytest.blockon', 469 | recommended='pytest_twisted.blockon', 470 | )(blockon) 471 | 472 | reactor_installers[config.getoption("reactor")]() 473 | 474 | 475 | def pytest_unconfigure(config): 476 | """Stop the reactor greenlet.""" 477 | stop_twisted_greenlet() 478 | 479 | 480 | def _use_asyncio_selector_if_required(config): 481 | """Set asyncio selector event loop policy if needed.""" 482 | # https://twistedmatrix.com/trac/ticket/9766 483 | # https://github.com/pytest-dev/pytest-twisted/issues/80 484 | 485 | is_asyncio = config.getoption("reactor", "default") == "asyncio" 486 | 487 | if is_asyncio and sys.platform == 'win32' and sys.version_info >= (3, 8): 488 | import asyncio 489 | 490 | selector_policy = asyncio.WindowsSelectorEventLoopPolicy() 491 | asyncio.set_event_loop_policy(selector_policy) 492 | -------------------------------------------------------------------------------- /testing/test_basic.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import textwrap 5 | 6 | import pytest 7 | 8 | 9 | # https://docs.python.org/3/whatsnew/3.5.html#pep-492-coroutines-with-async-and-await-syntax 10 | ASYNC_AWAIT = sys.version_info >= (3, 5) 11 | 12 | # https://docs.python.org/3/whatsnew/3.6.html#pep-525-asynchronous-generators 13 | ASYNC_GENERATORS = sys.version_info >= (3, 6) 14 | 15 | timeout = 15 16 | 17 | pytest_version = tuple( 18 | int(segment) 19 | for segment in pytest.__version__.split(".")[:3] 20 | ) 21 | 22 | 23 | # https://github.com/pytest-dev/pytest/issues/6505 24 | def force_plural(name): 25 | if name in {"error", "warning"}: 26 | return name + "s" 27 | 28 | return name 29 | 30 | 31 | def assert_outcomes(run_result, outcomes): 32 | formatted_output = format_run_result_output_for_assert(run_result) 33 | 34 | try: 35 | result_outcomes = run_result.parseoutcomes() 36 | except ValueError: 37 | assert False, formatted_output 38 | 39 | normalized_result_outcomes = { 40 | force_plural(name): outcome 41 | for name, outcome in result_outcomes.items() 42 | if name != "seconds" 43 | } 44 | 45 | assert normalized_result_outcomes == outcomes, formatted_output 46 | 47 | 48 | def format_run_result_output_for_assert(run_result): 49 | tpl = """ 50 | ---- stdout 51 | {} 52 | ---- stderr 53 | {} 54 | ---- 55 | """ 56 | return textwrap.dedent(tpl).format( 57 | run_result.stdout.str(), run_result.stderr.str() 58 | ) 59 | 60 | 61 | @pytest.fixture(name="default_conftest", autouse=True) 62 | def _default_conftest(testdir): 63 | testdir.makeconftest(textwrap.dedent(""" 64 | import pytest 65 | import pytest_twisted 66 | 67 | 68 | @pytest.hookimpl(tryfirst=True) 69 | def pytest_configure(config): 70 | pytest_twisted._use_asyncio_selector_if_required(config=config) 71 | """)) 72 | 73 | 74 | def skip_if_reactor_not(request, expected_reactor): 75 | actual_reactor = request.config.getoption("reactor", "default") 76 | if actual_reactor != expected_reactor: 77 | pytest.skip( 78 | "reactor is {} not {}".format(actual_reactor, expected_reactor), 79 | ) 80 | 81 | 82 | def skip_if_no_async_await(): 83 | return pytest.mark.skipif( 84 | not ASYNC_AWAIT, 85 | reason="async/await syntax not supported on Python <3.5", 86 | ) 87 | 88 | 89 | def skip_if_no_async_generators(): 90 | return pytest.mark.skipif( 91 | not ASYNC_GENERATORS, 92 | reason="async generators not support on Python <3.6", 93 | ) 94 | 95 | 96 | def skip_if_hypothesis_unavailable(): 97 | def hypothesis_unavailable(): 98 | try: 99 | import hypothesis # noqa: F401 100 | except ImportError: 101 | return True 102 | 103 | return False 104 | 105 | return pytest.mark.skipif( 106 | hypothesis_unavailable(), 107 | reason="hypothesis not installed", 108 | ) 109 | 110 | 111 | @pytest.fixture 112 | def cmd_opts(request): 113 | reactor = request.config.getoption("reactor", "default") 114 | return ( 115 | sys.executable, 116 | "-m", 117 | "pytest", 118 | "-v", 119 | "--reactor={}".format(reactor), 120 | ) 121 | 122 | 123 | def test_inline_callbacks_in_pytest(): 124 | assert hasattr(pytest, 'inlineCallbacks') 125 | 126 | 127 | @pytest.mark.parametrize( 128 | 'decorator, should_warn', 129 | ( 130 | ('pytest.inlineCallbacks', True), 131 | ('pytest_twisted.inlineCallbacks', False), 132 | ), 133 | ) 134 | def test_inline_callbacks_in_pytest_deprecation( 135 | testdir, 136 | cmd_opts, 137 | decorator, 138 | should_warn, 139 | ): 140 | import_path, _, _ = decorator.rpartition('.') 141 | test_file = """ 142 | import {import_path} 143 | 144 | def test_deprecation(): 145 | @{decorator} 146 | def f(): 147 | yield 42 148 | """.format(import_path=import_path, decorator=decorator) 149 | testdir.makepyfile(test_file) 150 | rr = testdir.run(*cmd_opts, timeout=timeout) 151 | 152 | expected_outcomes = {"passed": 1} 153 | if should_warn: 154 | expected_outcomes["warnings"] = 1 155 | 156 | assert_outcomes(rr, expected_outcomes) 157 | 158 | 159 | def test_blockon_in_pytest(): 160 | assert hasattr(pytest, 'blockon') 161 | 162 | 163 | @pytest.mark.parametrize( 164 | 'function, should_warn', 165 | ( 166 | ('pytest.blockon', True), 167 | ('pytest_twisted.blockon', False), 168 | ), 169 | ) 170 | def test_blockon_in_pytest_deprecation( 171 | testdir, 172 | cmd_opts, 173 | function, 174 | should_warn, 175 | ): 176 | import_path, _, _ = function.rpartition('.') 177 | test_file = """ 178 | import warnings 179 | 180 | from twisted.internet import reactor, defer 181 | import pytest 182 | import {import_path} 183 | 184 | @pytest.fixture 185 | def foo(request): 186 | d = defer.Deferred() 187 | d.callback(None) 188 | {function}(d) 189 | 190 | def test_succeed(foo): 191 | pass 192 | """.format(import_path=import_path, function=function) 193 | testdir.makepyfile(test_file) 194 | rr = testdir.run(*cmd_opts, timeout=timeout) 195 | 196 | expected_outcomes = {"passed": 1} 197 | if should_warn: 198 | expected_outcomes["warnings"] = 1 199 | 200 | assert_outcomes(rr, expected_outcomes) 201 | 202 | 203 | def test_fail_later(testdir, cmd_opts): 204 | test_file = """ 205 | from twisted.internet import reactor, defer 206 | 207 | def test_fail(): 208 | def doit(): 209 | try: 210 | 1 / 0 211 | except: 212 | d.errback() 213 | 214 | d = defer.Deferred() 215 | reactor.callLater(0.01, doit) 216 | return d 217 | """ 218 | testdir.makepyfile(test_file) 219 | rr = testdir.run(*cmd_opts, timeout=timeout) 220 | assert_outcomes(rr, {"failed": 1}) 221 | 222 | 223 | def test_succeed_later(testdir, cmd_opts): 224 | test_file = """ 225 | from twisted.internet import reactor, defer 226 | 227 | def test_succeed(): 228 | d = defer.Deferred() 229 | reactor.callLater(0.01, d.callback, 1) 230 | return d 231 | """ 232 | testdir.makepyfile(test_file) 233 | rr = testdir.run(*cmd_opts, timeout=timeout) 234 | assert_outcomes(rr, {"passed": 1}) 235 | 236 | 237 | def test_non_deferred(testdir, cmd_opts): 238 | test_file = """ 239 | from twisted.internet import reactor, defer 240 | 241 | def test_succeed(): 242 | return 42 243 | """ 244 | testdir.makepyfile(test_file) 245 | rr = testdir.run(*cmd_opts, timeout=timeout) 246 | assert_outcomes(rr, {"passed": 1}) 247 | 248 | 249 | def test_exception(testdir, cmd_opts): 250 | test_file = """ 251 | def test_more_fail(): 252 | raise RuntimeError("foo") 253 | """ 254 | testdir.makepyfile(test_file) 255 | rr = testdir.run(*cmd_opts, timeout=timeout) 256 | assert_outcomes(rr, {"failed": 1}) 257 | 258 | 259 | @pytest.fixture( 260 | name="empty_optional_call", 261 | params=["", "()"], 262 | ids=["no call", "empty call"], 263 | ) 264 | def empty_optional_call_fixture(request): 265 | return request.param 266 | 267 | 268 | def test_inlineCallbacks(testdir, cmd_opts, empty_optional_call): 269 | test_file = """ 270 | from twisted.internet import reactor, defer 271 | import pytest 272 | import pytest_twisted 273 | 274 | @pytest.fixture(scope="module", params=["fs", "imap", "web"]) 275 | def foo(request): 276 | return request.param 277 | 278 | @pytest_twisted.inlineCallbacks{optional_call} 279 | def test_succeed(foo): 280 | yield defer.succeed(foo) 281 | if foo == "web": 282 | raise RuntimeError("baz") 283 | """.format(optional_call=empty_optional_call) 284 | testdir.makepyfile(test_file) 285 | rr = testdir.run(*cmd_opts, timeout=timeout) 286 | assert_outcomes(rr, {"passed": 2, "failed": 1}) 287 | 288 | 289 | @skip_if_no_async_await() 290 | def test_async_await(testdir, cmd_opts, empty_optional_call): 291 | test_file = """ 292 | from twisted.internet import reactor, defer 293 | import pytest 294 | import pytest_twisted 295 | 296 | @pytest.fixture(scope="module", params=["fs", "imap", "web"]) 297 | def foo(request): 298 | return request.param 299 | 300 | @pytest_twisted.ensureDeferred{optional_call} 301 | async def test_succeed(foo): 302 | await defer.succeed(foo) 303 | if foo == "web": 304 | raise RuntimeError("baz") 305 | """.format(optional_call=empty_optional_call) 306 | testdir.makepyfile(test_file) 307 | rr = testdir.run(*cmd_opts, timeout=timeout) 308 | assert_outcomes(rr, {"passed": 2, "failed": 1}) 309 | 310 | 311 | def test_twisted_greenlet(testdir, cmd_opts): 312 | test_file = """ 313 | import pytest, greenlet 314 | 315 | MAIN = None 316 | 317 | @pytest.fixture(scope="session", autouse=True) 318 | def set_MAIN(request, twisted_greenlet): 319 | global MAIN 320 | MAIN = twisted_greenlet 321 | 322 | def test_MAIN(): 323 | assert MAIN is not None 324 | assert MAIN is greenlet.getcurrent() 325 | """ 326 | testdir.makepyfile(test_file) 327 | rr = testdir.run(*cmd_opts, timeout=timeout) 328 | assert_outcomes(rr, {"passed": 1}) 329 | 330 | 331 | def test_blockon_in_fixture(testdir, cmd_opts): 332 | test_file = """ 333 | from twisted.internet import reactor, defer 334 | import pytest 335 | import pytest_twisted 336 | 337 | @pytest.fixture(scope="module", params=["fs", "imap", "web"]) 338 | def foo(request): 339 | d1, d2 = defer.Deferred(), defer.Deferred() 340 | reactor.callLater(0.01, d1.callback, 1) 341 | reactor.callLater(0.02, d2.callback, request.param) 342 | pytest_twisted.blockon(d1) 343 | return d2 344 | 345 | @pytest_twisted.inlineCallbacks 346 | def test_succeed(foo): 347 | x = yield foo 348 | if x == "web": 349 | raise RuntimeError("baz") 350 | """ 351 | testdir.makepyfile(test_file) 352 | rr = testdir.run(*cmd_opts, timeout=timeout) 353 | assert_outcomes(rr, {"passed": 2, "failed": 1}) 354 | 355 | 356 | @skip_if_no_async_await() 357 | def test_blockon_in_fixture_async(testdir, cmd_opts): 358 | test_file = """ 359 | from twisted.internet import reactor, defer 360 | import pytest 361 | import pytest_twisted 362 | 363 | @pytest.fixture(scope="module", params=["fs", "imap", "web"]) 364 | def foo(request): 365 | d1, d2 = defer.Deferred(), defer.Deferred() 366 | reactor.callLater(0.01, d1.callback, 1) 367 | reactor.callLater(0.02, d2.callback, request.param) 368 | pytest_twisted.blockon(d1) 369 | return d2 370 | 371 | @pytest_twisted.ensureDeferred 372 | async def test_succeed(foo): 373 | x = await foo 374 | if x == "web": 375 | raise RuntimeError("baz") 376 | """ 377 | testdir.makepyfile(test_file) 378 | rr = testdir.run(*cmd_opts, timeout=timeout) 379 | assert_outcomes(rr, {"passed": 2, "failed": 1}) 380 | 381 | 382 | @skip_if_no_async_await() 383 | def test_async_fixture(testdir, cmd_opts): 384 | pytest_ini_file = """ 385 | [pytest] 386 | markers = 387 | redgreenblue 388 | """ 389 | testdir.makefile('.ini', pytest=pytest_ini_file) 390 | test_file = """ 391 | from twisted.internet import reactor, defer 392 | import pytest 393 | import pytest_twisted 394 | 395 | @pytest_twisted.async_fixture( 396 | scope="function", 397 | params=["fs", "imap", "web"], 398 | ) 399 | async def foo(request): 400 | d1, d2 = defer.Deferred(), defer.Deferred() 401 | reactor.callLater(0.01, d1.callback, 1) 402 | reactor.callLater(0.02, d2.callback, request.param) 403 | await d1 404 | return d2, 405 | 406 | @pytest_twisted.inlineCallbacks 407 | def test_succeed_blue(foo): 408 | x = yield foo[0] 409 | if x == "web": 410 | raise RuntimeError("baz") 411 | """ 412 | testdir.makepyfile(test_file) 413 | rr = testdir.run(*cmd_opts, timeout=timeout) 414 | assert_outcomes(rr, {"passed": 2, "failed": 1}) 415 | 416 | 417 | @skip_if_no_async_await() 418 | def test_async_fixture_no_arguments(testdir, cmd_opts, empty_optional_call): 419 | test_file = """ 420 | from twisted.internet import reactor, defer 421 | import pytest 422 | import pytest_twisted 423 | 424 | @pytest_twisted.async_fixture{optional_call} 425 | async def scope(request): 426 | return request.scope 427 | 428 | def test_is_function_scope(scope): 429 | assert scope == "function" 430 | """.format(optional_call=empty_optional_call) 431 | testdir.makepyfile(test_file) 432 | rr = testdir.run(*cmd_opts, timeout=timeout) 433 | assert_outcomes(rr, {"passed": 1}) 434 | 435 | 436 | @skip_if_no_async_generators() 437 | def test_async_yield_fixture_ordered_teardown(testdir, cmd_opts): 438 | test_file = """ 439 | from twisted.internet import reactor, defer 440 | import pytest 441 | import pytest_twisted 442 | 443 | 444 | results = [] 445 | 446 | @pytest.fixture(scope='function') 447 | def sync_fixture(): 448 | yield 42 449 | results.append(2) 450 | 451 | @pytest_twisted.async_yield_fixture(scope='function') 452 | async def async_fixture(sync_fixture): 453 | yield sync_fixture 454 | results.append(1) 455 | 456 | def test_first(async_fixture): 457 | assert async_fixture == 42 458 | 459 | def test_second(): 460 | assert results == [1, 2] 461 | """ 462 | testdir.makepyfile(test_file) 463 | rr = testdir.run(*cmd_opts, timeout=timeout) 464 | assert_outcomes(rr, {"passed": 2}) 465 | 466 | 467 | @skip_if_no_async_generators() 468 | def test_async_yield_fixture_can_await(testdir, cmd_opts): 469 | test_file = """ 470 | from twisted.internet import reactor, defer 471 | import pytest_twisted 472 | 473 | @pytest_twisted.async_yield_fixture() 474 | async def foo(): 475 | d1, d2 = defer.Deferred(), defer.Deferred() 476 | reactor.callLater(0.01, d1.callback, 1) 477 | reactor.callLater(0.02, d2.callback, 2) 478 | await d1 479 | 480 | # Twisted doesn't allow calling back with a Deferred as a value. 481 | # This deferred is being wrapped up in a tuple to sneak through. 482 | # https://github.com/twisted/twisted/blob/c0f1394c7bfb04d97c725a353a1f678fa6a1c602/src/twisted/internet/defer.py#L459 483 | yield d2, 484 | 485 | @pytest_twisted.ensureDeferred 486 | async def test(foo): 487 | x = await foo[0] 488 | assert x == 2 489 | """ 490 | testdir.makepyfile(test_file) 491 | rr = testdir.run(*cmd_opts, timeout=timeout) 492 | assert_outcomes(rr, {"passed": 1}) 493 | 494 | 495 | @skip_if_no_async_generators() 496 | def test_async_yield_fixture_failed_test(testdir, cmd_opts): 497 | test_file = """ 498 | import pytest_twisted 499 | 500 | @pytest_twisted.async_yield_fixture() 501 | async def foo(): 502 | yield 92 503 | 504 | @pytest_twisted.ensureDeferred 505 | async def test(foo): 506 | assert False 507 | """ 508 | testdir.makepyfile(test_file) 509 | rr = testdir.run(*cmd_opts, timeout=timeout) 510 | rr.stdout.fnmatch_lines(lines2=["E*assert False"]) 511 | assert_outcomes(rr, {"failed": 1}) 512 | 513 | 514 | @skip_if_no_async_generators() 515 | def test_async_yield_fixture_test_exception(testdir, cmd_opts): 516 | test_file = """ 517 | import pytest_twisted 518 | 519 | class UniqueLocalException(Exception): 520 | pass 521 | 522 | @pytest_twisted.async_yield_fixture() 523 | async def foo(): 524 | yield 92 525 | 526 | @pytest_twisted.ensureDeferred 527 | async def test(foo): 528 | raise UniqueLocalException("some message") 529 | """ 530 | testdir.makepyfile(test_file) 531 | rr = testdir.run(*cmd_opts, timeout=timeout) 532 | rr.stdout.fnmatch_lines(lines2=["E*.UniqueLocalException: some message*"]) 533 | assert_outcomes(rr, {"failed": 1}) 534 | 535 | 536 | @skip_if_no_async_generators() 537 | def test_async_yield_fixture_yields_twice(testdir, cmd_opts): 538 | test_file = """ 539 | import pytest_twisted 540 | 541 | @pytest_twisted.async_yield_fixture() 542 | async def foo(): 543 | yield 92 544 | yield 36 545 | 546 | @pytest_twisted.ensureDeferred 547 | async def test(foo): 548 | assert foo == 92 549 | """ 550 | testdir.makepyfile(test_file) 551 | rr = testdir.run(*cmd_opts, timeout=timeout) 552 | assert_outcomes(rr, {"passed": 1, "errors": 1}) 553 | 554 | 555 | @skip_if_no_async_generators() 556 | def test_async_yield_fixture_teardown_exception(testdir, cmd_opts): 557 | test_file = """ 558 | from twisted.internet import reactor, defer 559 | import pytest 560 | import pytest_twisted 561 | 562 | class UniqueLocalException(Exception): 563 | pass 564 | 565 | @pytest_twisted.async_yield_fixture() 566 | async def foo(request): 567 | yield 13 568 | 569 | raise UniqueLocalException("some message") 570 | 571 | @pytest_twisted.ensureDeferred 572 | async def test_succeed(foo): 573 | assert foo == 13 574 | """ 575 | 576 | testdir.makepyfile(test_file) 577 | rr = testdir.run(*cmd_opts, timeout=timeout) 578 | rr.stdout.fnmatch_lines(lines2=["E*.UniqueLocalException: some message*"]) 579 | assert_outcomes(rr, {"passed": 1, "errors": 1}) 580 | 581 | 582 | @skip_if_no_async_generators() 583 | def test_async_yield_fixture_no_arguments( 584 | testdir, 585 | cmd_opts, 586 | empty_optional_call, 587 | ): 588 | test_file = """ 589 | from twisted.internet import reactor, defer 590 | import pytest 591 | import pytest_twisted 592 | 593 | @pytest_twisted.async_yield_fixture{optional_call} 594 | async def scope(request): 595 | yield request.scope 596 | 597 | def test_is_function_scope(scope): 598 | assert scope == "function" 599 | """.format(optional_call=empty_optional_call) 600 | testdir.makepyfile(test_file) 601 | rr = testdir.run(*cmd_opts, timeout=timeout) 602 | assert_outcomes(rr, {"passed": 1}) 603 | 604 | 605 | @skip_if_no_async_generators() 606 | def test_async_yield_fixture_function_scope(testdir, cmd_opts): 607 | test_file = """ 608 | from twisted.internet import reactor, defer 609 | import pytest 610 | import pytest_twisted 611 | 612 | check_me = 0 613 | 614 | @pytest_twisted.async_yield_fixture(scope="function") 615 | async def foo(): 616 | global check_me 617 | 618 | if check_me != 0: 619 | raise Exception('check_me already modified before fixture run') 620 | 621 | check_me = 1 622 | 623 | yield 42 624 | 625 | if check_me != 2: 626 | raise Exception( 627 | 'check_me not updated properly: {}'.format(check_me), 628 | ) 629 | 630 | check_me = 0 631 | 632 | def test_first(foo): 633 | global check_me 634 | 635 | assert check_me == 1 636 | assert foo == 42 637 | 638 | check_me = 2 639 | 640 | def test_second(foo): 641 | global check_me 642 | 643 | assert check_me == 1 644 | assert foo == 42 645 | 646 | check_me = 2 647 | """ 648 | testdir.makepyfile(test_file) 649 | rr = testdir.run(*cmd_opts, timeout=timeout) 650 | assert_outcomes(rr, {"passed": 2}) 651 | 652 | 653 | @skip_if_no_async_await() 654 | def test_async_simple_fixture_in_fixture(testdir, cmd_opts): 655 | test_file = """ 656 | import itertools 657 | from twisted.internet import reactor, defer 658 | import pytest 659 | import pytest_twisted 660 | 661 | @pytest_twisted.async_fixture(name='four') 662 | async def fixture_four(): 663 | return 4 664 | 665 | @pytest_twisted.async_fixture(name='doublefour') 666 | async def fixture_doublefour(four): 667 | return 2 * four 668 | 669 | @pytest_twisted.ensureDeferred 670 | async def test_four(four): 671 | assert four == 4 672 | 673 | @pytest_twisted.ensureDeferred 674 | async def test_doublefour(doublefour): 675 | assert doublefour == 8 676 | """ 677 | testdir.makepyfile(test_file) 678 | rr = testdir.run(*cmd_opts, timeout=timeout) 679 | assert_outcomes(rr, {"passed": 2}) 680 | 681 | 682 | @skip_if_no_async_generators() 683 | def test_async_yield_simple_fixture_in_fixture(testdir, cmd_opts): 684 | test_file = """ 685 | import itertools 686 | from twisted.internet import reactor, defer 687 | import pytest 688 | import pytest_twisted 689 | 690 | @pytest_twisted.async_yield_fixture(name='four') 691 | async def fixture_four(): 692 | yield 4 693 | 694 | @pytest_twisted.async_yield_fixture(name='doublefour') 695 | async def fixture_doublefour(four): 696 | yield 2 * four 697 | 698 | @pytest_twisted.ensureDeferred 699 | async def test_four(four): 700 | assert four == 4 701 | 702 | @pytest_twisted.ensureDeferred 703 | async def test_doublefour(doublefour): 704 | assert doublefour == 8 705 | """ 706 | testdir.makepyfile(test_file) 707 | rr = testdir.run(*cmd_opts, timeout=timeout) 708 | assert_outcomes(rr, {"passed": 2}) 709 | 710 | 711 | @skip_if_no_async_await() 712 | @pytest.mark.parametrize('innerasync', [ 713 | pytest.param(truth, id='innerasync={}'.format(truth)) 714 | for truth in [True, False] 715 | ]) 716 | def test_async_fixture_in_fixture(testdir, cmd_opts, innerasync): 717 | maybe_async = 'async ' if innerasync else '' 718 | maybe_await = 'await ' if innerasync else '' 719 | test_file = """ 720 | import itertools 721 | from twisted.internet import reactor, defer 722 | import pytest 723 | import pytest_twisted 724 | 725 | @pytest_twisted.async_fixture(name='increment') 726 | async def fixture_increment(): 727 | counts = itertools.count() 728 | {maybe_async}def increment(): 729 | return next(counts) 730 | 731 | return increment 732 | 733 | @pytest_twisted.async_fixture(name='doubleincrement') 734 | async def fixture_doubleincrement(increment): 735 | {maybe_async}def doubleincrement(): 736 | n = {maybe_await}increment() 737 | return n * 2 738 | 739 | return doubleincrement 740 | 741 | @pytest_twisted.ensureDeferred 742 | async def test_increment(increment): 743 | first = {maybe_await}increment() 744 | second = {maybe_await}increment() 745 | assert (first, second) == (0, 1) 746 | 747 | @pytest_twisted.ensureDeferred 748 | async def test_doubleincrement(doubleincrement): 749 | first = {maybe_await}doubleincrement() 750 | second = {maybe_await}doubleincrement() 751 | assert (first, second) == (0, 2) 752 | """.format(maybe_async=maybe_async, maybe_await=maybe_await) 753 | testdir.makepyfile(test_file) 754 | rr = testdir.run(*cmd_opts, timeout=timeout) 755 | assert_outcomes(rr, {"passed": 2}) 756 | # assert_outcomes(rr, {"passed": 1}) 757 | 758 | 759 | @skip_if_no_async_generators() 760 | @pytest.mark.parametrize('innerasync', [ 761 | pytest.param(truth, id='innerasync={}'.format(truth)) 762 | for truth in [True, False] 763 | ]) 764 | def test_async_yield_fixture_in_fixture(testdir, cmd_opts, innerasync): 765 | maybe_async = 'async ' if innerasync else '' 766 | maybe_await = 'await ' if innerasync else '' 767 | test_file = """ 768 | import itertools 769 | from twisted.internet import reactor, defer 770 | import pytest 771 | import pytest_twisted 772 | 773 | @pytest_twisted.async_yield_fixture(name='increment') 774 | async def fixture_increment(): 775 | counts = itertools.count() 776 | {maybe_async}def increment(): 777 | return next(counts) 778 | 779 | yield increment 780 | 781 | @pytest_twisted.async_yield_fixture(name='doubleincrement') 782 | async def fixture_doubleincrement(increment): 783 | {maybe_async}def doubleincrement(): 784 | n = {maybe_await}increment() 785 | return n * 2 786 | 787 | yield doubleincrement 788 | 789 | @pytest_twisted.ensureDeferred 790 | async def test_increment(increment): 791 | first = {maybe_await}increment() 792 | second = {maybe_await}increment() 793 | assert (first, second) == (0, 1) 794 | 795 | @pytest_twisted.ensureDeferred 796 | async def test_doubleincrement(doubleincrement): 797 | first = {maybe_await}doubleincrement() 798 | second = {maybe_await}doubleincrement() 799 | assert (first, second) == (0, 2) 800 | """.format(maybe_async=maybe_async, maybe_await=maybe_await) 801 | testdir.makepyfile(test_file) 802 | rr = testdir.run(*cmd_opts, timeout=timeout) 803 | assert_outcomes(rr, {"passed": 2}) 804 | 805 | 806 | def test_blockon_in_hook(testdir, cmd_opts, request): 807 | skip_if_reactor_not(request, "default") 808 | conftest_file = """ 809 | import pytest_twisted 810 | from twisted.internet import reactor, defer 811 | 812 | def pytest_configure(config): 813 | pytest_twisted.init_default_reactor() 814 | d1, d2 = defer.Deferred(), defer.Deferred() 815 | reactor.callLater(0.01, d1.callback, 1) 816 | reactor.callLater(0.02, d2.callback, 1) 817 | pytest_twisted.blockon(d1) 818 | pytest_twisted.blockon(d2) 819 | """ 820 | testdir.makeconftest(conftest_file) 821 | test_file = """ 822 | from twisted.internet import reactor, defer 823 | 824 | def test_succeed(): 825 | d = defer.Deferred() 826 | reactor.callLater(0.01, d.callback, 1) 827 | return d 828 | """ 829 | testdir.makepyfile(test_file) 830 | rr = testdir.run(*cmd_opts, timeout=timeout) 831 | assert_outcomes(rr, {"passed": 1}) 832 | 833 | 834 | def test_wrong_reactor(testdir, cmd_opts, request): 835 | skip_if_reactor_not(request, "default") 836 | conftest_file = """ 837 | def pytest_addhooks(): 838 | import twisted.internet.reactor 839 | twisted.internet.reactor = None 840 | """ 841 | testdir.makeconftest(conftest_file) 842 | test_file = """ 843 | def test_succeed(): 844 | pass 845 | """ 846 | testdir.makepyfile(test_file) 847 | rr = testdir.run(*cmd_opts, timeout=timeout) 848 | assert "WrongReactorAlreadyInstalledError" in rr.stderr.str() 849 | 850 | 851 | def test_blockon_in_hook_with_qt5reactor(testdir, cmd_opts, request): 852 | skip_if_reactor_not(request, "qt5reactor") 853 | conftest_file = """ 854 | import pytest_twisted 855 | import pytestqt 856 | from twisted.internet import defer 857 | 858 | def pytest_configure(config): 859 | pytest_twisted.init_qt5_reactor() 860 | d = defer.Deferred() 861 | 862 | from twisted.internet import reactor 863 | 864 | reactor.callLater(0.01, d.callback, 1) 865 | pytest_twisted.blockon(d) 866 | """ 867 | testdir.makeconftest(conftest_file) 868 | test_file = """ 869 | from twisted.internet import reactor, defer 870 | 871 | def test_succeed(): 872 | d = defer.Deferred() 873 | reactor.callLater(0.01, d.callback, 1) 874 | return d 875 | """ 876 | testdir.makepyfile(test_file) 877 | rr = testdir.run(*cmd_opts, timeout=timeout) 878 | assert_outcomes(rr, {"passed": 1}) 879 | 880 | 881 | def test_wrong_reactor_with_qt5reactor(testdir, cmd_opts, request): 882 | skip_if_reactor_not(request, "qt5reactor") 883 | conftest_file = """ 884 | def pytest_addhooks(): 885 | import twisted.internet.default 886 | twisted.internet.default.install() 887 | """ 888 | testdir.makeconftest(conftest_file) 889 | test_file = """ 890 | def test_succeed(): 891 | pass 892 | """ 893 | testdir.makepyfile(test_file) 894 | rr = testdir.run(*cmd_opts, timeout=timeout) 895 | assert "WrongReactorAlreadyInstalledError" in rr.stderr.str() 896 | 897 | 898 | def test_pytest_from_reactor_thread(testdir, cmd_opts, request): 899 | skip_if_reactor_not(request, "default") 900 | test_file = """ 901 | import pytest 902 | import pytest_twisted 903 | from twisted.internet import reactor, defer 904 | 905 | @pytest.fixture 906 | def fix(): 907 | d = defer.Deferred() 908 | reactor.callLater(0.01, d.callback, 42) 909 | return pytest_twisted.blockon(d) 910 | 911 | def test_simple(fix): 912 | assert fix == 42 913 | 914 | @pytest_twisted.inlineCallbacks 915 | def test_fail(): 916 | d = defer.Deferred() 917 | reactor.callLater(0.01, d.callback, 1) 918 | yield d 919 | assert False 920 | """ 921 | testdir.makepyfile(test_file) 922 | runner_file = """ 923 | import pytest 924 | 925 | from twisted.internet import reactor 926 | from twisted.internet.defer import inlineCallbacks 927 | from twisted.internet.threads import deferToThread 928 | 929 | codes = [] 930 | 931 | @inlineCallbacks 932 | def main(): 933 | try: 934 | codes.append((yield deferToThread(pytest.main, ['-k simple']))) 935 | codes.append((yield deferToThread(pytest.main, ['-k fail']))) 936 | finally: 937 | reactor.stop() 938 | 939 | if __name__ == '__main__': 940 | reactor.callLater(0, main) 941 | reactor.run() 942 | codes == [0, 1] or exit(1) 943 | """ 944 | testdir.makepyfile(runner=runner_file) 945 | # check test file is ok in standalone mode: 946 | rr = testdir.run(*cmd_opts, timeout=timeout) 947 | assert_outcomes(rr, {"passed": 1, "failed": 1}) 948 | # test embedded mode: 949 | assert testdir.run(sys.executable, "runner.py", timeout=timeout).ret == 0 950 | 951 | 952 | def test_blockon_in_hook_with_asyncio(testdir, cmd_opts, request): 953 | skip_if_reactor_not(request, "asyncio") 954 | conftest_file = """ 955 | import pytest 956 | import pytest_twisted 957 | from twisted.internet import defer 958 | 959 | @pytest.hookimpl(tryfirst=True) 960 | def pytest_configure(config): 961 | pytest_twisted._use_asyncio_selector_if_required(config=config) 962 | 963 | pytest_twisted.init_asyncio_reactor() 964 | d = defer.Deferred() 965 | 966 | from twisted.internet import reactor 967 | 968 | reactor.callLater(0.01, d.callback, 1) 969 | pytest_twisted.blockon(d) 970 | """ 971 | testdir.makeconftest(conftest_file) 972 | test_file = """ 973 | from twisted.internet import reactor, defer 974 | 975 | def test_succeed(): 976 | d = defer.Deferred() 977 | reactor.callLater(0.01, d.callback, 1) 978 | return d 979 | """ 980 | testdir.makepyfile(test_file) 981 | rr = testdir.run(*cmd_opts, timeout=timeout) 982 | assert_outcomes(rr, {"passed": 1}) 983 | 984 | 985 | def test_wrong_reactor_with_asyncio(testdir, cmd_opts, request): 986 | skip_if_reactor_not(request, "asyncio") 987 | conftest_file = """ 988 | import pytest 989 | import pytest_twisted 990 | 991 | 992 | @pytest.hookimpl(tryfirst=True) 993 | def pytest_configure(config): 994 | pytest_twisted._use_asyncio_selector_if_required(config=config) 995 | 996 | def pytest_addhooks(): 997 | import twisted.internet.default 998 | twisted.internet.default.install() 999 | """ 1000 | testdir.makeconftest(conftest_file) 1001 | test_file = """ 1002 | def test_succeed(): 1003 | pass 1004 | """ 1005 | testdir.makepyfile(test_file) 1006 | rr = testdir.run(*cmd_opts, timeout=timeout) 1007 | assert "WrongReactorAlreadyInstalledError" in rr.stderr.str() 1008 | 1009 | 1010 | @skip_if_no_async_generators() 1011 | def test_async_fixture_module_scope(testdir, cmd_opts): 1012 | test_file = """ 1013 | from twisted.internet import reactor, defer 1014 | import pytest 1015 | import pytest_twisted 1016 | 1017 | check_me = 0 1018 | 1019 | @pytest_twisted.async_yield_fixture(scope="module") 1020 | async def foo(): 1021 | global check_me 1022 | 1023 | if check_me != 0: 1024 | raise Exception('check_me already modified before fixture run') 1025 | 1026 | check_me = 1 1027 | 1028 | yield 42 1029 | 1030 | if check_me != 3: 1031 | raise Exception( 1032 | 'check_me not updated properly: {}'.format(check_me), 1033 | ) 1034 | 1035 | check_me = 0 1036 | 1037 | def test_first(foo): 1038 | global check_me 1039 | 1040 | assert check_me == 1 1041 | assert foo == 42 1042 | 1043 | check_me = 2 1044 | 1045 | def test_second(foo): 1046 | global check_me 1047 | 1048 | assert check_me == 2 1049 | assert foo == 42 1050 | 1051 | check_me = 3 1052 | """ 1053 | testdir.makepyfile(test_file) 1054 | rr = testdir.run(*cmd_opts, timeout=timeout) 1055 | assert_outcomes(rr, {"passed": 2}) 1056 | 1057 | 1058 | def test_inlinecallbacks_method_with_fixture_gets_self(testdir, cmd_opts): 1059 | test_file = """ 1060 | import pytest 1061 | import pytest_twisted 1062 | from twisted.internet import defer 1063 | 1064 | @pytest.fixture 1065 | def foo(): 1066 | return 37 1067 | 1068 | class TestClass: 1069 | @pytest_twisted.inlineCallbacks 1070 | def test_self_isinstance(self, foo): 1071 | d = defer.succeed(None) 1072 | yield d 1073 | assert isinstance(self, TestClass) 1074 | """ 1075 | testdir.makepyfile(test_file) 1076 | rr = testdir.run(*cmd_opts) 1077 | assert_outcomes(rr, {"passed": 1}) 1078 | 1079 | 1080 | def test_inlinecallbacks_method_with_fixture_gets_fixture(testdir, cmd_opts): 1081 | test_file = """ 1082 | import pytest 1083 | import pytest_twisted 1084 | from twisted.internet import defer 1085 | 1086 | @pytest.fixture 1087 | def foo(): 1088 | return 37 1089 | 1090 | class TestClass: 1091 | @pytest_twisted.inlineCallbacks 1092 | def test_self_isinstance(self, foo): 1093 | d = defer.succeed(None) 1094 | yield d 1095 | assert foo == 37 1096 | """ 1097 | testdir.makepyfile(test_file) 1098 | rr = testdir.run(*cmd_opts, timeout=timeout) 1099 | assert_outcomes(rr, {"passed": 1}) 1100 | 1101 | 1102 | @skip_if_no_async_await() 1103 | def test_ensuredeferred_method_with_fixture_gets_self(testdir, cmd_opts): 1104 | test_file = """ 1105 | import pytest 1106 | import pytest_twisted 1107 | 1108 | @pytest.fixture 1109 | def foo(): 1110 | return 37 1111 | 1112 | class TestClass: 1113 | @pytest_twisted.ensureDeferred 1114 | async def test_self_isinstance(self, foo): 1115 | assert isinstance(self, TestClass) 1116 | """ 1117 | testdir.makepyfile(test_file) 1118 | rr = testdir.run(*cmd_opts, timeout=timeout) 1119 | assert_outcomes(rr, {"passed": 1}) 1120 | 1121 | 1122 | @skip_if_no_async_await() 1123 | def test_ensuredeferred_method_with_fixture_gets_fixture(testdir, cmd_opts): 1124 | test_file = """ 1125 | import pytest 1126 | import pytest_twisted 1127 | 1128 | @pytest.fixture 1129 | def foo(): 1130 | return 37 1131 | 1132 | class TestClass: 1133 | @pytest_twisted.ensureDeferred 1134 | async def test_self_isinstance(self, foo): 1135 | assert foo == 37 1136 | """ 1137 | testdir.makepyfile(test_file) 1138 | rr = testdir.run(*cmd_opts, timeout=timeout) 1139 | assert_outcomes(rr, {"passed": 1}) 1140 | 1141 | 1142 | def test_import_pytest_twisted_in_conftest_py_not_a_problem(testdir, cmd_opts): 1143 | conftest_file = """ 1144 | import pytest 1145 | import pytest_twisted 1146 | 1147 | 1148 | @pytest.hookimpl(tryfirst=True) 1149 | def pytest_configure(config): 1150 | pytest_twisted._use_asyncio_selector_if_required(config=config) 1151 | """ 1152 | testdir.makeconftest(conftest_file) 1153 | test_file = """ 1154 | import pytest_twisted 1155 | 1156 | def test_succeed(): 1157 | pass 1158 | """ 1159 | testdir.makepyfile(test_file) 1160 | rr = testdir.run(*cmd_opts, timeout=timeout) 1161 | assert_outcomes(rr, {"passed": 1}) 1162 | 1163 | 1164 | @pytest.mark.parametrize(argnames="kill", argvalues=[False, True]) 1165 | @pytest.mark.parametrize(argnames="event", argvalues=["shutdown"]) 1166 | @pytest.mark.parametrize( 1167 | argnames="phase", 1168 | argvalues=["before", "during", "after"], 1169 | ) 1170 | def test_addSystemEventTrigger(testdir, cmd_opts, kill, event, phase): 1171 | is_win32 = sys.platform == "win32" 1172 | is_qt = os.environ.get("REACTOR", "").startswith("qt") 1173 | is_kill = kill 1174 | 1175 | if (is_win32 or is_qt) and is_kill: 1176 | pytest.xfail(reason="Needs handled on Windows and with qt5reactor.") 1177 | 1178 | test_string = "1kljgf90u0lkj13l4jjklsfdo89898y24hlkjalkjs38" 1179 | 1180 | test_file = """ 1181 | import os 1182 | import signal 1183 | 1184 | import pytest_twisted 1185 | 1186 | def output_stuff(): 1187 | print({test_string!r}) 1188 | 1189 | @pytest_twisted.inlineCallbacks 1190 | def test_succeed(): 1191 | from twisted.internet import reactor 1192 | reactor.addSystemEventTrigger({phase!r}, {event!r}, output_stuff) 1193 | 1194 | if {kill!r}: 1195 | os.kill(os.getpid(), signal.SIGINT) 1196 | 1197 | yield 1198 | """.format(kill=kill, event=event, phase=phase, test_string=test_string) 1199 | testdir.makepyfile(test_file) 1200 | rr = testdir.run(*cmd_opts, timeout=timeout) 1201 | rr.stdout.fnmatch_lines(lines2=[test_string]) 1202 | 1203 | 1204 | def test_sigint_for_regular_tests(testdir, cmd_opts): 1205 | test_file = """ 1206 | import os 1207 | import signal 1208 | import time 1209 | 1210 | import twisted.internet 1211 | import twisted.internet.task 1212 | 1213 | def test_self_cancel(): 1214 | os.kill(os.getpid(), signal.SIGINT) 1215 | time.sleep(10) 1216 | 1217 | def test_should_not_run(): 1218 | assert False 1219 | """ 1220 | testdir.makepyfile(test_file) 1221 | rr = testdir.run(*cmd_opts, timeout=timeout) 1222 | if sys.platform != "win32": 1223 | # on Windows pytest isn't even reporting the status, just stopping... 1224 | assert_outcomes(rr, {}) 1225 | rr.stdout.re_match_lines(lines2=[r".* no tests ran in .*"]) 1226 | 1227 | pattern = r".*test_should_not_run.*" 1228 | 1229 | if pytest_version >= (5, 3, 0): 1230 | rr.stdout.no_re_match_line(pat=pattern) 1231 | else: 1232 | assert re.match(pattern, rr.stdout.str()) is None 1233 | 1234 | 1235 | def test_sigint_for_inline_callbacks_tests(testdir, cmd_opts): 1236 | test_file = """ 1237 | import os 1238 | import signal 1239 | 1240 | import twisted.internet 1241 | import twisted.internet.task 1242 | 1243 | import pytest_twisted 1244 | 1245 | @pytest_twisted.inlineCallbacks 1246 | def test_self_cancel(): 1247 | os.kill(os.getpid(), signal.SIGINT) 1248 | yield twisted.internet.task.deferLater( 1249 | twisted.internet.reactor, 1250 | 9999, 1251 | lambda: None, 1252 | ) 1253 | 1254 | @pytest_twisted.inlineCallbacks 1255 | def test_should_not_run(): 1256 | assert False 1257 | yield 1258 | """ 1259 | testdir.makepyfile(test_file) 1260 | rr = testdir.run(*cmd_opts, timeout=timeout) 1261 | if sys.platform != "win32": 1262 | # on Windows pytest isn't even reporting the status, just stopping... 1263 | assert_outcomes(rr, {}) 1264 | rr.stdout.re_match_lines(lines2=[r".* no tests ran in .*"]) 1265 | 1266 | pattern = r".*test_should_not_run.*" 1267 | 1268 | if pytest_version >= (5, 3, 0): 1269 | rr.stdout.no_re_match_line(pat=pattern) 1270 | else: 1271 | assert re.match(pattern, rr.stdout.str()) is None 1272 | 1273 | 1274 | @skip_if_no_async_await() 1275 | @skip_if_hypothesis_unavailable() 1276 | def test_hypothesis_async_passes(testdir, cmd_opts): 1277 | test_file = """ 1278 | import hypothesis 1279 | import hypothesis.strategies 1280 | 1281 | import pytest_twisted 1282 | 1283 | @hypothesis.given(x=hypothesis.strategies.integers()) 1284 | @pytest_twisted.ensureDeferred 1285 | async def test_async(x): 1286 | assert isinstance(x, int) 1287 | """ 1288 | testdir.makepyfile(test_file) 1289 | rr = testdir.run(*cmd_opts, timeout=timeout) 1290 | assert_outcomes(rr, {"passed": 1}) 1291 | 1292 | 1293 | @skip_if_hypothesis_unavailable() 1294 | def test_hypothesis_inline_callbacks_passes(testdir, cmd_opts): 1295 | test_file = """ 1296 | import hypothesis 1297 | import hypothesis.strategies 1298 | 1299 | import pytest_twisted 1300 | 1301 | @hypothesis.given(x=hypothesis.strategies.integers()) 1302 | @pytest_twisted.inlineCallbacks 1303 | def test_inline_callbacks(x): 1304 | assert isinstance(x, int) 1305 | return 1306 | yield 1307 | """ 1308 | testdir.makepyfile(test_file) 1309 | rr = testdir.run(*cmd_opts, timeout=timeout) 1310 | assert_outcomes(rr, {"passed": 1}) 1311 | 1312 | 1313 | @skip_if_no_async_await() 1314 | @skip_if_hypothesis_unavailable() 1315 | def test_hypothesis_async_fails(testdir, cmd_opts): 1316 | test_file = """ 1317 | import hypothesis 1318 | import hypothesis.strategies 1319 | 1320 | import pytest_twisted 1321 | 1322 | @hypothesis.given(x=hypothesis.strategies.integers()) 1323 | @pytest_twisted.ensureDeferred 1324 | async def test_async(x): 1325 | assert isinstance(x, str) 1326 | """ 1327 | testdir.makepyfile(test_file) 1328 | rr = testdir.run(*cmd_opts, timeout=timeout) 1329 | assert_outcomes(rr, {"failed": 1}) 1330 | 1331 | 1332 | @skip_if_hypothesis_unavailable() 1333 | def test_hypothesis_inline_callbacks_fails(testdir, cmd_opts): 1334 | test_file = """ 1335 | import hypothesis 1336 | import hypothesis.strategies 1337 | 1338 | import pytest_twisted 1339 | 1340 | @hypothesis.given(x=hypothesis.strategies.integers()) 1341 | @pytest_twisted.inlineCallbacks 1342 | def test_inline_callbacks(x): 1343 | assert isinstance(x, str) 1344 | return 1345 | yield 1346 | """ 1347 | testdir.makepyfile(test_file) 1348 | rr = testdir.run(*cmd_opts, timeout=3 * timeout) 1349 | assert_outcomes(rr, {"failed": 1}) 1350 | --------------------------------------------------------------------------------