-
-
Notifications
You must be signed in to change notification settings - Fork 25
Open
Description
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
svermeulen and rsokl
Metadata
Metadata
Assignees
Labels
No labels