Hi there,
I’m a new user of this library and I’m trying to wrap my head around writing unit tests for async functions that loop indefinitely.
Background
For context, I have a function run_task
that will be called multiple times by nursery.start_soon
. There will be a saver
function to periodically save some data until all run_task
function calls have been awaited.
# test_saver.py
from typing import Callable, Awaitable
from pathlib import Path
import trio
async def run_task(sleep_time: int) -> None:
await trio.sleep(sleep_time)
async def saver(nursery: trio.Nursery, target: Callable[..., Awaitable], savepath: Path) -> None:
"""Save data periodically until all `target` functions have finished executing."""
while any(target.__name__ in task.name for task in nursery.child_tasks):
with open(savepath, "w") as f:
f.write(str(trio.current_time()))
await trio.sleep(1)
nursery.cancel_scope.cancel()
Problem
I want to test that saver
cancels the nursery
after 3 secs, which is when all run_task
tasks have completed (in the examples below).
However, I am unable to achieve such granularity in my tests — I only can verify that saver
cancels the nursery
eventually, not exactly / around the elapsed time of 3 secs.
Here are some of my attempts to do so (including using pytest-trio
fixtures to no avail)
# continuation of test_saver.py
async def test_basic(tmp_path):
# PASSING: But how can I check that the nursery was cancelled at around 3 sec?
async with trio.open_nursery() as nursery:
for i in range(3):
nursery.start_soon(run_task, i)
nursery.start_soon(saver, nursery, run_task, tmp_path / 'save.txt')
assert nursery.cancel_scope.cancel_called
async def test_with_nursery_fixture(tmp_path, nursery):
# FAILING: Might be due to a lack of checkpoints?
for i in range(3):
nursery.start_soon(run_task, i)
nursery.start_soon(saver, nursery, run_task, tmp_path / 'save.txt')
assert nursery.cancel_scope.cancel_called
async def test_with_mock_clock_fixture(tmp_path, mock_clock):
# Hangs indefinitely regardless of whether any `mock_clock.jump` is included
async with trio.open_nursery() as nursery:
mock_clock.jump(10)
for i in range(3):
nursery.start_soon(run_task, i)
nursery.start_soon(saver, nursery, run_task, tmp_path / 'save.txt')
mock_clock.jump(10)
mock_clock.jump(10)
assert nursery.cancel_scope.cancel_called
async def test_with_autojump_clock_fixture(tmp_path, autojump_clock):
# PASSING: But how can I check that the nursery was cancelled at around 3 sec?
async with trio.open_nursery() as nursery:
for i in range(3):
nursery.start_soon(run_task, i)
nursery.start_soon(saver, nursery, run_task, tmp_path / 'save.txt')
assert nursery.cancel_scope.cancel_called
Thanks in advance for your time and in answering my question!
N.B. I understand that checking for a nursery cancellation circa 3 sec is unnecessarily strict/picky for this test, but I still would like to know if there’s a way to control the execution flow using time, so as to test more complex function logics that depend on time elapsed.