Skip to content

Design discussion: support higher-scoped (class/module/session level) fixtures? #89

@oremanj

Description

@oremanj

I'm pretty sure it's possible if we're willing to use greenlet.

Here's a sketch that's missing important details like exception propagation.

Hook pytest_runtest_loop to keep a Trio event loop running in another greenlet for the whole pytest invocation:

async def _manage_fixture(teardown_evt, done_evt, coroutine_or_agen, *, task_status):
    teardown = False
    if not hasattr(coroutine_or_agen, "asend"):
        # fixture with a return statement
        result = await coroutine_or_agen
    else:
        # fixture with a yield statement
        result = await coroutine_or_agen.asend(None)
        teardown = True
    task_status.started(result)
    if teardown:
        try:
            await coroutine_or_agen.asend(None)
        except StopAsyncIteration:
            pass
        else:
            raise RuntimeError("too many yields in fixture")
    done_evt.set()

async def _pytest_trio_main(pytest_greenlet):
    async with trio.open_nursery() as nursery:
        fixtures = {}
        next_msg = "ready"
        while True:
            request = pytest_greenlet.switch(next_msg)
            if request[0] == "setup":
                fixtures[request[1]] = (teardown_evt, done_evt) = (trio.Event(), trio.Event())
                next_switch = await nursery.start(_manage_fixture, teardown_evt, done_evt, request[1])
            elif request[0] == "teardown":
                teardown_evt, done_evt = fixtures.pop(request[1])
                teardown_evt.set()
                await done_evt.wait()
                next_switch = None
            elif request[0] == "run":
                next_switch = await request[1]
            else:
                assert request[0] == "exit"
                return "done"

@pytest.hookimpl(hookwrapper=True)
def pytest_runtestloop(session):
    pytest_greenlet = greenlet.getcurrent()
    session._pytest_trio_trio_greenlet = pytest_greenlet  # or somewhere better
    trio_greenlet = greenlet.greenlet(trio.run)
    first_send = trio_greenlet.switch(_pytest_trio_main, pytest_greenlet)
    assert first_send == "ready"
    yield
    del session._pytest_trio_trio_greenlet
    assert trio_greenlet.switch("exit") == "done"

Hook fixture setup to run async fixtures in the Trio greenlet:

@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(fixturedef, request):
    wrapped_result = yield
    result = wrapped_result.get_result()
    if not hasattr(result, "__await__") and not hasattr(result, "__aiter__"):
        return
    trio_greenlet = request.session._pytest_trio_trio_greenlet
    true_result = trio_greenlet.switch("setup", result)
    if hasattr(result, "__aiter__"):
        request.addfinalizer(partial(trio_greenlet.switch, "teardown", result))
    wrapped_result.force_result(true_result)
    my_cache_key = fixturedef.cache_key(request)
    fixturedef.cached_result[my_cache_key] = (true_result, my_cache_key, None)

Hook running-the-test to run async tests in the Trio greenlet (not shown).

The details are decidedly nontrivial, but I don't see any showstoppers... thoughts? Maybe this could totally replace pytest-trio's current strategy, but it's not a win across the board, so I'm imagining it more as an alternative mode.

Benefits:

  • higher-scoped fixtures!
  • can trivially take advantage of pytest's ordering of fixtures; no need for most of the current tricky setup/teardown logic
  • could imagine running multiple tests in parallel, since you have the shared event loop all set up -- this would involve setting up multiple greenlets to do pytest_runtest_protocol on different subsets of the session.items

Drawbacks:

  • depends on greenlet (though PyPy does support it at least)
  • if you don't propagate the exceptions totally right, can expect super confusing tracebacks
  • can't use custom clocks/instruments per test
  • running fixture setup in parallel is much harder

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions