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

synchronous fixture setup and teardown #138

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
255 changes: 213 additions & 42 deletions pytest_trio/_tests/test_fixture_ordering.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import pytest


# Tests that:
# - leaf_fix gets set up first and torn down last
# - the two fix_concurrent_{1,2} fixtures run their setup/teardown code
# at the same time -- their execution can be interleaved.
def test_fixture_basic_ordering(testdir):
"""
Tests that:
- leaf_fix gets set up first and torn down last
- the two fix_{1,2} fixtures run their setup/teardown code
in the expected order
fix_1 setup -> fix_2 setup -> fix_2 teardown -> fix_1 teardown
"""
testdir.makepyfile(
"""
import pytest
Expand All @@ -26,48 +29,189 @@ async def leaf_fix():
teardown_events.append("leaf_fix teardown")

assert teardown_events == [
"fix_concurrent_1 teardown 1",
"fix_concurrent_2 teardown 1",
"fix_concurrent_1 teardown 2",
"fix_concurrent_2 teardown 2",
"fix_2 teardown 1",
"fix_2 teardown 2",
"fix_1 teardown 1",
"fix_1 teardown 2",
"leaf_fix teardown",
]

@pytest.fixture
async def fix_concurrent_1(leaf_fix, seq):
async def fix_1(leaf_fix, seq):
async with seq(0):
setup_events.append("fix_concurrent_1 setup 1")
async with seq(2):
setup_events.append("fix_concurrent_1 setup 2")
setup_events.append("fix_1 setup 1")
async with seq(1):
setup_events.append("fix_1 setup 2")
yield
async with seq(4):
teardown_events.append("fix_concurrent_1 teardown 1")
async with seq(6):
teardown_events.append("fix_concurrent_1 teardown 2")
teardown_events.append("fix_1 teardown 1")
async with seq(7):
teardown_events.append("fix_1 teardown 2")

@pytest.fixture
async def fix_concurrent_2(leaf_fix, seq):
async with seq(1):
setup_events.append("fix_concurrent_2 setup 1")
async def fix_2(leaf_fix, seq):
async with seq(2):
setup_events.append("fix_2 setup 1")
async with seq(3):
setup_events.append("fix_concurrent_2 setup 2")
setup_events.append("fix_2 setup 2")
yield
async with seq(4):
teardown_events.append("fix_2 teardown 1")
async with seq(5):
teardown_events.append("fix_concurrent_2 teardown 1")
async with seq(7):
teardown_events.append("fix_concurrent_2 teardown 2")
teardown_events.append("fix_2 teardown 2")

@pytest.mark.trio
async def test_root(fix_concurrent_1, fix_concurrent_2):
async def test_root(fix_1, fix_2):
assert setup_events == [
"leaf_fix setup",
"fix_concurrent_1 setup 1",
"fix_concurrent_2 setup 1",
"fix_concurrent_1 setup 2",
"fix_concurrent_2 setup 2",
"fix_1 setup 1",
"fix_1 setup 2",
"fix_2 setup 1",
"fix_2 setup 2",
]
assert teardown_events == []

"""
)

result = testdir.runpytest()
result.assert_outcomes(passed=1)


def test_fixture_complicated_dag_ordering(testdir):
"""
This test involves several fixtures forming a pretty
complicated DAG, make sure we got the topological sort in order.
"""
testdir.makepyfile(
"""
import pytest
from pytest_trio import trio_fixture

setup_events = []
teardown_events = []

@trio_fixture
async def fix_6(fix_7):
setup_events.append("fix_6 setup")
yield
teardown_events.append("fix_6 teardown")

@pytest.fixture
async def fix_7():
setup_events.append("fix_7 setup")
yield
teardown_events.append("fix_7 teardown")
assert teardown_events == [
"fix_4 teardown",
"fix_5 teardown",
"fix_3 teardown",
"fix_1 teardown",
"fix_2 teardown",
"fix_6 teardown",
"fix_7 teardown",
]
@pytest.fixture
async def fix_4(fix_5, fix_6, fix_7):
setup_events.append("fix_4 setup")
yield
teardown_events.append("fix_4 teardown")

@pytest.fixture
async def fix_5(fix_3):
setup_events.append("fix_5 setup")
yield
teardown_events.append("fix_5 teardown")

@pytest.fixture
async def fix_3(fix_1, fix_6):
setup_events.append("fix_3 setup")
yield
teardown_events.append("fix_3 teardown")

@pytest.fixture
async def fix_1(fix_2, fix_7):
setup_events.append("fix_1 setup")
yield
teardown_events.append("fix_1 teardown")

@pytest.fixture
async def fix_2(fix_6, fix_7):
setup_events.append("fix_2 setup")
yield
teardown_events.append("fix_2 teardown")

@pytest.mark.trio
async def test_root(fix_1, fix_2, fix_3, fix_4, fix_5, fix_6, fix_7):
assert setup_events == [
"fix_7 setup",
"fix_6 setup",
"fix_2 setup",
"fix_1 setup",
"fix_3 setup",
"fix_5 setup",
"fix_4 setup",
]
assert teardown_events == []
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)


def test_contextvars_modification_follows_fixture_ordering(testdir):
"""
Tests that fixtures are being set up and tore down synchronously.

Specifically this ensures that fixtures that modify context variables
doesn't lead to a race condition.
This tries to simluate thing like trio_asyncio.open_loop that modifies
the contextvar.

Main assertion is that 2 async tasks in teardown (Resource.__aexit__)
doesn't crash.
"""
testdir.makepyfile(
"""
import contextvars
import pytest
import trio
from contextlib import asynccontextmanager
current_value = contextvars.ContextVar("variable", default=None)

@asynccontextmanager
async def variable_setter():
old_value = current_value.set("value")
try:
await trio.sleep(0)
yield
finally:
current_value.reset(old_value)

class Resource():
async def __aenter__(self):
await trio.sleep(0)

async def __aexit__(self, *_):
# We need to yield and run another trio task
await trio.sleep(0)
assert current_value.get() is not None

@pytest.fixture
async def resource():
async with variable_setter() as loop:
async with Resource():
yield

@pytest.fixture
async def trio_asyncio_loop():
async with variable_setter() as loop:
yield loop

@pytest.mark.trio
async def test_root(trio_asyncio_loop, resource):
await trio.sleep(0)
assert True
"""
)

Expand Down Expand Up @@ -127,18 +271,53 @@ async def test_root(fix2, nursery):
result.assert_outcomes(passed=1)


def test_error_message_upon_circular_dependency(testdir):
"""
Make sure that the error message is produced if there's
a circular dependency on the fixtures
"""
testdir.makepyfile(
"""
import pytest
from pytest_trio import trio_fixture

@trio_fixture
def seq(leaf_fix):
pass

@pytest.fixture
async def leaf_fix(seq):
pass

@pytest.mark.trio
async def test_root(leaf_fix, seq):
pass
"""
)

result = testdir.runpytest()
result.assert_outcomes(errors=1)
result.stdout.fnmatch_lines(
["*recursive dependency involving fixture 'leaf_fix' detected*"]
)


def test_error_collection(testdir):
# We want to make sure that pytest ultimately reports all the different
# We want to make sure that pytest ultimately reports all the
# exceptions. We call .upper() on all the exceptions so that we have
# tokens to look for in the output corresponding to each exception, where
# those tokens don't appear at all the source (so we can't get a false
# positive due to pytest printing out the source file).

# We sleep at the beginning of all the fixtures b/c currently if any
# fixture crashes, we skip setting up unrelated fixtures whose setup
# hasn't even started yet. Maybe we shouldn't? But for now the sleeps make
# sure that all the fixtures have started before any of them start
# crashing.
# We sleep at the beginning of all the fixtures to give opportunity
# for all fixtures to start the setup. Maybe we shouldn't?
# But for now the sleeps make sure that all the fixtures have
# started setting up before any of them start crashing.

# We only expect the crash output of the first fixture that crashes
# during the setup. This is because the setup are synchronous.
# Once the fixture has crashed the test contex, the others would
# immediately return and wouldn't even complete the setup process
testdir.makepyfile(
"""
import pytest
Expand Down Expand Up @@ -191,15 +370,7 @@ def test_followup():

result = testdir.runpytest()
result.assert_outcomes(passed=1, failed=1)
result.stdout.fnmatch_lines_random(
[
"*CRASH_NONGEN*",
"*CRASH_EARLY_AGEN*",
"*CRASH_LATE_AGEN*",
"*CRASH_BACKGROUND_EARLY*",
"*CRASH_BACKGROUND_LATE*",
]
)
result.stdout.fnmatch_lines(["*CRASH_NONGEN*"])


@pytest.mark.parametrize("bgmode", ["nursery fixture", "manual nursery"])
Expand Down
Loading
Loading