diff --git a/newsfragments/890.feature.rst b/newsfragments/890.feature.rst new file mode 100644 index 0000000000..e274f6982c --- /dev/null +++ b/newsfragments/890.feature.rst @@ -0,0 +1,3 @@ +Added an internal mechanism for pytest-trio's +`Hypothesis `__ integration +to make the task scheduler reproducible and avoid flaky tests. diff --git a/trio/_core/_run.py b/trio/_core/_run.py index f96f6c9181..98b44039b8 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -1,4 +1,5 @@ import functools +import itertools import logging import os import random @@ -58,6 +59,12 @@ else: # pragma: no cover raise NotImplementedError("unsupported platform") +# When running under Hypothesis, we want examples to be reproducible and +# shrinkable. pytest-trio's Hypothesis integration monkeypatches this +# variable to True, and registers the Random instance _r for Hypothesis +# to manage for each test case, which together should make Trio's task +# scheduling loop deterministic. We have a test for that, of course. +_ALLOW_DETERMINISTIC_SCHEDULING = False _r = random.Random() # Used to log exceptions in instruments @@ -682,6 +689,7 @@ class Task: name = attr.ib() # PEP 567 contextvars context context = attr.ib() + _counter = attr.ib(init=False, factory=itertools.count().__next__) # Invariant: # - for unscheduled tasks, _next_send is None @@ -1557,6 +1565,12 @@ def run_impl(runner, async_fn, args): # change too, like the deadlines tie-breaker and the non-deterministic # ordering of task._notify_queues.) batch = list(runner.runq) + if _ALLOW_DETERMINISTIC_SCHEDULING: + # We're running under Hypothesis, and pytest-trio has patched this + # in to make the scheduler deterministic and avoid flaky tests. + # It's not worth the (small) performance cost in normal operation, + # since we'll shuffle the list and _r is only seeded for tests. + batch.sort(key=lambda t: t._counter) runner.runq.clear() _r.shuffle(batch) while batch: diff --git a/trio/tests/test_scheduler_determinism.py b/trio/tests/test_scheduler_determinism.py new file mode 100644 index 0000000000..ba5f469396 --- /dev/null +++ b/trio/tests/test_scheduler_determinism.py @@ -0,0 +1,42 @@ +import trio + + +async def scheduler_trace(): + """Returns a scheduler-dependent value we can use to check determinism.""" + trace = [] + + async def tracer(name): + for i in range(10): + trace.append((name, i)) + await trio.sleep(0) + + async with trio.open_nursery() as nursery: + for i in range(5): + nursery.start_soon(tracer, i) + + return tuple(trace) + + +def test_the_trio_scheduler_is_not_deterministic(): + # At least, not yet. See https://github.com/python-trio/trio/issues/32 + traces = [] + for _ in range(10): + traces.append(trio.run(scheduler_trace)) + assert len(set(traces)) == len(traces) + + +def test_the_trio_scheduler_is_deterministic_if_seeded(monkeypatch): + monkeypatch.setattr( + trio._core._run, "_ALLOW_DETERMINISTIC_SCHEDULING", True + ) + traces = [] + for _ in range(10): + state = trio._core._run._r.getstate() + try: + trio._core._run._r.seed(0) + traces.append(trio.run(scheduler_trace)) + finally: + trio._core._run._r.setstate(state) + + assert len(traces) == 10 + assert len(set(traces)) == 1