From cdd7af2af5c90b747473b0e6a1fcb6fb36f49d3c Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 13 Dec 2017 00:53:27 +0100 Subject: [PATCH 1/5] Heavy improvements, add async yield fixture, fix bugs, add tests etc. --- pytest_trio/_tests/test_async_fixture.py | 132 +++++++++++ .../_tests/test_async_yield_fixture.py | 207 ++++++++++++++++++ pytest_trio/_tests/test_basic.py | 74 ++++++- pytest_trio/_tests/test_clock_fixture.py | 14 ++ pytest_trio/_tests/test_fixture_scope.py | 42 ++++ pytest_trio/_tests/test_sync_fixture.py | 44 ++++ pytest_trio/plugin.py | 179 +++++++++++---- 7 files changed, 636 insertions(+), 56 deletions(-) create mode 100644 pytest_trio/_tests/test_async_fixture.py create mode 100644 pytest_trio/_tests/test_async_yield_fixture.py create mode 100644 pytest_trio/_tests/test_clock_fixture.py create mode 100644 pytest_trio/_tests/test_fixture_scope.py create mode 100644 pytest_trio/_tests/test_sync_fixture.py diff --git a/pytest_trio/_tests/test_async_fixture.py b/pytest_trio/_tests/test_async_fixture.py new file mode 100644 index 0000000..78674bc --- /dev/null +++ b/pytest_trio/_tests/test_async_fixture.py @@ -0,0 +1,132 @@ +import pytest + + +def test_single_async_fixture(testdir): + + testdir.makepyfile(""" + import pytest + import trio + + @pytest.fixture + async def fix1(): + await trio.sleep(0) + return 'fix1' + + @pytest.mark.trio + async def test_simple(fix1): + assert fix1 == 'fix1' + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=1) + + +def test_async_fixture_recomputed_for_each_test(testdir): + + testdir.makepyfile(""" + import pytest + import trio + + counter = 0 + + @pytest.fixture + async def fix1(): + global counter + await trio.sleep(0) + counter += 1 + return counter + + @pytest.mark.trio + async def test_first(fix1): + assert fix1 == 1 + + @pytest.mark.trio + async def test_second(fix1): + assert fix1 == 2 + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=2) + + +def test_nested_async_fixture(testdir): + + testdir.makepyfile(""" + import pytest + import trio + + @pytest.fixture + async def fix1(): + await trio.sleep(0) + return 'fix1' + + @pytest.fixture + async def fix2(fix1): + await trio.sleep(0) + return 'fix2(%s)' % fix1 + + @pytest.mark.trio + async def test_simple(fix2): + assert fix2 == 'fix2(fix1)' + + @pytest.mark.trio + async def test_both(fix1, fix2): + assert fix1 == 'fix1' + assert fix2 == 'fix2(fix1)' + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=2) + + +def test_async_within_sync_fixture(testdir): + + testdir.makepyfile(""" + import pytest + import trio + + @pytest.fixture + async def async_fix(): + await trio.sleep(0) + return 42 + + @pytest.fixture + def sync_fix(async_fix): + return async_fix + + @pytest.mark.trio + async def test_simple(sync_fix): + assert sync_fix == 42 + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=1) + + +# In pytest, ERROR status occurs when an exception is raised in fixture code. +# The trouble is our async fixtures must be run whithin a trio context, hence +# they are actually run just before the test, providing no way to make the +# difference between an exception comming from the real test or from an +# async fixture... +@pytest.mark.xfail(reason='Not implemented yet') +def test_raise_in_async_fixture_cause_pytest_error(testdir): + + testdir.makepyfile(""" + import pytest + + @pytest.fixture + async def fix1(): + raise ValueError('Ouch !') + + @pytest.mark.trio + async def test_base(fix1): + pass # Crash should have occures before arriving here + """) + + result = testdir.runpytest() + + result.assert_outcomes(error=1) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py new file mode 100644 index 0000000..e7bbb45 --- /dev/null +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -0,0 +1,207 @@ +def test_single_async_yield_fixture(testdir): + + testdir.makepyfile(""" + import pytest + import trio + + events = [] + + @pytest.fixture + async def fix1(): + events.append('fix1 setup') + await trio.sleep(0) + + yield 'fix1' + + await trio.sleep(0) + events.append('fix1 teardown') + + def test_before(): + assert not events + + @pytest.mark.trio + async def test_actual_test(fix1): + assert events == ['fix1 setup'] + assert fix1 == 'fix1' + + def test_after(): + assert events == [ + 'fix1 setup', + 'fix1 teardown', + ] + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=3) + + +def test_nested_async_yield_fixture(testdir): + + testdir.makepyfile(""" + import pytest + import trio + + events = [] + + @pytest.fixture + async def fix2(): + events.append('fix2 setup') + await trio.sleep(0) + + yield 'fix2' + + await trio.sleep(0) + events.append('fix2 teardown') + + @pytest.fixture + async def fix1(fix2): + events.append('fix1 setup') + await trio.sleep(0) + + yield 'fix1' + + await trio.sleep(0) + events.append('fix1 teardown') + + def test_before(): + assert not events + + @pytest.mark.trio + async def test_actual_test(fix1): + assert events == [ + 'fix2 setup', + 'fix1 setup', + ] + assert fix1 == 'fix1' + + def test_after(): + assert events == [ + 'fix2 setup', + 'fix1 setup', + 'fix1 teardown', + 'fix2 teardown', + ] + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=3) + + +def test_async_yield_fixture_within_sync_fixture(testdir): + + testdir.makepyfile(""" + import pytest + import trio + + events = [] + + @pytest.fixture + async def fix2(): + events.append('fix2 setup') + await trio.sleep(0) + + yield 'fix2' + + await trio.sleep(0) + events.append('fix2 teardown') + + @pytest.fixture + def fix1(fix2): + return 'fix1' + + def test_before(): + assert not events + + @pytest.mark.trio + async def test_actual_test(fix1): + assert events == [ + 'fix2 setup', + ] + assert fix1 == 'fix1' + + def test_after(): + assert events == [ + 'fix2 setup', + 'fix2 teardown', + ] + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=3) + + +def test_async_yield_fixture_within_sync_yield_fixture(testdir): + + testdir.makepyfile(""" + import pytest + import trio + + events = [] + + @pytest.fixture + async def fix2(): + events.append('fix2 setup') + await trio.sleep(0) + + yield 'fix2' + + await trio.sleep(0) + events.append('fix2 teardown') + + @pytest.fixture + def fix1(fix2): + events.append('fix1 setup') + yield 'fix1' + events.append('fix1 teardown') + + def test_before(): + assert not events + + @pytest.mark.trio + async def test_actual_test(fix1): + assert events == [ + 'fix2 setup', + 'fix1 setup', + ] + assert fix1 == 'fix1' + + def test_after(): + assert events == [ + 'fix2 setup', + 'fix1 setup', + 'fix1 teardown', + 'fix2 teardown', + ] + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=3) + + +def test_async_yield_fixture_with_multiple_yields(testdir): + + testdir.makepyfile(""" + import pytest + import trio + + @pytest.fixture + async def fix1(): + await trio.sleep(0) + yield 'good' + await trio.sleep(0) + yield 'bad' + + @pytest.mark.trio + async def test_actual_test(fix1): + pass + """) + + result = testdir.runpytest() + + # TODO: should trigger error instead of failure + # result.assert_outcomes(error=1) + result.assert_outcomes(failed=1) diff --git a/pytest_trio/_tests/test_basic.py b/pytest_trio/_tests/test_basic.py index 7f7bc65..16aa5d3 100644 --- a/pytest_trio/_tests/test_basic.py +++ b/pytest_trio/_tests/test_basic.py @@ -2,14 +2,70 @@ import trio -@pytest.mark.trio -async def test_sleep_with_autojump_clock(autojump_clock): - assert trio.current_time() == 0 +def test_async_test_is_executed(testdir): - for i in range(10): - print("Sleeping {} seconds".format(i)) - start_time = trio.current_time() - await trio.sleep(i) - end_time = trio.current_time() + testdir.makepyfile(""" + import pytest + import trio - assert end_time - start_time == i + async_test_called = False + + @pytest.mark.trio + async def test_base(): + global async_test_called + await trio.sleep(0) + async_test_called = True + + def test_check_async_test_called(): + assert async_test_called + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=2) + + +def test_async_test_as_class_method(testdir): + + testdir.makepyfile(""" + import pytest + import trio + + async_test_called = False + + @pytest.fixture + async def fix(): + await trio.sleep(0) + return 'fix' + + class TestInClass: + @pytest.mark.trio + async def test_base(self, fix): + global async_test_called + assert fix == 'fix' + await trio.sleep(0) + async_test_called = True + + def test_check_async_test_called(): + assert async_test_called + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=2) + + +@pytest.mark.xfail(reason='Raises pytest internal error so far...') +def test_sync_function_with_trio_mark(testdir): + + testdir.makepyfile(""" + import pytest + + @pytest.mark.trio + def test_invalid(): + pass + """) + + result = testdir.runpytest() + + result.assert_outcomes(error=1) diff --git a/pytest_trio/_tests/test_clock_fixture.py b/pytest_trio/_tests/test_clock_fixture.py new file mode 100644 index 0000000..22c803a --- /dev/null +++ b/pytest_trio/_tests/test_clock_fixture.py @@ -0,0 +1,14 @@ +import pytest +import trio + + +@pytest.mark.trio +async def test_sleep_with_autojump_clock(autojump_clock): + assert trio.current_time() == 0 + + for i in range(10): + start_time = trio.current_time() + await trio.sleep(i) + end_time = trio.current_time() + + assert end_time - start_time == i diff --git a/pytest_trio/_tests/test_fixture_scope.py b/pytest_trio/_tests/test_fixture_scope.py new file mode 100644 index 0000000..483f5dd --- /dev/null +++ b/pytest_trio/_tests/test_fixture_scope.py @@ -0,0 +1,42 @@ +import pytest + + +@pytest.mark.xfail(reason='Scope check not implemented yet') +@pytest.mark.parametrize('scope', ['class', 'module', 'session']) +def test_not_allowed_scopes(testdir, scope): + + testdir.makepyfile(""" + import pytest + + @pytest.fixture(scope=%r) + async def fix1(): + return 'fix1' + + @pytest.mark.trio + async def test_base(fix1): + pass # Crash should have occures before arriving here + """ % scope) + + result = testdir.runpytest() + + result.assert_outcomes(error=1) + + +@pytest.mark.parametrize('scope', ['function']) +def test_allowed_scopes(testdir, scope): + + testdir.makepyfile(""" + import pytest + + @pytest.fixture(scope=%r) + async def fix1(): + return 'fix1' + + @pytest.mark.trio + async def test_base(fix1): + assert fix1 == 'fix1' + """ % scope) + + result = testdir.runpytest() + + result.assert_outcomes(passed=1) diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py new file mode 100644 index 0000000..0f16056 --- /dev/null +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -0,0 +1,44 @@ +import pytest + + +@pytest.fixture +def sync_fix(): + return 'sync_fix' + + +@pytest.mark.trio +async def test_single_sync_fixture(sync_fix): + assert sync_fix == 'sync_fix' + + +def test_single_yield_fixture(testdir): + + testdir.makepyfile(""" + import pytest + + events = [] + + @pytest.fixture + def fix1(): + events.append('fixture setup') + yield 'fix1' + events.append('fixture teardown') + + def test_before(): + assert not events + + @pytest.mark.trio + async def test_actual_test(fix1): + assert events == ['fixture setup'] + assert fix1 == 'fix1' + + def test_after(): + assert events == [ + 'fixture setup', + 'fixture teardown', + ] + """) + + result = testdir.runpytest() + + result.assert_outcomes(passed=3) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 7f9abc8..15c75d8 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -2,7 +2,6 @@ import contextlib import inspect import socket -from functools import partial from traceback import format_exception import pytest @@ -24,13 +23,15 @@ def _trio_test_runner_factory(item): @trio_test async def _bootstrap_fixture_and_run_test(**kwargs): - kwargs = await _resolve_async_fixtures_in(kwargs) - await testfunc(**kwargs) + __tracebackhide__ = True + resolved_kwargs = await _setup_async_fixtures_in(kwargs) + await testfunc(**resolved_kwargs) + await _teardown_async_fixtures_in(kwargs) return _bootstrap_fixture_and_run_test -async def _resolve_async_fixtures_in(deps): +async def _setup_async_fixtures_in(deps): resolved_deps = {**deps} async def _resolve_and_update_deps(afunc, deps, entry): @@ -38,74 +39,158 @@ async def _resolve_and_update_deps(afunc, deps, entry): async with trio.open_nursery() as nursery: for depname, depval in resolved_deps.items(): - if isinstance(depval, AsyncFixture): + if isinstance(depval, BaseAsyncFixture): nursery.start_soon( - _resolve_and_update_deps, depval.resolve, resolved_deps, + _resolve_and_update_deps, depval.setup, resolved_deps, depname ) return resolved_deps -class AsyncFixture: +async def _teardown_async_fixtures_in(deps): + async with trio.open_nursery() as nursery: + for depval in deps.values(): + if isinstance(depval, BaseAsyncFixture): + nursery.start_soon(depval.teardown) + + +class BaseAsyncFixture: """ Represent a fixture that need to be run in a trio context to be resolved. - Can be async function fixture or a syncronous fixture with async - dependencies fixtures. """ - NOTSET = object() - def __init__(self, fixturefunc, fixturedef, deps={}): - self.fixturefunc = fixturefunc - # Note fixturedef.func + def __init__(self, fixturedef, deps={}): self.fixturedef = fixturedef self.deps = deps - self._ret = self.NOTSET + self.setup_done = False + self.teardown_done = False + self.result = None + self.lock = trio.Lock() + + async def setup(self): + async with self.lock: + if not self.setup_done: + self.result = await self._setup() + self.setup_done = True + return self.result + + async def _setup(self): + raise NotImplementedError() + + async def teardown(self): + async with self.lock: + if not self.teardown_done: + await self._teardown() + self.teardown_done = True + + async def _teardown(self): + raise NotImplementedError() + + +class AsyncYieldFixture(BaseAsyncFixture): + """ + Async generator fixture. + """ - async def resolve(self): - if self._ret is self.NOTSET: - resolved_deps = await _resolve_async_fixtures_in(self.deps) - if inspect.iscoroutinefunction(self.fixturefunc): - self._ret = await self.fixturefunc(**resolved_deps) - else: - self._ret = self.fixturefunc(**resolved_deps) - return self._ret + def __init__(self, *args): + super().__init__(*args) + self.agen = None + + async def _setup(self): + resolved_deps = await _setup_async_fixtures_in(self.deps) + self.agen = self.fixturedef.func(**resolved_deps) + return await self.agen.asend(None) + + async def _teardown(self): + try: + await self.agen.asend(None) + except StopAsyncIteration: + await _teardown_async_fixtures_in(self.deps) + else: + raise RuntimeError('Only one yield in fixture is allowed') + + +class SyncFixtureWithAsyncDeps(BaseAsyncFixture): + """ + Synchronous function fixture with asynchronous dependencies fixtures. + """ + + async def _setup(self): + resolved_deps = await _setup_async_fixtures_in(self.deps) + return self.fixturedef.func(**resolved_deps) + + async def _teardown(self): + await _teardown_async_fixtures_in(self.deps) + + +class SyncYieldFixtureWithAsyncDeps(BaseAsyncFixture): + """ + Synchronous generator fixture with asynchronous dependencies fixtures. + """ + def __init__(self, *args): + super().__init__(*args) + self.agen = None + + async def _setup(self): + resolved_deps = await _setup_async_fixtures_in(self.deps) + self.gen = self.fixturedef.func(**resolved_deps) + return self.gen.send(None) + + async def _teardown(self): + try: + await self.gen.send(None) + except StopIteration: + await _teardown_async_fixtures_in(self.deps) + else: + raise RuntimeError('Only one yield in fixture is allowed') + + +class AsyncFixture(BaseAsyncFixture): + """ + Regular async fixture (i.e. coroutine). + """ + async def _setup(self): + resolved_deps = await _setup_async_fixtures_in(self.deps) + return await self.fixturedef.func(**resolved_deps) + + async def _teardown(self): + await _teardown_async_fixtures_in(self.deps) def _install_async_fixture_if_needed(fixturedef, request): - deps = {dep: request.getfixturevalue(dep) for dep in fixturedef.argnames} asyncfix = None - if not deps and inspect.iscoroutinefunction(fixturedef.func): - # Top level async fixture - asyncfix = AsyncFixture(fixturedef.func, fixturedef) - elif any(dep for dep in deps.values() if isinstance(dep, AsyncFixture)): - # Fixture with async fixture dependencies - asyncfix = AsyncFixture(fixturedef.func, fixturedef, deps) - # The async fixture must be evaluated from within the trio context - # which is spawed in the function test's trio decorator. - # The trick is to make pytest's fixture call return the AsyncFixture - # object which will be actully resolved just before we run the test. + deps = {dep: request.getfixturevalue(dep) for dep in fixturedef.argnames} + if inspect.iscoroutinefunction(fixturedef.func): + asyncfix = AsyncFixture(fixturedef, deps) + elif inspect.isasyncgenfunction(fixturedef.func): + asyncfix = AsyncYieldFixture(fixturedef, deps) + elif any(dep for dep in deps.values() if isinstance(dep, BaseAsyncFixture)): + if inspect.isgeneratorfunction(fixturedef.func): + asyncfix = SyncYieldFixtureWithAsyncDeps(fixturedef, deps) + else: + asyncfix = SyncFixtureWithAsyncDeps(fixturedef, deps) if asyncfix: - fixturedef.func = lambda **kwargs: asyncfix + fixturedef.cached_result = (asyncfix, request.param_index, None) + return asyncfix -@pytest.hookimpl(tryfirst=True) -def pytest_fixture_setup(fixturedef, request): - if 'trio' in request.keywords: - _install_async_fixture_if_needed(fixturedef, request) - - -@pytest.hookimpl(tryfirst=True) -def pytest_collection_modifyitems(session, config, items): - # Retrieve test marked as `trio` - for item in items: - if 'trio' not in item.keywords: - continue - if not inspect.iscoroutinefunction(item.function): +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_call(item): + if 'trio' in item.keywords: + if not inspect.iscoroutinefunction(item.obj): pytest.fail( 'test function `%r` is marked trio but is not async' % item ) item.obj = _trio_test_runner_factory(item) + yield + + +@pytest.hookimpl() +def pytest_fixture_setup(fixturedef, request): + if 'trio' in request.keywords: + return _install_async_fixture_if_needed(fixturedef, request) + @pytest.hookimpl(tryfirst=True) def pytest_exception_interact(node, call, report): From dbb915f7f484537ce072e8343d336f2363d50ccb Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 13 Dec 2017 01:05:04 +0100 Subject: [PATCH 2/5] Correct style + disable async yield tests for python < 3.6 --- pytest_trio/_tests/test_async_fixture.py | 30 +++++++++----- .../_tests/test_async_yield_fixture.py | 39 ++++++++++++++----- pytest_trio/_tests/test_basic.py | 18 ++++++--- pytest_trio/_tests/test_fixture_scope.py | 12 ++++-- pytest_trio/_tests/test_sync_fixture.py | 6 ++- pytest_trio/plugin.py | 5 ++- 6 files changed, 77 insertions(+), 33 deletions(-) diff --git a/pytest_trio/_tests/test_async_fixture.py b/pytest_trio/_tests/test_async_fixture.py index 78674bc..8ca95c4 100644 --- a/pytest_trio/_tests/test_async_fixture.py +++ b/pytest_trio/_tests/test_async_fixture.py @@ -3,7 +3,8 @@ def test_single_async_fixture(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -15,7 +16,8 @@ async def fix1(): @pytest.mark.trio async def test_simple(fix1): assert fix1 == 'fix1' - """) + """ + ) result = testdir.runpytest() @@ -24,7 +26,8 @@ async def test_simple(fix1): def test_async_fixture_recomputed_for_each_test(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -44,7 +47,8 @@ async def test_first(fix1): @pytest.mark.trio async def test_second(fix1): assert fix1 == 2 - """) + """ + ) result = testdir.runpytest() @@ -53,7 +57,8 @@ async def test_second(fix1): def test_nested_async_fixture(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -75,7 +80,8 @@ async def test_simple(fix2): async def test_both(fix1, fix2): assert fix1 == 'fix1' assert fix2 == 'fix2(fix1)' - """) + """ + ) result = testdir.runpytest() @@ -84,7 +90,8 @@ async def test_both(fix1, fix2): def test_async_within_sync_fixture(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -100,7 +107,8 @@ def sync_fix(async_fix): @pytest.mark.trio async def test_simple(sync_fix): assert sync_fix == 42 - """) + """ + ) result = testdir.runpytest() @@ -115,7 +123,8 @@ async def test_simple(sync_fix): @pytest.mark.xfail(reason='Not implemented yet') def test_raise_in_async_fixture_cause_pytest_error(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest @pytest.fixture @@ -125,7 +134,8 @@ async def fix1(): @pytest.mark.trio async def test_base(fix1): pass # Crash should have occures before arriving here - """) + """ + ) result = testdir.runpytest() diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index e7bbb45..88ed8ff 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -1,6 +1,12 @@ +import sys +import pytest + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") def test_single_async_yield_fixture(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -29,16 +35,19 @@ def test_after(): 'fix1 setup', 'fix1 teardown', ] - """) + """ + ) result = testdir.runpytest() result.assert_outcomes(passed=3) +@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") def test_nested_async_yield_fixture(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -82,16 +91,19 @@ def test_after(): 'fix1 teardown', 'fix2 teardown', ] - """) + """ + ) result = testdir.runpytest() result.assert_outcomes(passed=3) +@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") def test_async_yield_fixture_within_sync_fixture(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -126,16 +138,19 @@ def test_after(): 'fix2 setup', 'fix2 teardown', ] - """) + """ + ) result = testdir.runpytest() result.assert_outcomes(passed=3) +@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") def test_async_yield_fixture_within_sync_yield_fixture(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -175,16 +190,19 @@ def test_after(): 'fix1 teardown', 'fix2 teardown', ] - """) + """ + ) result = testdir.runpytest() result.assert_outcomes(passed=3) +@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") def test_async_yield_fixture_with_multiple_yields(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -198,7 +216,8 @@ async def fix1(): @pytest.mark.trio async def test_actual_test(fix1): pass - """) + """ + ) result = testdir.runpytest() diff --git a/pytest_trio/_tests/test_basic.py b/pytest_trio/_tests/test_basic.py index 16aa5d3..369da8a 100644 --- a/pytest_trio/_tests/test_basic.py +++ b/pytest_trio/_tests/test_basic.py @@ -4,7 +4,8 @@ def test_async_test_is_executed(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -18,7 +19,8 @@ async def test_base(): def test_check_async_test_called(): assert async_test_called - """) + """ + ) result = testdir.runpytest() @@ -27,7 +29,8 @@ def test_check_async_test_called(): def test_async_test_as_class_method(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest import trio @@ -48,7 +51,8 @@ async def test_base(self, fix): def test_check_async_test_called(): assert async_test_called - """) + """ + ) result = testdir.runpytest() @@ -58,13 +62,15 @@ def test_check_async_test_called(): @pytest.mark.xfail(reason='Raises pytest internal error so far...') def test_sync_function_with_trio_mark(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest @pytest.mark.trio def test_invalid(): pass - """) + """ + ) result = testdir.runpytest() diff --git a/pytest_trio/_tests/test_fixture_scope.py b/pytest_trio/_tests/test_fixture_scope.py index 483f5dd..fbd5b89 100644 --- a/pytest_trio/_tests/test_fixture_scope.py +++ b/pytest_trio/_tests/test_fixture_scope.py @@ -5,7 +5,8 @@ @pytest.mark.parametrize('scope', ['class', 'module', 'session']) def test_not_allowed_scopes(testdir, scope): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest @pytest.fixture(scope=%r) @@ -15,7 +16,8 @@ async def fix1(): @pytest.mark.trio async def test_base(fix1): pass # Crash should have occures before arriving here - """ % scope) + """ % scope + ) result = testdir.runpytest() @@ -25,7 +27,8 @@ async def test_base(fix1): @pytest.mark.parametrize('scope', ['function']) def test_allowed_scopes(testdir, scope): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest @pytest.fixture(scope=%r) @@ -35,7 +38,8 @@ async def fix1(): @pytest.mark.trio async def test_base(fix1): assert fix1 == 'fix1' - """ % scope) + """ % scope + ) result = testdir.runpytest() diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py index 0f16056..a530051 100644 --- a/pytest_trio/_tests/test_sync_fixture.py +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -13,7 +13,8 @@ async def test_single_sync_fixture(sync_fix): def test_single_yield_fixture(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest events = [] @@ -37,7 +38,8 @@ def test_after(): 'fixture setup', 'fixture teardown', ] - """) + """ + ) result = testdir.runpytest() diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 15c75d8..e9d3839 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -127,6 +127,7 @@ class SyncYieldFixtureWithAsyncDeps(BaseAsyncFixture): """ Synchronous generator fixture with asynchronous dependencies fixtures. """ + def __init__(self, *args): super().__init__(*args) self.agen = None @@ -149,6 +150,7 @@ class AsyncFixture(BaseAsyncFixture): """ Regular async fixture (i.e. coroutine). """ + async def _setup(self): resolved_deps = await _setup_async_fixtures_in(self.deps) return await self.fixturedef.func(**resolved_deps) @@ -164,7 +166,8 @@ def _install_async_fixture_if_needed(fixturedef, request): asyncfix = AsyncFixture(fixturedef, deps) elif inspect.isasyncgenfunction(fixturedef.func): asyncfix = AsyncYieldFixture(fixturedef, deps) - elif any(dep for dep in deps.values() if isinstance(dep, BaseAsyncFixture)): + elif any(dep for dep in deps.values() + if isinstance(dep, BaseAsyncFixture)): if inspect.isgeneratorfunction(fixturedef.func): asyncfix = SyncYieldFixtureWithAsyncDeps(fixturedef, deps) else: From 01510bb5b0fae6c63136dc817e7f6a4e7b1088e6 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 13 Dec 2017 10:17:21 +0100 Subject: [PATCH 3/5] Mock inspect.isasyncgenfunction when using Python<3.6 --- pytest_trio/plugin.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index e9d3839..1ab2860 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -1,8 +1,14 @@ """pytest-trio implementation.""" import contextlib -import inspect import socket from traceback import format_exception +from inspect import iscoroutinefunction, isgeneratorfunction +try: + from inspect import isasyncgenfunction +except ImportError: + # `inspect.isasyncgenfunction` not available with Python<3.6 + def isasyncgenfunction(x): + return False import pytest import trio @@ -162,13 +168,13 @@ async def _teardown(self): def _install_async_fixture_if_needed(fixturedef, request): asyncfix = None deps = {dep: request.getfixturevalue(dep) for dep in fixturedef.argnames} - if inspect.iscoroutinefunction(fixturedef.func): + if iscoroutinefunction(fixturedef.func): asyncfix = AsyncFixture(fixturedef, deps) - elif inspect.isasyncgenfunction(fixturedef.func): + elif isasyncgenfunction(fixturedef.func): asyncfix = AsyncYieldFixture(fixturedef, deps) elif any(dep for dep in deps.values() if isinstance(dep, BaseAsyncFixture)): - if inspect.isgeneratorfunction(fixturedef.func): + if isgeneratorfunction(fixturedef.func): asyncfix = SyncYieldFixtureWithAsyncDeps(fixturedef, deps) else: asyncfix = SyncFixtureWithAsyncDeps(fixturedef, deps) @@ -180,7 +186,7 @@ def _install_async_fixture_if_needed(fixturedef, request): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): if 'trio' in item.keywords: - if not inspect.iscoroutinefunction(item.obj): + if not iscoroutinefunction(item.obj): pytest.fail( 'test function `%r` is marked trio but is not async' % item ) From d8ef9c1cc60cd7c51ce4eb16f57dca4aa8635cc0 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 13 Dec 2017 10:23:05 +0100 Subject: [PATCH 4/5] Fix style --- pytest_trio/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 1ab2860..a6b2bef 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -10,6 +10,7 @@ def isasyncgenfunction(x): return False + import pytest import trio from trio.testing import MockClock, trio_test From 110910f67d9d09c692be634bd4c63c38e7002ed1 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 13 Dec 2017 22:01:36 +0100 Subject: [PATCH 5/5] Remove test_fixture_scope.py (fix core dump on python 3.5) --- pytest_trio/_tests/test_fixture_scope.py | 46 ------------------------ 1 file changed, 46 deletions(-) delete mode 100644 pytest_trio/_tests/test_fixture_scope.py diff --git a/pytest_trio/_tests/test_fixture_scope.py b/pytest_trio/_tests/test_fixture_scope.py deleted file mode 100644 index fbd5b89..0000000 --- a/pytest_trio/_tests/test_fixture_scope.py +++ /dev/null @@ -1,46 +0,0 @@ -import pytest - - -@pytest.mark.xfail(reason='Scope check not implemented yet') -@pytest.mark.parametrize('scope', ['class', 'module', 'session']) -def test_not_allowed_scopes(testdir, scope): - - testdir.makepyfile( - """ - import pytest - - @pytest.fixture(scope=%r) - async def fix1(): - return 'fix1' - - @pytest.mark.trio - async def test_base(fix1): - pass # Crash should have occures before arriving here - """ % scope - ) - - result = testdir.runpytest() - - result.assert_outcomes(error=1) - - -@pytest.mark.parametrize('scope', ['function']) -def test_allowed_scopes(testdir, scope): - - testdir.makepyfile( - """ - import pytest - - @pytest.fixture(scope=%r) - async def fix1(): - return 'fix1' - - @pytest.mark.trio - async def test_base(fix1): - assert fix1 == 'fix1' - """ % scope - ) - - result = testdir.runpytest() - - result.assert_outcomes(passed=1)