Tools & Helpers

Chronometers

For convenience, the library also provides a class and a fixture to measure the duration of arbitrary code blocks in real-world time:

It can be used as a sync or async context manager:

import asyncio
import pytest

@pytest.mark.asyncio
@pytest.mark.looptime
async def test_me(chronometer):
    with chronometer:
        await asyncio.sleep(1)
        await asyncio.sleep(1)
    assert chronometer.seconds < 0.01  # random code overhead

Usually, the loop-time duration is not needed or can be retrieved via asyncio.get_running_loop().time(). If needed, it can be measured using the provided context manager class with the event loop’s clock:

import asyncio
import looptime
import pytest

@pytest.mark.asyncio
@pytest.mark.looptime(start=100)
async def test_me(chronometer, event_loop):
    with chronometer, looptime.Chronometer(event_loop.time) as loopometer:
        await asyncio.sleep(1)
        await asyncio.sleep(1)
    assert chronometer.seconds < 0.01  # random code overhead
    assert loopometer.seconds == 2  # precise timing, no code overhead
    assert event_loop.time() == 102

Assertions

The looptime fixture is syntactic sugar for easy loop time assertions:

import asyncio
import pytest

@pytest.mark.asyncio
@pytest.mark.looptime(start=100)
async def test_me(looptime):
    await asyncio.sleep(1.23)
    assert looptime == 101.23

Technically, it is a proxy object for asyncio.get_running_loop().time(). The proxy object supports direct comparison with numbers (integers/floats), as well as some basic arithmetic (addition, subtraction, multiplication, etc.). However, it adjusts to a time precision of 1 nanosecond (1e-9): every digit beyond that precision is ignored — so you do not need to be afraid of 123.456/1.2 suddenly becoming 102.88000000000001 and not equal to 102.88 (as long as the time proxy object is used and not converted to a native float).

The proxy object can be used to create a new proxy that is bound to a specific event loop with the @ operation (it works for loops with both fake and real-world time):

import asyncio
from looptime import patch_event_loop

def test_me(looptime):
    new_loop = patch_event_loop(asyncio.new_event_loop(), start=100)
    new_loop.run_until_complete(asyncio.sleep(1.23))
    assert looptime @ new_loop == 101.23

Keep in mind that it is not the same as Chronometer for the whole test. The time proxy reflects the time of the loop, not the duration of the test: the loop time can start at a non-zero point; even if it starts at zero, the loop time also includes the time of all fixture setups.

Custom event loops & mixins

Do you use a custom event loop? No problem! Create a test-specific descendant with the provided mixin — and it will work the same as the default event loop.

For pytest-asyncio<1.0.0, use the event_loop fixture:

import looptime
import pytest
from wherever import CustomEventLoop


class LooptimeCustomEventLoop(looptime.LoopTimeEventLoop, CustomEventLoop):
  pass


@pytest.fixture
def event_loop():
    return LooptimeCustomEventLoop()

For pytest-asyncio>=1.0.0, use the event_loop_policy:

import asyncio
import looptime
import pytest
from wherever import CustomEventLoop


class LooptimeCustomEventLoop(looptime.LoopTimeEventLoop, CustomEventLoop):
    pass


class LooptimeCustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
    def new_event_loop(self):
        return LooptimeCustomEventLoop()


@pytest.fixture(scope='session')
def event_loop_policy():
    return LooptimeCustomEventLoopPolicy()

Only selector-based event loops are supported: the event loop must rely on self._selector.select(timeout) to sleep for timeout true-time seconds. Everything that inherits from asyncio.BaseEventLoop should work, but a more generic asyncio.AbstractEventLoop might be a problem.

You can also patch almost any event loop class or event loop object the same way as looptime does (via some dirty hackery):

For pytest-asyncio<1.0.0 and the even_loop fixture:

import asyncio
import looptime
import pytest


@pytest.fixture
def event_loop():
    loop = asyncio.new_event_loop()
    return looptime.patch_event_loop(loop)

For pytest-asyncio>=1.0.0 and the event_loop_policy fixture:

import asyncio
import looptime
import pytest


class LooptimeEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
    def new_event_loop(self):
        loop = super().new_event_loop()
        return looptime.patch_event_loop(loop)


@pytest.fixture(scope='session')
def event_loop_policy():
    return LooptimeEventLoopPolicy()

looptime.make_event_loop_class(cls) constructs a new class that inherits from the referenced class and the specialized event loop class mentioned above. The resulting classes are cached, so it can be safely called multiple times.

looptime.patch_event_loop() replaces the event loop’s class with the newly constructed one. For those who care, it is an equivalent of the following hack (some restrictions apply to the derived class).

In general, patching the existing event loop instance is done by this hack:

loop.__class__ = looptime.make_event_loop_class(loop.__class__)