33[ ![ CI] ( https://github.com/nolar/looptime/workflows/Thorough%20tests/badge.svg )] ( https://github.com/nolar/looptime/actions/workflows/thorough.yaml )
44[ ![ codecov] ( https://codecov.io/gh/nolar/looptime/branch/main/graph/badge.svg )] ( https://codecov.io/gh/nolar/looptime )
55[ ![ Coverage Status] ( https://coveralls.io/repos/github/nolar/looptime/badge.svg?branch=main )] ( https://coveralls.io/github/nolar/looptime?branch=main )
6- [ ![ Total alerts] ( https://img.shields.io/lgtm/alerts/g/nolar/looptime.svg?logo=lgtm&logoWidth=18 )] ( https://lgtm.com/projects/g/nolar/looptime/alerts/ )
7- [ ![ Language grade: Python] ( https://img.shields.io/lgtm/grade/python/g/nolar/looptime.svg?logo=lgtm&logoWidth=18 )] ( https://lgtm.com/projects/g/nolar/looptime/context:python )
86[ ![ pre-commit] ( https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white )] ( https://github.com/pre-commit/pre-commit )
97
108## What?
@@ -19,10 +17,10 @@ The effects of time removal can be seen from both sides:
1917 This hides the code overhead and network latencies from the time measurements,
2018 making the loop time sharply and predictably advancing in configured steps.
2119
22- * From the ** observer's (i.e. your) point of view,**
20+ * From the ** observer's (i.e. your personal ) point of view,**
2321 all activities of the event loop, such as sleeps, events/conditions waits,
2422 timeouts, "later" callbacks, happen in near-zero amount of the real time
25- (above the usual code overhead).
23+ (due to the usual code execution overhead).
2624 This speeds up the execution of tests without breaking the tests' time-based
2725 design, even if they are designed to run in seconds or minutes.
2826
@@ -59,28 +57,28 @@ at the same time:
5957* One is for the coroutine-under-test which moves between states
6058 in the background.
6159* Another one is for the test itself, which controls the flow
62- of that coroutine-under-test: it sets events, injects data, etc.
60+ of that coroutine-under-test: it schedules events, injects data, etc.
6361
6462In textbook cases with simple coroutines that are more like regular functions,
6563it is possible to design a test so that it runs straight to the end in one hop
6664— with all the preconditions set and data prepared in advance in the test setup.
6765
68- However, in real-world cases, the tests often must verify that
66+ However, in the real-world cases, the tests often must verify that
6967the coroutine stops at some point, waits for a condition for some limited time,
7068and then passes or fails.
7169
7270The problem is often "solved" by mocking the low-level coroutines of sleep/wait
7371that we expect the coroutine-under-test to call. But this violates the main
74- principle of good unit-tests: test the promise, not the implementation.
72+ principle of good unit-tests: ** test the promise, not the implementation.**
7573Mocking and checking the low-level coroutines is based on the assumptions
7674of how the coroutine is implemented internally, which can change over time.
7775Good tests do not change on refactoring if the protocol remains the same.
7876
7977Another (straightforward) approach is to not mock the low-level routines, but
8078to spend the real-world time, just in short bursts as hard-coded in the test.
8179Not only it makes the whole test-suite slower, it also brings the execution
82- time close to the values where the code overhead affects the timing
83- and makes it difficult to assert on the coroutine's pure time.
80+ time close to the values where the code overhead or measurement errors affect
81+ the timing, which makes it difficult to assert on the coroutine's pure time.
8482
8583
8684## Solution
@@ -232,13 +230,26 @@ async def test_me():
232230` start ` (` float ` or ` None ` , or a no-argument callable that returns the same)
233231is the initial time of the event loop.
234232
235- If it is callable, it is invoked once per event loop to get the value:
233+ If it is a callable, it is invoked once per event loop to get the value:
236234e.g. ` start=time.monotonic ` to align with the true time,
237235or ` start=lambda: random.random() * 100 ` to add some unpredictability.
238236
239237` None ` is treated the same as ` 0.0 ` .
240238
241- The default is ` 0.0 ` .
239+ The default is ` 0.0 ` . For reusable event loops, the default is to keep
240+ the time untouched, which means ` 0.0 ` or the explicit value for the first test,
241+ but then an ever-increasing value for the 2nd, 3rd, and further tests.
242+
243+ Note: pytest-asyncio 1.0.0+ introduced event loops with higher scopes,
244+ e.g. class-, module-, packages-, session-scoped event loops used in tests.
245+ Such event loops are reused, so their time continues growing through many tests.
246+ However, if the test is explicitly configured with the start time,
247+ that time is enforced to the event loop when the test function starts —
248+ to satisfy the clearly declared intentions — even if the time moves backwards,
249+ which goes against the nature of the time itself (monotonically growing).
250+ This might lead to surprises in time measurements outside of the test,
251+ e.g. in fixtures: the code durations can become negative, or the events can
252+ happen (falsely) before they are scheduled (loop-clock-wise). Be careful.
242253
243254
244255### The end of time
@@ -255,6 +266,8 @@ e.g. with `asyncio.sleep(0)`, simple `await` statements, etc.
255266
256267If set to ` None ` , there is no end of time, and the event loop runs
257268as long as needed. Note: ` 0 ` means ending the time immediately on start.
269+ Be careful with the explicit ending time in higher-scoped event loops
270+ of pytest-asyncio>=1.0.0, since they time increases through many tests.
258271
259272If it is a callable, it is called once per event loop to get the value:
260273e.g. ` end=lambda: time.monotonic() + 10 ` .
@@ -512,6 +525,63 @@ For example, with the resolution `0.001`, the time
512525everything smaller than ` 0.001 ` becomes ` 0 ` and probably misbehaves._
513526
514527
528+ ### Time magic coverage
529+
530+ The time compaction magic is enabled only for the duration of the test,
531+ i.e. the test function — but not the fixtures.
532+ The fixtures run in the real (wall-clock) time.
533+
534+ The options (including the force starting time) are applied at the test function
535+ starting moment, not when it is setting up the fixtures (even function-scoped).
536+
537+ This is caused by a new concept of multiple co-existing event loops
538+ in pytest-asyncio>=1.0.0:
539+
540+ - It is unclear which options to apply to higher-scoped fixtures
541+ used by many tests, which themselves use higher-scoped event loops —
542+ especially in selective partial runs. Technically, it is the 1st test,
543+ with the options of 2nd and further tests simply ignored.
544+ - It is impossible to guess which event loop will be the running loop
545+ in the test until we reach the test itself, i.e. we do not know this
546+ when setting up the fixtures, even function-scoped fixtures.
547+ - There is no way to cover the fixture teardown (no hook in pytest),
548+ only for the fixture setup and post-teardown cleanup.
549+
550+ As such, this functionality (covering of function-scoped fixtures)
551+ was abandoned — since it was never promised, tested, or documented —
552+ plus an assumption that it was never used by anyone (it should not be).
553+ It was rather a side effect of the previous implemention,
554+ which is not available or possible anymore.
555+
556+
557+ ### pytest-asyncio>=1.0.0
558+
559+ As it is said above, pytest-asyncio>=1.0.0 introduced several co-existing
560+ event loops of different scopes. The time compaction in these event loops
561+ is NOT activated. Only the running loop of the test function is activated.
562+
563+ Configuring and activating multiple co-existing event loops brings a few
564+ conceptual challenges, which require a good sample case to look into,
565+ and some time to think.
566+
567+ Would you need time compaction in your fixtures of higher scopes,
568+ do it explicitly:
569+
570+ ``` python
571+ import asyncio
572+ import pytest
573+
574+ @pytest.fixture
575+ async def fixt ():
576+ loop = asyncio.get_running_loop()
577+ loop.setup_looptime(start = 123 , end = 456 )
578+ with loop.looptime_enabled():
579+ await do_things()
580+ ```
581+
582+ There is #11 to add a feature to do this automatically, but it is not yet done.
583+
584+
515585## Extras
516586
517587### Chronometers
@@ -605,6 +675,8 @@ the loop time also includes the time of all fixtures setups.
605675Do you use a custom event loop? No problem! Create a test-specific descendant
606676with the provided mixin — and it will work the same as the default event loop.
607677
678+ For ` pytest-asyncio<1.0.0 ` :
679+
608680``` python
609681import looptime
610682import pytest
@@ -620,13 +692,38 @@ def event_loop():
620692 return LooptimeCustomEventLoop()
621693```
622694
695+ For ` pytest-asyncio>=1.0.0 ` :
696+
697+ ``` python
698+ import asyncio
699+ import looptime
700+ import pytest
701+ from wherever import CustomEventLoop
702+
703+
704+ class LooptimeCustomEventLoop (looptime .LoopTimeEventLoop , CustomEventLoop ):
705+ pass
706+
707+
708+ class LooptimeCustomEventLoopPolicy (asyncio .DefaultEventLoopPolicy ):
709+ def new_event_loop (self ):
710+ return LooptimeCustomEventLoop()
711+
712+
713+ @pytest.fixture (scope = ' session' )
714+ def event_loop_policy ():
715+ return LooptimeCustomEventLoopPolicy()
716+ ```
717+
623718Only selector-based event loops are supported: the event loop must rely on
624719` self._selector.select(timeout) ` to sleep for ` timeout ` true-time seconds.
625720Everything that inherits from ` asyncio.BaseEventLoop ` should work.
626721
627722You can also patch almost any event loop class or event loop object
628723the same way as ` looptime ` does that (via some dirty hackery):
629724
725+ For ` pytest-asyncio<1.0.0 ` :
726+
630727``` python
631728import asyncio
632729import looptime
@@ -639,6 +736,25 @@ def event_loop():
639736 return looptime.patch_event_loop(loop)
640737```
641738
739+ For ` pytest-asyncio>=1.0.0 ` :
740+
741+ ``` python
742+ import asyncio
743+ import looptime
744+ import pytest
745+
746+
747+ class LooptimeEventLoopPolicy (asyncio .DefaultEventLoopPolicy ):
748+ def new_event_loop (self ):
749+ loop = super ().new_event_loop()
750+ return looptime.patch_event_loop(loop)
751+
752+
753+ @pytest.fixture (scope = ' session' )
754+ def event_loop_policy ():
755+ return LooptimeEventLoopPolicy()
756+ ```
757+
642758` looptime.make_event_loop_class(cls) ` constructs a new class that inherits
643759from the referenced class and the specialised event loop class mentioned above.
644760The resulting classes are cached, so it can be safely called multiple times.
0 commit comments