import asyncio import contextlib import warnings from collections.abc import Callable import pytest from aiohttp.helpers import isasyncgenfunction from aiohttp.web import Application from .test_utils import (BaseTestServer, RawTestServer, TestClient, TestServer, loop_context, setup_test_loop, teardown_test_loop) from .test_utils import unused_port as _unused_port try: import uvloop except ImportError: # pragma: no cover uvloop = None try: import tokio except ImportError: # pragma: no cover tokio = None def pytest_addoption(parser): parser.addoption( '--aiohttp-fast', action='store_true', default=False, help='run tests faster by disabling extra checks') parser.addoption( '--aiohttp-loop', action='store', default='pyloop', help='run tests with specific loop: pyloop, uvloop, tokio or all') parser.addoption( '--aiohttp-enable-loop-debug', action='store_true', default=False, help='enable event loop debug mode') def pytest_fixture_setup(fixturedef): """ Allow fixtures to be coroutines. Run coroutine fixtures in an event loop. """ func = fixturedef.func if isasyncgenfunction(func): # async generator fixture is_async_gen = True elif asyncio.iscoroutinefunction(func): # regular async fixture is_async_gen = False else: # not an async fixture, nothing to do return strip_request = False if 'request' not in fixturedef.argnames: fixturedef.argnames += ('request',) strip_request = True def wrapper(*args, **kwargs): request = kwargs['request'] if strip_request: del kwargs['request'] # if neither the fixture nor the test use the 'loop' fixture, # 'getfixturevalue' will fail because the test is not parameterized # (this can be removed someday if 'loop' is no longer parameterized) if 'loop' not in request.fixturenames: raise Exception( "Asynchronous fixtures must depend on the 'loop' fixture or " "be used in tests depending from it." ) _loop = request.getfixturevalue('loop') if is_async_gen: # for async generators, we need to advance the generator once, # then advance it again in a finalizer gen = func(*args, **kwargs) def finalizer(): try: return _loop.run_until_complete(gen.__anext__()) except StopAsyncIteration: # NOQA pass request.addfinalizer(finalizer) return _loop.run_until_complete(gen.__anext__()) else: return _loop.run_until_complete(func(*args, **kwargs)) fixturedef.func = wrapper @pytest.fixture def fast(request): """--fast config option""" return request.config.getoption('--aiohttp-fast') @pytest.fixture def loop_debug(request): """--enable-loop-debug config option""" return request.config.getoption('--aiohttp-enable-loop-debug') @contextlib.contextmanager def _runtime_warning_context(): """ Context manager which checks for RuntimeWarnings, specifically to avoid "coroutine 'X' was never awaited" warnings being missed. If RuntimeWarnings occur in the context a RuntimeError is raised. """ with warnings.catch_warnings(record=True) as _warnings: yield rw = ['{w.filename}:{w.lineno}:{w.message}'.format(w=w) for w in _warnings if w.category == RuntimeWarning] if rw: raise RuntimeError('{} Runtime Warning{},\n{}'.format( len(rw), '' if len(rw) == 1 else 's', '\n'.join(rw) )) @contextlib.contextmanager def _passthrough_loop_context(loop, fast=False): """ setups and tears down a loop unless one is passed in via the loop argument when it's passed straight through. """ if loop: # loop already exists, pass it straight through yield loop else: # this shadows loop_context's standard behavior loop = setup_test_loop() yield loop teardown_test_loop(loop, fast=fast) def pytest_pycollect_makeitem(collector, name, obj): """ Fix pytest collecting for coroutines. """ if collector.funcnamefilter(name) and asyncio.iscoroutinefunction(obj): return list(collector._genfunctions(name, obj)) def pytest_pyfunc_call(pyfuncitem): """ Run coroutines in an event loop instead of a normal function call. """ fast = pyfuncitem.config.getoption("--aiohttp-fast") if asyncio.iscoroutinefunction(pyfuncitem.function): existing_loop = pyfuncitem.funcargs.get('loop', None) with _runtime_warning_context(): with _passthrough_loop_context(existing_loop, fast=fast) as _loop: testargs = {arg: pyfuncitem.funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} _loop.run_until_complete(pyfuncitem.obj(**testargs)) return True def pytest_generate_tests(metafunc): if 'loop_factory' not in metafunc.fixturenames: return loops = metafunc.config.option.aiohttp_loop avail_factories = {'pyloop': asyncio.DefaultEventLoopPolicy} if uvloop is not None: # pragma: no cover avail_factories['uvloop'] = uvloop.EventLoopPolicy if tokio is not None: # pragma: no cover avail_factories['tokio'] = tokio.EventLoopPolicy if loops == 'all': loops = 'pyloop,uvloop?,tokio?' factories = {} for name in loops.split(','): required = not name.endswith('?') name = name.strip(' ?') if name not in avail_factories: # pragma: no cover if required: raise ValueError( "Unknown loop '%s', available loops: %s" % ( name, list(factories.keys()))) else: continue factories[name] = avail_factories[name] metafunc.parametrize("loop_factory", list(factories.values()), ids=list(factories.keys())) @pytest.fixture def loop(loop_factory, fast, loop_debug): """Return an instance of the event loop.""" policy = loop_factory() asyncio.set_event_loop_policy(policy) with loop_context(fast=fast) as _loop: if loop_debug: _loop.set_debug(True) # pragma: no cover asyncio.set_event_loop(_loop) yield _loop @pytest.fixture def unused_port(aiohttp_unused_port): # pragma: no cover warnings.warn("Deprecated, use aiohttp_unused_port fixture instead", DeprecationWarning) return aiohttp_unused_port @pytest.fixture def aiohttp_unused_port(): """Return a port that is unused on the current host.""" return _unused_port @pytest.fixture def aiohttp_server(loop): """Factory to create a TestServer instance, given an app. aiohttp_server(app, **kwargs) """ servers = [] async def go(app, *, port=None, **kwargs): server = TestServer(app, port=port) await server.start_server(loop=loop, **kwargs) servers.append(server) return server yield go async def finalize(): while servers: await servers.pop().close() loop.run_until_complete(finalize()) @pytest.fixture def test_server(aiohttp_server): # pragma: no cover warnings.warn("Deprecated, use aiohttp_server fixture instead", DeprecationWarning) return aiohttp_server @pytest.fixture def aiohttp_raw_server(loop): """Factory to create a RawTestServer instance, given a web handler. aiohttp_raw_server(handler, **kwargs) """ servers = [] async def go(handler, *, port=None, **kwargs): server = RawTestServer(handler, port=port) await server.start_server(loop=loop, **kwargs) servers.append(server) return server yield go async def finalize(): while servers: await servers.pop().close() loop.run_until_complete(finalize()) @pytest.fixture def raw_test_server(aiohttp_raw_server): # pragma: no cover warnings.warn("Deprecated, use aiohttp_raw_server fixture instead", DeprecationWarning) return aiohttp_raw_server @pytest.fixture def aiohttp_client(loop): """Factory to create a TestClient instance. aiohttp_client(app, **kwargs) aiohttp_client(server, **kwargs) aiohttp_client(raw_server, **kwargs) """ clients = [] async def go(__param, *args, server_kwargs=None, **kwargs): if (isinstance(__param, Callable) and not isinstance(__param, (Application, BaseTestServer))): __param = __param(loop, *args, **kwargs) kwargs = {} else: assert not args, "args should be empty" if isinstance(__param, Application): server_kwargs = server_kwargs or {} server = TestServer(__param, loop=loop, **server_kwargs) client = TestClient(server, loop=loop, **kwargs) elif isinstance(__param, BaseTestServer): client = TestClient(__param, loop=loop, **kwargs) else: raise ValueError("Unknown argument type: %r" % type(__param)) await client.start_server() clients.append(client) return client yield go async def finalize(): while clients: await clients.pop().close() loop.run_until_complete(finalize()) @pytest.fixture def test_client(aiohttp_client): # pragma: no cover warnings.warn("Deprecated, use aiohttp_client fixture instead", DeprecationWarning) return aiohttp_client