Skip to content

Freezing

provide.testkit.time.freezing

Time freezing fixtures for testing.

Fixtures for freezing time and controlling time flow in tests.

Classes

Functions

freeze_time

freeze_time() -> (
    Callable[[datetime.datetime | None], FrozenTime]
)

Fixture to freeze time at a specific point.

Returns:

Type Description
Callable[[datetime | None], FrozenTime]

Function that freezes time and returns a context manager.

Example

def test_with_frozen_time(freeze_time): ... with freeze_time(datetime.datetime(2024, 1, 1)) as frozen: ... # Time is frozen at 2024-01-01 ... frozen.tick(seconds=60) # Advance by 60 seconds

Source code in provide/testkit/time/freezing.py
@pytest.fixture
def freeze_time() -> Callable[[datetime.datetime | None], FrozenTime]:
    """Fixture to freeze time at a specific point.

    Returns:
        Function that freezes time and returns a context manager.

    Example:
        >>> def test_with_frozen_time(freeze_time):
        ...     with freeze_time(datetime.datetime(2024, 1, 1)) as frozen:
        ...         # Time is frozen at 2024-01-01
        ...         frozen.tick(seconds=60)  # Advance by 60 seconds
    """

    def _freeze(at: datetime.datetime | None = None) -> FrozenTime:
        """Freeze time at a specific point.

        Args:
            at: Optional datetime to freeze at (defaults to now)

        Returns:
            FrozenTime context manager
        """
        return FrozenTime(at)

    return _freeze

time_machine

time_machine(request: FixtureRequest) -> TimeMachine

Advanced time manipulation fixture.

Yields:

Type Description
TimeMachine

TimeMachine instance for time manipulation.

IMPORTANT: Uses request.addfinalizer() to ensure patches are stopped BEFORE pytest-asyncio creates event loops for the next test. Also forcibly closes ALL event loops to prevent cached frozen time.monotonic references.

Example

def test_with_time_machine(time_machine): ... time_machine.freeze(at=time.time()) ... # Perform tests with frozen time ... time_machine.jump(seconds=60) # Jump forward ... time_machine.unfreeze()

Source code in provide/testkit/time/freezing.py
@pytest.fixture
def time_machine(request: pytest.FixtureRequest) -> TimeMachine:
    """Advanced time manipulation fixture.

    Yields:
        TimeMachine instance for time manipulation.

    IMPORTANT: Uses request.addfinalizer() to ensure patches are stopped
    BEFORE pytest-asyncio creates event loops for the next test. Also forcibly
    closes ALL event loops to prevent cached frozen time.monotonic references.

    Example:
        >>> def test_with_time_machine(time_machine):
        ...     time_machine.freeze(at=time.time())
        ...     # Perform tests with frozen time
        ...     time_machine.jump(seconds=60)  # Jump forward
        ...     time_machine.unfreeze()
    """
    machine = TimeMachine()

    # Register cleanup with highest priority (runs before standard teardown)
    # This ensures time patches are stopped before pytest-asyncio creates
    # event loops for the next test
    def cleanup_patches() -> None:
        machine.cleanup()

        # NUCLEAR OPTION: Close ALL event loops to force fresh creation
        # This is necessary because event loops cache time.monotonic references
        # at creation time, and those cached values persist even after patches stop
        try:
            import asyncio

            with suppress(RuntimeError):
                loop = asyncio.get_event_loop()
                if not loop.is_running() and not loop.is_closed():
                    loop.close()

            # Also close the running loop if there is one
            with suppress(RuntimeError):
                asyncio.get_running_loop()
                # Can't close running loop, but we can stop it

        except Exception:
            pass

    request.addfinalizer(cleanup_patches)

    yield machine

    # Also call cleanup here as backup (defensive)
    machine.cleanup()