def autojump_clock(request): # Event dispatching through PostgreSQL LISTEN/NOTIFY is # invisible from trio point of view, hence waiting for # event with autojump_threshold=0 means we jump to timeout if request.config.getoption("--postgresql"): return MockClock(autojump_threshold=0.1) else: return MockClock(autojump_threshold=0)
from .head import * from .resource import Memory, ZERO import pytest import trio from trio.testing import MockClock from functools import partial from io import StringIO sprint = partial(trio.run, clock=MockClock(rate=1000)) GrainExecutor = partial(GrainExecutor, config_file=False) async def rev(n, i): await trio.sleep(n - i) return i def test_order(monkeypatch): monkeypatch.setattr(trio, "run", sprint) N = 10 with GrainExecutor([], Memory(8)) as exer: for i in range(N): exer.submit(Memory(2), rev, N, i) assert exer.results == list(range(N)) class Critical(Exception): pass
def autojump_clock(): return MockClock(autojump_threshold=0)
def mock_clock(): return MockClock()
def _try_customizing(self): with pytest.raises(RuntimeError): self.set_clock(MockClock()) with pytest.raises(RuntimeError): self.push_instrument(Instrument())
def frozen_clock(): # Mocked clock is a slippy slope: we want time to go faster (or even to # jump to arbitrary point in time !) on some part of our application while # some other parts should keep using the real time. # For instance we want to make sure some part of a test doesn't take more than # x seconds in real life (typically to detect deadlock), but this test might # be about a ping occurring every 30s so we want to simulate this wait. # # The simple solution is to use `MockClock.rate` to make time go faster, # but it's bad idea given we end up with two antagonistic goals: # - rate should be as high as possible so that ping wait goes as fast as possible # - the highest rate is, the smallest real time window we have when checking for # deadlock, this is especially an issue given developer machine is a behemoth # while CI run on potatoes (especially on MacOS) shared with other builds... # # So the solution we choose here is to separate the two times: # - Parsec codebase uses the trio clock and `trio.fail_after/move_on_after` # - Test codebase can use `trio.fail_after/move_on_after` as long as the test # doesn't use a mock clock # - In case of mock clock, test codebase must use `real_clock_timeout` that # relies on monotonic clock and hence is totally isolated from trio's clock. # # On top of that we must be careful about the configuration of the mock clock ! # As we said the Parsec codebase (i.e. not the tests) uses the trio clock for # timeout handling & sleep (e.g. in the managers), hence: # - Using `MockClock.rate` with a high value still lead to the issue dicussed above. # - `trio.to_thread.run_sync` doesn't play nice with `MockClock.autojump_threshold = 0` # given trio considers the coroutine waiting for the thread is idle and hence # trigger the clock jump. So a perfectly fine async code may break tests in # an unexpected way if it starts using `trio.to_thread.run_sync`... # # So the idea of the `frozen_clock` is to only advance when expecially # specified in the test (i.e. rate 0 and no autojump_threshold). # This way only the test code has control over the application timeout # handling, and we have a clean separation with the test timeout (i.e. using # `real_clock_timeout` to detect the test endup in a deadlock) # # The drawback of this approach is manually handling time jump can be cumbersome. # For instance the backend connection retry logic: # - sleeps for some time # - connects to the backend # - starts sync&message monitors # - message monitor may trigger modifications in the sync monitor # - in case of modification, sync monitor is going to sleep for a short time # before doing the sync of the modification # # So to avoid having to mix `MockClock.jump` and `trio.testing.wait_all_tasks_blocked` # in a very complex and fragile way, we introduce the `sleep_with_autojump()` # method that is the only place where clock is going to move behind our back, but # for only the amount of time we choose, and only in a very explicit manner. # # Finally, an additional bonus to this approach is we can use breakpoint in the # code without worrying about triggering a timeout ;-) clock = MockClock(rate=0, autojump_threshold=math.inf) clock.real_clock_timeout = real_clock_timeout # Quick access helper async def _sleep_with_autojump(seconds): old_rate = clock.rate old_autojump_threshold = clock.autojump_threshold clock.rate = 0 clock.autojump_threshold = 0.01 try: await trio.sleep(seconds) finally: clock.rate = old_rate clock.autojump_threshold = old_autojump_threshold clock.sleep_with_autojump = _sleep_with_autojump yield clock