| """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`. |
| """ |
|
|
| |
| |
| time: float = dataclasses.field(default_factory=lambda: time(), init=False) |
|
|
| |
| |
| 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 |
|
|
| monkeypatch.setattr(timing, "sleep", self.sleep) |
| monkeypatch.setattr(timing, "time", self.time) |
| monkeypatch.setattr(timing, "perf_counter", self.time) |
|
|
|
|
| __all__ = ["perf_counter", "sleep", "time"] |
|
|