timing.py 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. """Indirection for time functions.
  2. We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect
  3. pytest runtime information (issue #185).
  4. Fixture "mock_timing" also interacts with this module for pytest's own tests.
  5. """
  6. from __future__ import annotations
  7. import dataclasses
  8. from datetime import datetime
  9. from datetime import timezone
  10. from time import perf_counter
  11. from time import sleep
  12. from time import time
  13. from typing import TYPE_CHECKING
  14. if TYPE_CHECKING:
  15. from pytest import MonkeyPatch
  16. @dataclasses.dataclass(frozen=True)
  17. class Instant:
  18. """
  19. Represents an instant in time, used to both get the timestamp value and to measure
  20. the duration of a time span.
  21. Inspired by Rust's `std::time::Instant`.
  22. """
  23. # Creation time of this instant, using time.time(), to measure actual time.
  24. # Note: using a `lambda` to correctly get the mocked time via `MockTiming`.
  25. time: float = dataclasses.field(default_factory=lambda: time(), init=False)
  26. # Performance counter tick of the instant, used to measure precise elapsed time.
  27. # Note: using a `lambda` to correctly get the mocked time via `MockTiming`.
  28. perf_count: float = dataclasses.field(
  29. default_factory=lambda: perf_counter(), init=False
  30. )
  31. def elapsed(self) -> Duration:
  32. """Measure the duration since `Instant` was created."""
  33. return Duration(start=self, stop=Instant())
  34. def as_utc(self) -> datetime:
  35. """Instant as UTC datetime."""
  36. return datetime.fromtimestamp(self.time, timezone.utc)
  37. @dataclasses.dataclass(frozen=True)
  38. class Duration:
  39. """A span of time as measured by `Instant.elapsed()`."""
  40. start: Instant
  41. stop: Instant
  42. @property
  43. def seconds(self) -> float:
  44. """Elapsed time of the duration in seconds, measured using a performance counter for precise timing."""
  45. return self.stop.perf_count - self.start.perf_count
  46. @dataclasses.dataclass
  47. class MockTiming:
  48. """Mocks _pytest.timing with a known object that can be used to control timing in tests
  49. deterministically.
  50. pytest itself should always use functions from `_pytest.timing` instead of `time` directly.
  51. This then allows us more control over time during testing, if testing code also
  52. uses `_pytest.timing` functions.
  53. Time is static, and only advances through `sleep` calls, thus tests might sleep over large
  54. numbers and obtain accurate time() calls at the end, making tests reliable and instant."""
  55. _current_time: float = datetime(2020, 5, 22, 14, 20, 50).timestamp()
  56. def sleep(self, seconds: float) -> None:
  57. self._current_time += seconds
  58. def time(self) -> float:
  59. return self._current_time
  60. def patch(self, monkeypatch: MonkeyPatch) -> None:
  61. # pylint: disable-next=import-self
  62. from _pytest import timing # noqa: PLW0406
  63. monkeypatch.setattr(timing, "sleep", self.sleep)
  64. monkeypatch.setattr(timing, "time", self.time)
  65. monkeypatch.setattr(timing, "perf_counter", self.time)
  66. __all__ = ["perf_counter", "sleep", "time"]