looptime 0.1__py3-none-any.whl → 0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of looptime might be problematic. Click here for more details.
- looptime/__init__.py +2 -1
- looptime/loops.py +71 -4
- looptime/math.py +3 -1
- looptime/plugin.py +293 -22
- looptime/timeproxies.py +0 -10
- {looptime-0.1.dist-info → looptime-0.3.dist-info}/METADATA +158 -32
- looptime-0.3.dist-info/RECORD +15 -0
- {looptime-0.1.dist-info → looptime-0.3.dist-info}/WHEEL +1 -1
- {looptime-0.1.dist-info → looptime-0.3.dist-info}/entry_points.txt +0 -1
- looptime-0.1.dist-info/RECORD +0 -15
- {looptime-0.1.dist-info → looptime-0.3.dist-info/licenses}/LICENSE +0 -0
- {looptime-0.1.dist-info → looptime-0.3.dist-info}/top_level.txt +0 -0
- {looptime-0.1.dist-info → looptime-0.3.dist-info}/zip-safe +0 -0
looptime/__init__.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from .chronometers import Chronometer
|
|
2
|
-
from .loops import IdleTimeoutError, LoopTimeEventLoop, LoopTimeoutError
|
|
2
|
+
from .loops import IdleTimeoutError, LoopTimeEventLoop, LoopTimeoutError, TimeWarning
|
|
3
3
|
from .patchers import make_event_loop_class, new_event_loop, patch_event_loop, reset_caches
|
|
4
4
|
from .timeproxies import LoopTimeProxy
|
|
5
5
|
|
|
6
6
|
__all__ = [
|
|
7
7
|
'Chronometer',
|
|
8
|
+
'TimeWarning',
|
|
8
9
|
'LoopTimeProxy',
|
|
9
10
|
'IdleTimeoutError',
|
|
10
11
|
'LoopTimeoutError',
|
looptime/loops.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
import selectors
|
|
5
6
|
import time
|
|
7
|
+
import warnings
|
|
6
8
|
import weakref
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Callable, MutableSet, TypeVar, cast, overload
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterator, MutableSet, TypeVar, cast, overload
|
|
8
10
|
|
|
9
11
|
_T = TypeVar('_T')
|
|
10
12
|
|
|
@@ -16,6 +18,11 @@ else:
|
|
|
16
18
|
AnyTask = asyncio.Task
|
|
17
19
|
|
|
18
20
|
|
|
21
|
+
class TimeWarning(UserWarning):
|
|
22
|
+
"""Issued when the loop time moves backwards, violating its monotonicity."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
19
26
|
class LoopTimeoutError(asyncio.TimeoutError):
|
|
20
27
|
"""A special kind of timeout when the loop's time reaches its end."""
|
|
21
28
|
pass
|
|
@@ -61,6 +68,7 @@ class LoopTimeEventLoop(asyncio.BaseEventLoop):
|
|
|
61
68
|
idle_step: float | None = None,
|
|
62
69
|
idle_timeout: float | None = None,
|
|
63
70
|
noop_cycles: int = 42,
|
|
71
|
+
_enabled: bool | None = None, # None means do nothing
|
|
64
72
|
) -> None:
|
|
65
73
|
"""
|
|
66
74
|
Set all the fake-time fields and patch the i/o selector.
|
|
@@ -69,9 +77,33 @@ class LoopTimeEventLoop(asyncio.BaseEventLoop):
|
|
|
69
77
|
when the mixin/class is injected into the existing event loop object.
|
|
70
78
|
In that case, the object is already initialised except for these fields.
|
|
71
79
|
"""
|
|
80
|
+
new_time: float | None = start() if callable(start) else start
|
|
81
|
+
end_time: float | None = end() if callable(end) else end
|
|
82
|
+
old_time: float | None
|
|
83
|
+
try:
|
|
84
|
+
# NB: using the existing (old) reciprocal!
|
|
85
|
+
old_time = self.__int2time(self.__now)
|
|
86
|
+
except AttributeError: # initial setup: either reciprocals or __now are absent
|
|
87
|
+
old_time = None
|
|
88
|
+
new_time = float(new_time) if new_time is not None else None
|
|
89
|
+
|
|
90
|
+
# If it is the 2nd or later setup, double-check on time monotonicity.
|
|
91
|
+
# In some configurations, this waring might raise an error and fail the test.
|
|
92
|
+
# In that case, the time must not be changed for the next test.
|
|
93
|
+
if old_time is not None and new_time is not None and new_time < old_time:
|
|
94
|
+
warnings.warn(
|
|
95
|
+
f"The time of the event loop moves backwards from {old_time} to {new_time},"
|
|
96
|
+
" thus breaking the monotonicity of time. This is dangerous!"
|
|
97
|
+
" Perhaps, caused by reusing a higher-scope event loop in tests."
|
|
98
|
+
" Revise the scopes of fixtures & event loops."
|
|
99
|
+
" Remove the start=… kwarg and rely on arbitrary time values."
|
|
100
|
+
" Migrate from `loop.time()` to the `looptime` numeric fixture.",
|
|
101
|
+
TimeWarning,
|
|
102
|
+
)
|
|
103
|
+
|
|
72
104
|
self.__resolution_reciprocal: int = round(1/resolution)
|
|
73
|
-
self.__now: int = self.__time2int(
|
|
74
|
-
self.__end: int | None = self.__time2int(
|
|
105
|
+
self.__now: int = self.__time2int(new_time or old_time) or 0
|
|
106
|
+
self.__end: int | None = self.__time2int(end_time)
|
|
75
107
|
|
|
76
108
|
self.__idle_timeout: int | None = self.__time2int(idle_timeout)
|
|
77
109
|
self.__idle_step: int | None = self.__time2int(idle_step)
|
|
@@ -85,6 +117,13 @@ class LoopTimeEventLoop(asyncio.BaseEventLoop):
|
|
|
85
117
|
self.__sync_clock: Callable[[], float] = time.perf_counter
|
|
86
118
|
self.__sync_ts: float | None = None # system/true-time clock timestamp
|
|
87
119
|
|
|
120
|
+
try:
|
|
121
|
+
self.__enabled # type: ignore
|
|
122
|
+
except AttributeError:
|
|
123
|
+
self.__enabled = _enabled if _enabled is not None else True # old behaviour
|
|
124
|
+
else:
|
|
125
|
+
self.__enabled = _enabled if _enabled is not None else self.__enabled
|
|
126
|
+
|
|
88
127
|
# TODO: why do we patch the selector as an object while the event loop as a class?
|
|
89
128
|
# this should be the same patching method for both.
|
|
90
129
|
try:
|
|
@@ -93,10 +132,28 @@ class LoopTimeEventLoop(asyncio.BaseEventLoop):
|
|
|
93
132
|
self.__original_select = self._selector.select
|
|
94
133
|
self._selector.select = self.__replaced_select # type: ignore
|
|
95
134
|
|
|
135
|
+
@property
|
|
136
|
+
def looptime_on(self) -> bool:
|
|
137
|
+
return bool(self.__enabled)
|
|
138
|
+
|
|
139
|
+
@contextlib.contextmanager
|
|
140
|
+
def looptime_enabled(self) -> Iterator[None]:
|
|
141
|
+
"""
|
|
142
|
+
Temporarily enable the time compaction, restore the normal mode on exit.
|
|
143
|
+
"""
|
|
144
|
+
if self.__enabled:
|
|
145
|
+
raise RuntimeError('Looptime mode is already enabled. Entered twice? Avoid this!')
|
|
146
|
+
old_enabled = self.__enabled
|
|
147
|
+
self.__enabled = True
|
|
148
|
+
try:
|
|
149
|
+
yield
|
|
150
|
+
finally:
|
|
151
|
+
self.__enabled = old_enabled
|
|
152
|
+
|
|
96
153
|
def time(self) -> float:
|
|
97
154
|
return self.__int2time(self.__now)
|
|
98
155
|
|
|
99
|
-
def run_in_executor(self, executor: Any, func: Any, *args: Any) -> AnyFuture:
|
|
156
|
+
def run_in_executor(self, executor: Any, func: Any, *args: Any) -> AnyFuture: # type: ignore
|
|
100
157
|
future = super().run_in_executor(executor, func, *args)
|
|
101
158
|
if isinstance(future, asyncio.Future):
|
|
102
159
|
self.__sync_futures.add(future)
|
|
@@ -111,6 +168,16 @@ class LoopTimeEventLoop(asyncio.BaseEventLoop):
|
|
|
111
168
|
if ready:
|
|
112
169
|
pass
|
|
113
170
|
|
|
171
|
+
# If nothing to do right now, and the time is not compacted, truly sleep as requested.
|
|
172
|
+
# Move the fake time by the exact real time spent in this wait (±discrepancies).
|
|
173
|
+
elif not self.__enabled:
|
|
174
|
+
t0 = time.monotonic()
|
|
175
|
+
ready = self.__original_select(timeout=timeout)
|
|
176
|
+
t1 = time.monotonic()
|
|
177
|
+
|
|
178
|
+
# If timeout=None, it never exists until ready. This timeout check is for typing only.
|
|
179
|
+
self.__now += self.__time2int(t1 - t0 if ready or timeout is None else timeout)
|
|
180
|
+
|
|
114
181
|
# Regardless of the timeout, if there are executors sync futures, we move the time in steps.
|
|
115
182
|
# The timeout (if present) can limit the size of the step, but not the logic of stepping.
|
|
116
183
|
# Generally, external things (threads) take some time (e.g. for thread spawning).
|
looptime/math.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class Numeric(metaclass=abc.ABCMeta):
|
|
@@ -108,5 +109,6 @@ class Numeric(metaclass=abc.ABCMeta):
|
|
|
108
109
|
else:
|
|
109
110
|
return NotImplemented
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
# See the StdLib's comments on pow() on why it is Any, not float.
|
|
113
|
+
def __pow__(self, power: float, modulo: None = None) -> Any:
|
|
112
114
|
return pow(round(self._value * self.__rr), power, modulo) / pow(self.__rr, power, modulo)
|
looptime/plugin.py
CHANGED
|
@@ -1,36 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integrations with pytest & pytest-asyncio (irrelevant for other frameworks).
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
IMPLEMENTATION DETAIL - reverse time movement
|
|
6
|
+
=============================================
|
|
7
|
+
|
|
8
|
+
Problem
|
|
9
|
+
-------
|
|
10
|
+
|
|
11
|
+
Pytest-asyncio>=1.0.0 has removed the ``event_loop`` fixture and fully switched
|
|
12
|
+
to the ``event_loop_policy`` (session-scoped) plus several independent fixtures:
|
|
13
|
+
session-, package-, module-, class-, function-scoped. It means that a test
|
|
14
|
+
might use any of these fixtures or several or all of them at the same time.
|
|
15
|
+
|
|
16
|
+
As a result, our previous assumption that every test & all its fixtures run
|
|
17
|
+
in its own isolated short-lived event loop, is now broken:
|
|
18
|
+
|
|
19
|
+
- A single event loop can be shared by multiple (but not all) tests.
|
|
20
|
+
- A single test can be spread over multiple (but not all) event loops.
|
|
21
|
+
|
|
22
|
+
An classic example:
|
|
23
|
+
|
|
24
|
+
- A session-scoped fixture ``server`` starts a port listener & an HTTP server.
|
|
25
|
+
- A module-scoped fixture ``data`` populates the server via POST requests.
|
|
26
|
+
- A few function-scoped tests access & assert these data via GET requests.
|
|
27
|
+
- Other tests verify the database and do not touch the event loops.
|
|
28
|
+
|
|
29
|
+
Looptime suggests setting the start time of the event loop or expect it to be 0.
|
|
30
|
+
This simplifies assertions, scheduling of events, callbacks, and other triggers.
|
|
31
|
+
|
|
32
|
+
As a result, a long-living event loop might see the time set/reset by tests,
|
|
33
|
+
and in most cases, it will be moving the time backwards.
|
|
34
|
+
|
|
35
|
+
Time, by its nature, is supposed to be monotonic (but can be non-linear),
|
|
36
|
+
specifically positively monotonic — always growing, never going backwards.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
Solution
|
|
40
|
+
--------
|
|
41
|
+
|
|
42
|
+
We sacrifice this core property of time for the sake of simplicity of tests.
|
|
43
|
+
|
|
44
|
+
So we should be prepared for the consequences when all hell breaks loose. E.g.:
|
|
45
|
+
the callbacks and other events triggering before they were set up (clock-wise);
|
|
46
|
+
the durations of activities being negative; so on.
|
|
47
|
+
|
|
48
|
+
Either way, we set the loop time as requested, but with a few nuances:
|
|
49
|
+
|
|
50
|
+
1. If the start time is NOT explicitly defined, for higher-scoped event loops,
|
|
51
|
+
we keep the time as is for every test and let it flow monotonically.
|
|
52
|
+
Previously, the higher-scoped fixtures did not exist, so nothing breaks.
|
|
53
|
+
|
|
54
|
+
2. If the start time is explicitly defined and is in the future, move the time
|
|
55
|
+
forwards as specified — indistinguishable from the previous behaviour
|
|
56
|
+
(except there could be artifacts from the previous tests in the loop).
|
|
57
|
+
|
|
58
|
+
3. If the start time is explicitly defined and is in the past, issue a warning
|
|
59
|
+
of class ``looptime.TimeWarning``, which inherits from ``UserWarning``,
|
|
60
|
+
indicating a user-side misbehaviour & broken test-suite design.
|
|
61
|
+
It can be configured to raise an error (strict mode), or be ignored.
|
|
62
|
+
|
|
63
|
+
This ensures the most possible backwards compatibility with the old behavior
|
|
64
|
+
with a few truthworthy assumptions in mind:
|
|
65
|
+
|
|
66
|
+
- Fixtures do not measure time and do not rely on time. Their purpose should be
|
|
67
|
+
preparing the environment, filling the data. Only the tests can move the time.
|
|
68
|
+
As such, they will not suffer much from the backward time movements.
|
|
69
|
+
|
|
70
|
+
- Old-style tests typically use the function scope & the function-scoped loop,
|
|
71
|
+
which has the time set at 0 by default. No changes to the previous behaviour.
|
|
72
|
+
|
|
73
|
+
- New-style tests that run in higher-scoped loops (a new pytest-asyncio feature)
|
|
74
|
+
should not rely on an isolated event loop and the time starting with 0,
|
|
75
|
+
and should be clearly prepared for the backward time movements
|
|
76
|
+
if they express the intention to reset the start time of the event loop.
|
|
77
|
+
Such tests should measure the "since" and "till" and assert on the difference.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
IMPLEMENTATION DETAIL — patching always, activating on-demand
|
|
81
|
+
=============================================================
|
|
82
|
+
|
|
83
|
+
In order to make event loops compatible with looptime, they (the event loops)
|
|
84
|
+
MUST be patched at creation, not in the middle of a runtime when it reaches
|
|
85
|
+
the looptime-enabled tests (consider a global session-scoped event loop here).
|
|
86
|
+
|
|
87
|
+
Therefore, we patch ALL the implicit event loops of pytest-asyncio, regardless
|
|
88
|
+
of whether they are supposed to be used or not. They are disabled (inactive)
|
|
89
|
+
initally, i.e. their time flows normally, using the wall-clock (true) time.
|
|
90
|
+
|
|
91
|
+
We then activate the looptime magic on demand for those tests that need it,
|
|
92
|
+
and only when needed (i.e. when requested/configured/marked).
|
|
93
|
+
|
|
94
|
+
We only activate the time magic on the running loop of the test, and only
|
|
95
|
+
during the test execution. We do not compact the time of the event loops
|
|
96
|
+
used in fixtures, even when the fixtures use the same-scoped event loop.
|
|
97
|
+
|
|
98
|
+
(This might be a breaking change. See the assumptions above for the rationale.)
|
|
99
|
+
|
|
100
|
+
Even for the lowest "function" scope, we cannot patch-and-activate it only once
|
|
101
|
+
at creation, since at the time of the event loop setup (creation),
|
|
102
|
+
we do not know which event loop will be the running loop of the test.
|
|
103
|
+
This affects to which loop the configured options should be applied.
|
|
104
|
+
|
|
105
|
+
We only know this when we reach the test.
|
|
106
|
+
We then apply the options, and activate the pre-patched running event loop.
|
|
107
|
+
"""
|
|
1
108
|
from __future__ import annotations
|
|
2
109
|
|
|
3
110
|
import asyncio
|
|
4
|
-
|
|
111
|
+
import warnings
|
|
112
|
+
from typing import Any
|
|
5
113
|
|
|
114
|
+
import _pytest.nodes
|
|
6
115
|
import pytest
|
|
7
116
|
|
|
8
|
-
from looptime import loops, patchers
|
|
117
|
+
from looptime import loops, patchers, timeproxies
|
|
9
118
|
|
|
10
119
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
120
|
+
# Critical implementation details: It MUST be sync! It CANNOT be async!
|
|
121
|
+
# It might seem that the easiest way to implement the ``looptime`` fixture is
|
|
122
|
+
# to make it ``async def`` and get the running loop inside. This does NOT work.
|
|
123
|
+
# When a function-scoped fixture is used in any higher-scoped test, it degrades
|
|
124
|
+
# the test from its scope to the function scope and breaks the test design.
|
|
125
|
+
# See an example at https://github.com/pytest-dev/pytest-asyncio/issues/1142.
|
|
126
|
+
# As such, the fixture MUST be synchronous (simple ``def``). As a result,
|
|
127
|
+
# the fixture CANNOT get a running loop, because there is no running loop.
|
|
128
|
+
# We take the running loop inside the time-measuring proxy at runtime.
|
|
129
|
+
@pytest.fixture
|
|
130
|
+
def looptime(request: pytest.FixtureRequest) -> timeproxies.LoopTimeProxy:
|
|
131
|
+
"""
|
|
132
|
+
The event loop time for assertions.
|
|
17
133
|
|
|
18
|
-
|
|
19
|
-
|
|
134
|
+
The fixture's numeric value is the loop time as a number of seconds
|
|
135
|
+
since the "time zero", which is usuaully the creation time
|
|
136
|
+
of the event loop, but can be adjusted by the ``start=…`` option.
|
|
20
137
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for marker in reversed(markers):
|
|
25
|
-
options.update(marker.kwargs)
|
|
26
|
-
enabled = bool(marker.args[0]) if marker.args else enabled
|
|
138
|
+
- It can be used in assertions & comparisons (``==``, ``<=``, etc).
|
|
139
|
+
- It can also be used in simple math (additions, substractions, etc).
|
|
140
|
+
- It can be converted to ``int()`` or ``float()``.
|
|
27
141
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
result.force_result(patched_loop)
|
|
32
|
-
else:
|
|
33
|
-
yield
|
|
142
|
+
It is an equivalent of a more wordy ``asyncio.get_running_loop().time()``.
|
|
143
|
+
"""
|
|
144
|
+
return timeproxies.LoopTimeProxy()
|
|
34
145
|
|
|
35
146
|
|
|
36
147
|
def pytest_configure(config: Any) -> None:
|
|
@@ -43,3 +154,163 @@ def pytest_addoption(parser: Any) -> None:
|
|
|
43
154
|
help="Force all (even marked) tests to the true loop time.")
|
|
44
155
|
group.addoption("--looptime", dest='looptime', action="store_const", const=True,
|
|
45
156
|
help="Run unmarked tests with the fake loop time by default.")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
EventLoopScopes = dict[str, list[str]] # {fixture_name -> [outer_scopes, …, innermost_scope]}
|
|
160
|
+
EVENT_LOOP_SCOPES = pytest.StashKey[EventLoopScopes]()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.hookimpl(wrapper=True)
|
|
164
|
+
def pytest_fixture_setup(fixturedef: pytest.FixtureDef[Any], request: pytest.FixtureRequest) -> Any:
|
|
165
|
+
# Setup as usual. We do the magic only afterwards, when we have the event loop created.
|
|
166
|
+
result = yield
|
|
167
|
+
|
|
168
|
+
# Only do the magic if in the area of our interest & only for fixtures making the event loops.
|
|
169
|
+
if _should_patch(fixturedef, request) and isinstance(result, asyncio.BaseEventLoop):
|
|
170
|
+
|
|
171
|
+
# Populate the helper mapper of names-to-scopes, as used in the test hook below.
|
|
172
|
+
if EVENT_LOOP_SCOPES not in request.session.stash:
|
|
173
|
+
request.session.stash[EVENT_LOOP_SCOPES] = {}
|
|
174
|
+
event_loop_scopes: EventLoopScopes = request.session.stash[EVENT_LOOP_SCOPES]
|
|
175
|
+
event_loop_scopes.setdefault(fixturedef.argname, []).append(fixturedef.scope)
|
|
176
|
+
|
|
177
|
+
# Patch the event loop at creation — even if unused and not enabled. We cannot patch later
|
|
178
|
+
# in the middle of the run: e.g. for a session-scoped loop used in a few tests out of many.
|
|
179
|
+
# NB: For the lowest "function" scope, we still cannot decide which options to use, since
|
|
180
|
+
# we do not know yet if it will be the running loop or not — so we cannot optimize here
|
|
181
|
+
# in order to patch-and-configure only once; we must patch here & configure+activate later.
|
|
182
|
+
result = patchers.patch_event_loop(result, _enabled=False)
|
|
183
|
+
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.hookimpl(wrapper=True)
|
|
188
|
+
def pytest_fixture_post_finalizer(fixturedef: pytest.FixtureDef[Any], request: pytest.FixtureRequest) -> Any:
|
|
189
|
+
# Cleanup the helper mapper of the fixture's names-to-scopes, as used in the test-running hook.
|
|
190
|
+
# Internal consistency check: some cases should not happen, but we do not fail if they do.
|
|
191
|
+
if EVENT_LOOP_SCOPES in request.session.stash:
|
|
192
|
+
event_loop_scopes: EventLoopScopes = request.session.stash[EVENT_LOOP_SCOPES]
|
|
193
|
+
if fixturedef.argname not in event_loop_scopes:
|
|
194
|
+
warnings.warn(
|
|
195
|
+
f"Fixture {fixturedef.argname!r} not found in the cache of scopes."
|
|
196
|
+
f" Report as a bug, please add a reproducible snippet.",
|
|
197
|
+
RuntimeWarning,
|
|
198
|
+
)
|
|
199
|
+
elif not event_loop_scopes[fixturedef.argname]:
|
|
200
|
+
warnings.warn(
|
|
201
|
+
f"Fixture {fixturedef.argname!r} has the empty cache of scopes."
|
|
202
|
+
f" Report as a bug, please add a reproducible snippet.",
|
|
203
|
+
RuntimeWarning,
|
|
204
|
+
)
|
|
205
|
+
elif event_loop_scopes[fixturedef.argname][-1] != fixturedef.scope:
|
|
206
|
+
warnings.warn(
|
|
207
|
+
f"Fixture {fixturedef.argname!r} has the broken cache of scopes:"
|
|
208
|
+
f" {event_loop_scopes[fixturedef.argname]!r}, expecting {fixturedef.scope!r}"
|
|
209
|
+
f" Report as a bug, please add a reproducible snippet.",
|
|
210
|
+
RuntimeWarning,
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
event_loop_scopes[fixturedef.argname][-1:] = []
|
|
214
|
+
|
|
215
|
+
# Go as usual.
|
|
216
|
+
return (yield)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# This hook is the latest (deepest) possible entrypoint before diving into the test function itself,
|
|
220
|
+
# with all the fixtures executed earlier, so that their setup time is not taken into account.
|
|
221
|
+
# Here, we know the actual running loop (out of many) chosen by pytest-asyncio & its marks/configs.
|
|
222
|
+
# The alternatives to consider — the subtle differences are unclear to me for now:
|
|
223
|
+
# - pytest_pyfunc_call(pyfuncitem)
|
|
224
|
+
# - pytest_runtest_call(item)
|
|
225
|
+
# - pytest_runtest_setup(item), wrapped as @hookimpl(trylast=True)
|
|
226
|
+
@pytest.hookimpl(wrapper=True)
|
|
227
|
+
def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Any:
|
|
228
|
+
|
|
229
|
+
# Get the running loop from the pre-populated & pre-resolved fixtures (done in the setup stage).
|
|
230
|
+
# This includes all the auto-used fixtures, but NOT the dynamic `getfixturevalue(…)` ones.
|
|
231
|
+
# Alternatively, use the private `pyfuncitem._request.getfixturevalue(…)`, though this is hacky.
|
|
232
|
+
funcargs: dict[str, Any] = pyfuncitem.funcargs
|
|
233
|
+
if 'event_loop_policy' in funcargs: # pytest-asyncio>=1.0.0
|
|
234
|
+
# This can be ANY event loop of ANY declared scope of pytest-asyncio.
|
|
235
|
+
policy: asyncio.AbstractEventLoopPolicy = funcargs['event_loop_policy']
|
|
236
|
+
running_loop = policy.get_event_loop()
|
|
237
|
+
elif 'event_loop' in funcargs: # pytest-asyncio<1.0.0
|
|
238
|
+
# The hook itself has NO "running" loop — because it is sync, not async.
|
|
239
|
+
running_loop = funcargs['event_loop']
|
|
240
|
+
else: # not pytest-asyncio? not our business!
|
|
241
|
+
return (yield)
|
|
242
|
+
|
|
243
|
+
# The event loop is not patched? We are doomed to fail, so let it run somehow on its own.
|
|
244
|
+
# This might happen if the custom event loop policy was set not by pytest-asyncio.
|
|
245
|
+
if not isinstance(running_loop, loops.LoopTimeEventLoop):
|
|
246
|
+
return (yield)
|
|
247
|
+
|
|
248
|
+
# If not enabled/enforced for this test, even if the event loop is patched, let it run as usual.
|
|
249
|
+
options: dict[str, Any] | None = _get_options(pyfuncitem)
|
|
250
|
+
if options is None:
|
|
251
|
+
return (yield)
|
|
252
|
+
|
|
253
|
+
# Finally, if enabled/enforced, activate the magic and run the test in the compacted time mode.
|
|
254
|
+
# We only activate the running loop for the test, not the other event loops used in fixtures.
|
|
255
|
+
running_loop.setup_looptime(**options)
|
|
256
|
+
with running_loop.looptime_enabled():
|
|
257
|
+
return (yield)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _should_patch(fixturedef: pytest.FixtureDef[Any], request: pytest.FixtureRequest) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
Check if the fixture should be patched (in case it is an event loop).
|
|
263
|
+
|
|
264
|
+
Only patch the implicit (hidden) event loops and their user-side overrides.
|
|
265
|
+
They are declared as internal with underscored names, but nevertheless.
|
|
266
|
+
Example implicit names: ``_session_event_loop`` … ``_function_event_loop``.
|
|
267
|
+
|
|
268
|
+
We do not intercept arbitrary fixtures or event loops of unknown plugins.
|
|
269
|
+
Custom event loops can be patched explicitly if needed.
|
|
270
|
+
"""
|
|
271
|
+
# pytest-asyncio<1.0.0 exposed the specific fixture; deprecated since >=0.23.0, removed >=1.0.0.
|
|
272
|
+
if fixturedef.argname == "event_loop":
|
|
273
|
+
return True
|
|
274
|
+
|
|
275
|
+
# pytest-asyncio>=1.0.0 exposes several event loops, one per scope, all hidden in the module.
|
|
276
|
+
asyncio_plugin = request.config.pluginmanager.getplugin("asyncio") # a module object
|
|
277
|
+
asyncio_names: set[str] = {
|
|
278
|
+
name for name in dir(asyncio_plugin) if _is_fixture(getattr(asyncio_plugin, name))
|
|
279
|
+
}
|
|
280
|
+
asyncio_module = asyncio_plugin.__name__
|
|
281
|
+
fixture_module = fixturedef.func.__module__
|
|
282
|
+
should_patch = fixture_module == asyncio_module or fixturedef.argname in asyncio_names
|
|
283
|
+
return should_patch
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _is_fixture(obj: Any) -> bool:
|
|
287
|
+
# Any of these internal names can be moved or renamed any time. Do our best to guess.
|
|
288
|
+
import _pytest.fixtures
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
if isinstance(obj, _pytest.fixtures.FixtureFunctionDefinition):
|
|
292
|
+
return True
|
|
293
|
+
except AttributeError:
|
|
294
|
+
pass
|
|
295
|
+
try:
|
|
296
|
+
if isinstance(obj, _pytest.fixtures.FixtureFunctionMarker):
|
|
297
|
+
return True
|
|
298
|
+
except AttributeError:
|
|
299
|
+
pass
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _get_options(node: _pytest.nodes.Node) -> dict[str, Any] | None:
|
|
304
|
+
"""Combine all the declared looptime options; None for disabled."""
|
|
305
|
+
|
|
306
|
+
# True means implicitly on; False means explicitly off; None means "only if marked".
|
|
307
|
+
flag: bool | None = node.config.getoption('looptime')
|
|
308
|
+
|
|
309
|
+
markers = list(node.iter_markers('looptime'))
|
|
310
|
+
enabled: bool = bool((markers or flag is True) and not flag is False)
|
|
311
|
+
options: dict[str, Any] = {}
|
|
312
|
+
for marker in reversed(markers):
|
|
313
|
+
options.update(marker.kwargs)
|
|
314
|
+
enabled = bool(marker.args[0]) if marker.args else enabled
|
|
315
|
+
|
|
316
|
+
return options if enabled else None
|
looptime/timeproxies.py
CHANGED
|
@@ -31,13 +31,3 @@ class LoopTimeProxy(math.Numeric):
|
|
|
31
31
|
@property
|
|
32
32
|
def _value(self) -> float:
|
|
33
33
|
return self._loop.time() if self._loop is not None else asyncio.get_running_loop().time()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
import pytest
|
|
38
|
-
except ImportError:
|
|
39
|
-
pass
|
|
40
|
-
else:
|
|
41
|
-
@pytest.fixture()
|
|
42
|
-
def looptime() -> LoopTimeProxy:
|
|
43
|
-
return LoopTimeProxy()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: looptime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3
|
|
4
4
|
Summary: Fast-forward asyncio event loop time (in tests)
|
|
5
5
|
Home-page: https://github.com/nolar/looptime
|
|
6
6
|
Author: Sergey Vasilyev
|
|
@@ -11,18 +11,28 @@ License: MIT
|
|
|
11
11
|
Project-URL: Bug Tracker, https://github.com/nolar/looptime/issues
|
|
12
12
|
Project-URL: Source Code, https://github.com/nolar/looptime
|
|
13
13
|
Keywords: asyncio,event loop,time,python,pytest
|
|
14
|
-
|
|
15
|
-
Requires-Python: >=3.7
|
|
14
|
+
Requires-Python: >=3.9
|
|
16
15
|
Description-Content-Type: text/markdown
|
|
17
16
|
License-File: LICENSE
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: keywords
|
|
23
|
+
Dynamic: license
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
Dynamic: maintainer
|
|
26
|
+
Dynamic: maintainer-email
|
|
27
|
+
Dynamic: project-url
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
18
30
|
|
|
19
31
|
# Fast-forward asyncio event loop time (in tests)
|
|
20
32
|
|
|
21
33
|
[](https://github.com/nolar/looptime/actions/workflows/thorough.yaml)
|
|
22
34
|
[](https://codecov.io/gh/nolar/looptime)
|
|
23
35
|
[](https://coveralls.io/github/nolar/looptime?branch=main)
|
|
24
|
-
[](https://lgtm.com/projects/g/nolar/looptime/alerts/)
|
|
25
|
-
[](https://lgtm.com/projects/g/nolar/looptime/context:python)
|
|
26
36
|
[](https://github.com/pre-commit/pre-commit)
|
|
27
37
|
|
|
28
38
|
## What?
|
|
@@ -37,11 +47,11 @@ The effects of time removal can be seen from both sides:
|
|
|
37
47
|
This hides the code overhead and network latencies from the time measurements,
|
|
38
48
|
making the loop time sharply and predictably advancing in configured steps.
|
|
39
49
|
|
|
40
|
-
* From the **observer's (i.e. your) point of view,**
|
|
50
|
+
* From the **observer's (i.e. your personal) point of view,**
|
|
41
51
|
all activities of the event loop, such as sleeps, events/conditions waits,
|
|
42
52
|
timeouts, "later" callbacks, happen in near-zero amount of the real time
|
|
43
|
-
(
|
|
44
|
-
This speeds up the tests
|
|
53
|
+
(due to the usual code execution overhead).
|
|
54
|
+
This speeds up the execution of tests without breaking the tests' time-based
|
|
45
55
|
design, even if they are designed to run in seconds or minutes.
|
|
46
56
|
|
|
47
57
|
For the latter case, there are a few exceptions when the event loop's activities
|
|
@@ -77,28 +87,28 @@ at the same time:
|
|
|
77
87
|
* One is for the coroutine-under-test which moves between states
|
|
78
88
|
in the background.
|
|
79
89
|
* Another one is for the test itself, which controls the flow
|
|
80
|
-
of that coroutine-under-test: it
|
|
90
|
+
of that coroutine-under-test: it schedules events, injects data, etc.
|
|
81
91
|
|
|
82
92
|
In textbook cases with simple coroutines that are more like regular functions,
|
|
83
93
|
it is possible to design a test so that it runs straight to the end in one hop
|
|
84
94
|
— with all the preconditions set and data prepared in advance in the test setup.
|
|
85
95
|
|
|
86
96
|
However, in the real-world cases, the tests often must verify that
|
|
87
|
-
the
|
|
97
|
+
the coroutine stops at some point, waits for a condition for some limited time,
|
|
88
98
|
and then passes or fails.
|
|
89
99
|
|
|
90
100
|
The problem is often "solved" by mocking the low-level coroutines of sleep/wait
|
|
91
101
|
that we expect the coroutine-under-test to call. But this violates the main
|
|
92
|
-
principle of good unit-tests: test the promise, not the implementation
|
|
102
|
+
principle of good unit-tests: **test the promise, not the implementation.**
|
|
93
103
|
Mocking and checking the low-level coroutines is based on the assumptions
|
|
94
104
|
of how the coroutine is implemented internally, which can change over time.
|
|
95
105
|
Good tests do not change on refactoring if the protocol remains the same.
|
|
96
106
|
|
|
97
107
|
Another (straightforward) approach is to not mock the low-level routines, but
|
|
98
|
-
to spend the real-world time, just in short bursts as hard-
|
|
99
|
-
Not only it makes the whole test-suite slower, it also brings the
|
|
100
|
-
time close to the values where the code overhead
|
|
101
|
-
|
|
108
|
+
to spend the real-world time, just in short bursts as hard-coded in the test.
|
|
109
|
+
Not only it makes the whole test-suite slower, it also brings the execution
|
|
110
|
+
time close to the values where the code overhead or measurement errors affect
|
|
111
|
+
the timing, which makes it difficult to assert on the coroutine's pure time.
|
|
102
112
|
|
|
103
113
|
|
|
104
114
|
## Solution
|
|
@@ -125,7 +135,7 @@ pip install pytest-asyncio
|
|
|
125
135
|
pip install looptime
|
|
126
136
|
```
|
|
127
137
|
|
|
128
|
-
Nothing is needed to make async tests
|
|
138
|
+
Nothing is needed to make async tests run with the fake time, it just works:
|
|
129
139
|
|
|
130
140
|
```python
|
|
131
141
|
import asyncio
|
|
@@ -221,14 +231,14 @@ for each setting separately (i.e. not the closest marker as a whole).
|
|
|
221
231
|
marked as using the fake loop time —including those not marked at all—
|
|
222
232
|
as if all tests were implicitly marked.
|
|
223
233
|
|
|
224
|
-
`--no-looptime` runs all tests —both marked and unmarked— with real time.
|
|
234
|
+
`--no-looptime` runs all tests —both marked and unmarked— with the real time.
|
|
225
235
|
This flag effectively disables the plugin.
|
|
226
236
|
|
|
227
237
|
|
|
228
238
|
## Settings
|
|
229
239
|
|
|
230
240
|
The marker accepts several settings for the test. The closest to the test
|
|
231
|
-
function applies. This lets you
|
|
241
|
+
function applies. This lets you define the test-suite defaults
|
|
232
242
|
and override them on the directory, module, class, function, or test level:
|
|
233
243
|
|
|
234
244
|
```python
|
|
@@ -256,7 +266,20 @@ or `start=lambda: random.random() * 100` to add some unpredictability.
|
|
|
256
266
|
|
|
257
267
|
`None` is treated the same as `0.0`.
|
|
258
268
|
|
|
259
|
-
The default is `0.0`.
|
|
269
|
+
The default is `0.0`. For reusable event loops, the default is to keep
|
|
270
|
+
the time untouched, which means `0.0` or the explicit value for the first test,
|
|
271
|
+
but then an ever-increasing value for the 2nd, 3rd, and further tests.
|
|
272
|
+
|
|
273
|
+
Note: pytest-asyncio 1.0.0+ introduced event loops with higher scopes,
|
|
274
|
+
e.g. class-, module-, packages-, session-scoped event loops used in tests.
|
|
275
|
+
Such event loops are reused, so their time continues growing through many tests.
|
|
276
|
+
However, if the test is explicitly configured with the start time,
|
|
277
|
+
that time is enforced to the event loop when the test function starts —
|
|
278
|
+
to satisfy the clearly declared intentions — even if the time moves backwards,
|
|
279
|
+
which goes against the nature of the time itself (monotonically growing).
|
|
280
|
+
This might lead to surprises in time measurements outside of the test,
|
|
281
|
+
e.g. in fixtures: the code durations can become negative, or the events can
|
|
282
|
+
happen (falsely) before they are scheduled (loop-clock-wise). Be careful.
|
|
260
283
|
|
|
261
284
|
|
|
262
285
|
### The end of time
|
|
@@ -273,6 +296,8 @@ e.g. with `asyncio.sleep(0)`, simple `await` statements, etc.
|
|
|
273
296
|
|
|
274
297
|
If set to `None`, there is no end of time, and the event loop runs
|
|
275
298
|
as long as needed. Note: `0` means ending the time immediately on start.
|
|
299
|
+
Be careful with the explicit ending time in higher-scoped event loops
|
|
300
|
+
of pytest-asyncio>=1.0.0, since they time increases through many tests.
|
|
276
301
|
|
|
277
302
|
If it is a callable, it is called once per event loop to get the value:
|
|
278
303
|
e.g. `end=lambda: time.monotonic() + 10`.
|
|
@@ -304,7 +329,7 @@ Normally, it should not fail. However, with fake time (without workarounds)
|
|
|
304
329
|
the following scenario is possible:
|
|
305
330
|
|
|
306
331
|
* `async_timeout` library sets its delayed timer at 9 seconds since now.
|
|
307
|
-
* the event loop notices that there is
|
|
332
|
+
* the event loop notices that there is only one timer at T0+9s.
|
|
308
333
|
* the event loop fast-forwards time to be `9`.
|
|
309
334
|
* since there are no other handles/timers, that timer is executed.
|
|
310
335
|
* `async_timeout` fails the test with `asyncio.TimeoutError`
|
|
@@ -359,11 +384,11 @@ However, with the fake time (with no workarounds), the following happens:
|
|
|
359
384
|
* The test suppresses the timeout, checks the assertion, and fails:
|
|
360
385
|
the sync event is still unset.
|
|
361
386
|
* A fraction of a second (e.g. `0.001` second) later, the thread starts,
|
|
362
|
-
calls the function
|
|
387
|
+
calls the function and sets the sync event, but it is too late.
|
|
363
388
|
|
|
364
389
|
Compared to the fake fast-forwarding time, even such fast things as threads
|
|
365
390
|
are too slow to start. Unfortunately, `looptime` and the event loop can
|
|
366
|
-
neither control what is happening outside of the event loop
|
|
391
|
+
neither control what is happening outside of the event loop nor predict
|
|
367
392
|
how long it will take.
|
|
368
393
|
|
|
369
394
|
To work around this, `looptime` remembers all calls to executors and then
|
|
@@ -375,11 +400,11 @@ So, the fake time and real time move along while waiting for executors.
|
|
|
375
400
|
Luckily for this case, in 1 or 2 such steps, the executor's thread will
|
|
376
401
|
do its job, the event will be set, so as the synchronous & asynchronous
|
|
377
402
|
futures of the executor. The latter one (the async future) will also
|
|
378
|
-
let the `await`
|
|
403
|
+
let the `await` move on.
|
|
379
404
|
|
|
380
405
|
The `idle_step` (`float` or `None`) setting is the duration of a single
|
|
381
406
|
time step when fast-forwarding the time if there are executors used —
|
|
382
|
-
i.e. if
|
|
407
|
+
i.e. if some synchronous tasks are running in the thread pools.
|
|
383
408
|
|
|
384
409
|
Note that the steps are both true-time and fake-time: they spend the same
|
|
385
410
|
amount of the observer's true time as they increment the loop's fake time.
|
|
@@ -413,7 +438,7 @@ With no workarounds, the test will hang forever waiting for the i/o to happen.
|
|
|
413
438
|
This mostly happens when the only thing left in the event loop is the i/o,
|
|
414
439
|
all internal scheduled callbacks are gone.
|
|
415
440
|
|
|
416
|
-
`looptime` can
|
|
441
|
+
`looptime` can artificially limit the lifetime of the event loop.
|
|
417
442
|
This can be done as a default setting for the whole test suite, for example.
|
|
418
443
|
|
|
419
444
|
The `idle_timeout` (`float` or `None`) setting is the true-time limit
|
|
@@ -459,7 +484,7 @@ while `looptime(end=N)` applies to the lifecycle of the whole event loop,
|
|
|
459
484
|
which is usually the duration of the whole test and monotonically increases.
|
|
460
485
|
|
|
461
486
|
Second, `looptime(end=N)` syncs the loop time with the real time for N seconds,
|
|
462
|
-
i.e. it does not instantly fast-
|
|
487
|
+
i.e. it does not instantly fast-forward the loop time when the loops
|
|
463
488
|
attempts to make an "infinite sleep" (technically, `selector.select(None)`).
|
|
464
489
|
`async_timeout.timeout()` and `asyncio.wait_for()` set a delayed callback,
|
|
465
490
|
so the time fast-forwards to it on the first possible occasion.
|
|
@@ -530,6 +555,63 @@ For example, with the resolution `0.001`, the time
|
|
|
530
555
|
everything smaller than `0.001` becomes `0` and probably misbehaves._
|
|
531
556
|
|
|
532
557
|
|
|
558
|
+
### Time magic coverage
|
|
559
|
+
|
|
560
|
+
The time compaction magic is enabled only for the duration of the test,
|
|
561
|
+
i.e. the test function — but not the fixtures.
|
|
562
|
+
The fixtures run in the real (wall-clock) time.
|
|
563
|
+
|
|
564
|
+
The options (including the force starting time) are applied at the test function
|
|
565
|
+
starting moment, not when it is setting up the fixtures (even function-scoped).
|
|
566
|
+
|
|
567
|
+
This is caused by a new concept of multiple co-existing event loops
|
|
568
|
+
in pytest-asyncio>=1.0.0:
|
|
569
|
+
|
|
570
|
+
- It is unclear which options to apply to higher-scoped fixtures
|
|
571
|
+
used by many tests, which themselves use higher-scoped event loops —
|
|
572
|
+
especially in selective partial runs. Technically, it is the 1st test,
|
|
573
|
+
with the options of 2nd and further tests simply ignored.
|
|
574
|
+
- It is impossible to guess which event loop will be the running loop
|
|
575
|
+
in the test until we reach the test itself, i.e. we do not know this
|
|
576
|
+
when setting up the fixtures, even function-scoped fixtures.
|
|
577
|
+
- There is no way to cover the fixture teardown (no hook in pytest),
|
|
578
|
+
only for the fixture setup and post-teardown cleanup.
|
|
579
|
+
|
|
580
|
+
As such, this functionality (covering of function-scoped fixtures)
|
|
581
|
+
was abandoned — since it was never promised, tested, or documented —
|
|
582
|
+
plus an assumption that it was never used by anyone (it should not be).
|
|
583
|
+
It was rather a side effect of the previous implemention,
|
|
584
|
+
which is not available or possible anymore.
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
### pytest-asyncio>=1.0.0
|
|
588
|
+
|
|
589
|
+
As it is said above, pytest-asyncio>=1.0.0 introduced several co-existing
|
|
590
|
+
event loops of different scopes. The time compaction in these event loops
|
|
591
|
+
is NOT activated. Only the running loop of the test function is activated.
|
|
592
|
+
|
|
593
|
+
Configuring and activating multiple co-existing event loops brings a few
|
|
594
|
+
conceptual challenges, which require a good sample case to look into,
|
|
595
|
+
and some time to think.
|
|
596
|
+
|
|
597
|
+
Would you need time compaction in your fixtures of higher scopes,
|
|
598
|
+
do it explicitly:
|
|
599
|
+
|
|
600
|
+
```python
|
|
601
|
+
import asyncio
|
|
602
|
+
import pytest
|
|
603
|
+
|
|
604
|
+
@pytest.fixture
|
|
605
|
+
async def fixt():
|
|
606
|
+
loop = asyncio.get_running_loop()
|
|
607
|
+
loop.setup_looptime(start=123, end=456)
|
|
608
|
+
with loop.looptime_enabled():
|
|
609
|
+
await do_things()
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
There is #11 to add a feature to do this automatically, but it is not yet done.
|
|
613
|
+
|
|
614
|
+
|
|
533
615
|
## Extras
|
|
534
616
|
|
|
535
617
|
### Chronometers
|
|
@@ -578,7 +660,7 @@ async def test_me(chronometer, event_loop):
|
|
|
578
660
|
|
|
579
661
|
### Loop time assertions
|
|
580
662
|
|
|
581
|
-
The `looptime` **fixture** is
|
|
663
|
+
The `looptime` **fixture** is syntax sugar for easy loop time assertions::
|
|
582
664
|
|
|
583
665
|
```python
|
|
584
666
|
import asyncio
|
|
@@ -593,13 +675,13 @@ async def test_me(looptime):
|
|
|
593
675
|
|
|
594
676
|
Technically, it is a proxy object to `asyncio.get_running_loop().time()`.
|
|
595
677
|
The proxy object supports the direct comparison with numbers (integers/floats),
|
|
596
|
-
so as some basic arithmetics (adding,
|
|
678
|
+
so as some basic arithmetics (adding, subtracting, multiplication, etc).
|
|
597
679
|
However, it adjusts to the time precision of 1 nanosecond (1e-9): every digit
|
|
598
680
|
beyond that precision is ignored — so you can be not afraid of
|
|
599
681
|
`123.456/1.2` suddenly becoming `102.88000000000001` and not equal to `102.88`
|
|
600
682
|
(as long as the time proxy object is used and not converted to a native float).
|
|
601
683
|
|
|
602
|
-
The proxy object can be used to create a new proxy
|
|
684
|
+
The proxy object can be used to create a new proxy that is bound to a specific
|
|
603
685
|
event loop (it works for loops both with fake- and real-world time)::
|
|
604
686
|
|
|
605
687
|
```python
|
|
@@ -623,6 +705,8 @@ the loop time also includes the time of all fixtures setups.
|
|
|
623
705
|
Do you use a custom event loop? No problem! Create a test-specific descendant
|
|
624
706
|
with the provided mixin — and it will work the same as the default event loop.
|
|
625
707
|
|
|
708
|
+
For `pytest-asyncio<1.0.0`:
|
|
709
|
+
|
|
626
710
|
```python
|
|
627
711
|
import looptime
|
|
628
712
|
import pytest
|
|
@@ -638,6 +722,29 @@ def event_loop():
|
|
|
638
722
|
return LooptimeCustomEventLoop()
|
|
639
723
|
```
|
|
640
724
|
|
|
725
|
+
For `pytest-asyncio>=1.0.0`:
|
|
726
|
+
|
|
727
|
+
```python
|
|
728
|
+
import asyncio
|
|
729
|
+
import looptime
|
|
730
|
+
import pytest
|
|
731
|
+
from wherever import CustomEventLoop
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
class LooptimeCustomEventLoop(looptime.LoopTimeEventLoop, CustomEventLoop):
|
|
735
|
+
pass
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
class LooptimeCustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
|
|
739
|
+
def new_event_loop(self):
|
|
740
|
+
return LooptimeCustomEventLoop()
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
@pytest.fixture(scope='session')
|
|
744
|
+
def event_loop_policy():
|
|
745
|
+
return LooptimeCustomEventLoopPolicy()
|
|
746
|
+
```
|
|
747
|
+
|
|
641
748
|
Only selector-based event loops are supported: the event loop must rely on
|
|
642
749
|
`self._selector.select(timeout)` to sleep for `timeout` true-time seconds.
|
|
643
750
|
Everything that inherits from `asyncio.BaseEventLoop` should work.
|
|
@@ -645,6 +752,8 @@ Everything that inherits from `asyncio.BaseEventLoop` should work.
|
|
|
645
752
|
You can also patch almost any event loop class or event loop object
|
|
646
753
|
the same way as `looptime` does that (via some dirty hackery):
|
|
647
754
|
|
|
755
|
+
For `pytest-asyncio<1.0.0`:
|
|
756
|
+
|
|
648
757
|
```python
|
|
649
758
|
import asyncio
|
|
650
759
|
import looptime
|
|
@@ -657,6 +766,25 @@ def event_loop():
|
|
|
657
766
|
return looptime.patch_event_loop(loop)
|
|
658
767
|
```
|
|
659
768
|
|
|
769
|
+
For `pytest-asyncio>=1.0.0`:
|
|
770
|
+
|
|
771
|
+
```python
|
|
772
|
+
import asyncio
|
|
773
|
+
import looptime
|
|
774
|
+
import pytest
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
class LooptimeEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
|
|
778
|
+
def new_event_loop(self):
|
|
779
|
+
loop = super().new_event_loop()
|
|
780
|
+
return looptime.patch_event_loop(loop)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
@pytest.fixture(scope='session')
|
|
784
|
+
def event_loop_policy():
|
|
785
|
+
return LooptimeEventLoopPolicy()
|
|
786
|
+
```
|
|
787
|
+
|
|
660
788
|
`looptime.make_event_loop_class(cls)` constructs a new class that inherits
|
|
661
789
|
from the referenced class and the specialised event loop class mentioned above.
|
|
662
790
|
The resulting classes are cached, so it can be safely called multiple times.
|
|
@@ -668,5 +796,3 @@ constructed one. For those who care, it is an equivalent of the following hack
|
|
|
668
796
|
```python
|
|
669
797
|
loop.__class__ = looptime.make_event_loop_class(loop.__class__)
|
|
670
798
|
```
|
|
671
|
-
|
|
672
|
-
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
looptime/__init__.py,sha256=oxt_f8ZdFSTmpjSRNMAnwNjyp4ajwucpFKnpisGPDik,497
|
|
2
|
+
looptime/chronometers.py,sha256=AgLYweIq-bjfxhR0Qqt0QOXrKR48fornjEIhNMrycOo,1803
|
|
3
|
+
looptime/loops.py,sha256=ccrfsa15W0JW2LLYJUEagH03XlE2Lw2Uxe_W9ZuuQNU,17621
|
|
4
|
+
looptime/math.py,sha256=50d4ul1XEMzhvtBnYqYdkLwiovvhqEWu00q9ZySHVR0,3689
|
|
5
|
+
looptime/patchers.py,sha256=T8eDCyf7bPGAKm1cMbi6V3xTtH7VbJpYgB_kDIr6tOw,1252
|
|
6
|
+
looptime/plugin.py,sha256=XtJ7NxLk2SFXQPRiQHmpenfcvQ44VVFKnlw7446AUuU,14869
|
|
7
|
+
looptime/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
looptime/timeproxies.py,sha256=dhFng7mw8V6icDnKeHw8SNk74iXsOCtCe2mD8kRO-uI,874
|
|
9
|
+
looptime-0.3.dist-info/licenses/LICENSE,sha256=QBTPjghK-H2Ctv9yejhXyT3zrf6HTKCETMQaOGotl28,1091
|
|
10
|
+
looptime-0.3.dist-info/METADATA,sha256=82gW_FB6vxusJLpIrZPIWXaLzNAQAONA7V33e2pyZ70,27917
|
|
11
|
+
looptime-0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
looptime-0.3.dist-info/entry_points.txt,sha256=KSBS46wdBGv7lmhfsCyAPsWlQNbmkktVYZc2Ze0spcU,135
|
|
13
|
+
looptime-0.3.dist-info/top_level.txt,sha256=S4Ty0YHUCB1cf-PE5YiskuOSiMctlsLrUp3c2Yywzz4,9
|
|
14
|
+
looptime-0.3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
15
|
+
looptime-0.3.dist-info/RECORD,,
|
looptime-0.1.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
looptime/__init__.py,sha256=xDfdZVz49skblf5TGh9PfPG1EDS8VZnHOLKDft_hiWM,465
|
|
2
|
-
looptime/chronometers.py,sha256=AgLYweIq-bjfxhR0Qqt0QOXrKR48fornjEIhNMrycOo,1803
|
|
3
|
-
looptime/loops.py,sha256=dREp8usEU3Bdt8VhGGmjji9lZ0ud0djTQ8VCr66eabg,14706
|
|
4
|
-
looptime/math.py,sha256=fgtKLHWry7ivZNHHqakUcK_A-YP2nTVpZ1yZfMm0Y-M,3598
|
|
5
|
-
looptime/patchers.py,sha256=T8eDCyf7bPGAKm1cMbi6V3xTtH7VbJpYgB_kDIr6tOw,1252
|
|
6
|
-
looptime/plugin.py,sha256=Cspm-sTZ3q6K9gX0K0Jinr8CxOBBrHmdo52kyMz2ZCI,1818
|
|
7
|
-
looptime/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
looptime/timeproxies.py,sha256=9E5pFcwMv4Rr2K7bqOa0ucfdXbUib7C9122WKIf6pNk,1024
|
|
9
|
-
looptime-0.1.dist-info/LICENSE,sha256=QBTPjghK-H2Ctv9yejhXyT3zrf6HTKCETMQaOGotl28,1091
|
|
10
|
-
looptime-0.1.dist-info/METADATA,sha256=KbbgHMn3IfOHqG0qLGKeFFlNriGJwABxWkHEnF9PkEw,23800
|
|
11
|
-
looptime-0.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
12
|
-
looptime-0.1.dist-info/entry_points.txt,sha256=KiodpY8Q7SvZrrbmSWbck3HTB65NYJQkIA0wij4bjQ4,136
|
|
13
|
-
looptime-0.1.dist-info/top_level.txt,sha256=S4Ty0YHUCB1cf-PE5YiskuOSiMctlsLrUp3c2Yywzz4,9
|
|
14
|
-
looptime-0.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
15
|
-
looptime-0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|