Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pytest async fixtures #2226

Merged
merged 8 commits into from
Sep 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ Will McGugan
Willem de Groot
Wilson Ong
Yannick Koechlin
Yannick Péroux
Yegor Roganov
Young-Ho Cha
Yuriy Shatrov
Expand Down
7 changes: 7 additions & 0 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import cgi
import datetime
import functools
import inspect
import os
import re
import sys
Expand Down Expand Up @@ -237,6 +238,12 @@ def current_task(loop=None):
return task


def isasyncgenfunction(obj):
if hasattr(inspect, 'isasyncgenfunction'):
return inspect.isasyncgenfunction(obj)
return False


def parse_mimetype(mimetype):
"""Parses a MIME type into its components.

Expand Down
57 changes: 57 additions & 0 deletions aiohttp/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest
from py import path

from aiohttp.helpers import isasyncgenfunction
from aiohttp.web import Application

from .test_utils import unused_port as _unused_port
Expand Down Expand Up @@ -37,6 +38,62 @@ def pytest_addoption(parser):
help='enable event loop debug mode')


def pytest_fixture_setup(fixturedef, request):
"""
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 """
Expand Down
1 change: 1 addition & 0 deletions changes/2223.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Accept coroutine fixtures in pytest plugin
115 changes: 115 additions & 0 deletions tests/test_pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import pytest

from aiohttp.pytest_plugin import LOOP_FACTORIES


pytest_plugins = 'pytester'

Expand Down Expand Up @@ -180,3 +182,116 @@ async def test_bad():
stdout, _ = capsys.readouterr()
assert ("test_warning_checks.py:__LINE__:coroutine 'foobar' was "
"never awaited" in re.sub('\d{2,}', '__LINE__', stdout))


def test_aiohttp_plugin_async_fixture(testdir, capsys):
testdir.makepyfile("""\
import asyncio
import pytest

from aiohttp import web


pytest_plugins = 'aiohttp.pytest_plugin'


@asyncio.coroutine
def hello(request):
return web.Response(body=b'Hello, world')


def create_app(loop):
app = web.Application()
app.router.add_route('GET', '/', hello)
return app


@pytest.fixture
@asyncio.coroutine
def cli(test_client):
client = yield from test_client(create_app)
return client


@pytest.fixture
@asyncio.coroutine
def foo():
return 42


@pytest.fixture
@asyncio.coroutine
def bar(request):
# request should be accessible in async fixtures if needed
return request.function


@asyncio.coroutine
def test_hello(cli):
resp = yield from cli.get('/')
assert resp.status == 200


def test_foo(loop, foo):
assert foo == 42


def test_foo_without_loop(foo):
# will raise an error because there is no loop
pass


def test_bar(loop, bar):
assert bar is test_bar
""")
nb_loops = len(LOOP_FACTORIES)
result = testdir.runpytest('-p', 'no:sugar')
result.assert_outcomes(passed=3 * nb_loops, error=1)
result.stdout.fnmatch_lines(
"*Asynchronous fixtures must depend on the 'loop' fixture "
"or be used in tests depending from it."
)


@pytest.mark.skipif(sys.version_info < (3, 6), reason='old python')
def test_aiohttp_plugin_async_gen_fixture(testdir):
testdir.makepyfile("""\
import asyncio
import pytest
from unittest import mock

from aiohttp import web


pytest_plugins = 'aiohttp.pytest_plugin'

canary = mock.Mock()


async def hello(request):
return web.Response(body=b'Hello, world')


def create_app(loop):
app = web.Application()
app.router.add_route('GET', '/', hello)
return app


@pytest.fixture
async def cli(test_client):
yield await test_client(create_app)
canary()


async def test_hello(cli):
resp = await cli.get('/')
assert resp.status == 200


def test_finalized():
assert canary.called is True
""")
nb_loops = len(LOOP_FACTORIES)
result = testdir.runpytest('-p', 'no:sugar')
result.assert_outcomes(passed=1 * nb_loops + 1)