"""Indirection for time functions. We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect pytest runtime information (issue #185). Fixture "mock_timing" also interacts with this module for pytest's own tests. """ from __future__ import annotations import dataclasses from datetime import datetime from datetime import timezone from time import perf_counter from time import sleep from time import time from typing import TYPE_CHECKING if TYPE_CHECKING: from pytest import MonkeyPatch @dataclasses.dataclass(frozen=True) class Instant: """ Represents an instant in time, used to both get the timestamp value and to measure the duration of a time span. Inspired by Rust's `std::time::Instant`. """ # Creation time of this instant, using time.time(), to measure actual time. # Note: using a `lambda` to correctly get the mocked time via `MockTiming`. time: float = dataclasses.field(default_factory=lambda: time(), init=False) # Performance counter tick of the instant, used to measure precise elapsed time. # Note: using a `lambda` to correctly get the mocked time via `MockTiming`. perf_count: float = dataclasses.field( default_factory=lambda: perf_counter(), init=False ) def elapsed(self) -> Duration: """Measure the duration since `Instant` was created.""" return Duration(start=self, stop=Instant()) def as_utc(self) -> datetime: """Instant as UTC datetime.""" return datetime.fromtimestamp(self.time, timezone.utc) @dataclasses.dataclass(frozen=True) class Duration: """A span of time as measured by `Instant.elapsed()`.""" start: Instant stop: Instant @property def seconds(self) -> float: """Elapsed time of the duration in seconds, measured using a performance counter for precise timing.""" return self.stop.perf_count - self.start.perf_count @dataclasses.dataclass class MockTiming: """Mocks _pytest.timing with a known object that can be used to control timing in tests deterministically. pytest itself should always use functions from `_pytest.timing` instead of `time` directly. This then allows us more control over time during testing, if testing code also uses `_pytest.timing` functions. Time is static, and only advances through `sleep` calls, thus tests might sleep over large numbers and obtain accurate time() calls at the end, making tests reliable and instant.""" _current_time: float = datetime(2020, 5, 22, 14, 20, 50).timestamp() def sleep(self, seconds: float) -> None: self._current_time += seconds def time(self) -> float: return self._current_time def patch(self, monkeypatch: MonkeyPatch) -> None: from _pytest import timing # noqa: PLW0406 monkeypatch.setattr(timing, "sleep", self.sleep) monkeypatch.setattr(timing, "time", self.time) monkeypatch.setattr(timing, "perf_counter", self.time) __all__ = ["perf_counter", "sleep", "time"]