Skip to content

Commit 02f595f

Browse files
committed
Add TimeMachine testing utility
Introduce a TimeMachine utility for tests providing freeze/unfreeze/advance/rewind/set_time and time() inspection, with context-manager support. Also add MonkeyPatchedTimeMachine which temporarily replaces time.time while in a context and restores it on exit. Convenience helpers freeze_time and advance_time are included to create common frozen TimeMachine instances. This allows deterministic time manipulation in tests.
1 parent 4086c53 commit 02f595f

1 file changed

Lines changed: 98 additions & 0 deletions

File tree

ratelink/testing/time_machine.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import time
2+
from typing import Optional
3+
4+
class TimeMachine:
5+
def __init__(self):
6+
self._frozen = False
7+
self._frozen_time: Optional[float] = None
8+
self._offset: float = 0.0
9+
self._base_time: Optional[float] = None
10+
11+
def freeze(self, at: Optional[float] = None):
12+
if at is None:
13+
at = time.time() + self._offset
14+
15+
self._frozen = True
16+
self._frozen_time = at
17+
18+
def unfreeze(self):
19+
if self._frozen and self._frozen_time is not None:
20+
real_now = time.time()
21+
self._offset = self._frozen_time - real_now
22+
23+
self._frozen = False
24+
self._frozen_time = None
25+
26+
def advance(self, seconds: float):
27+
if self._frozen and self._frozen_time is not None:
28+
self._frozen_time += seconds
29+
else:
30+
self._offset += seconds
31+
32+
def rewind(self, seconds: float):
33+
self.advance(-seconds)
34+
35+
def time(self) -> float:
36+
if self._frozen and self._frozen_time is not None:
37+
return self._frozen_time
38+
else:
39+
return time.time() + self._offset
40+
41+
def reset(self):
42+
self._frozen = False
43+
self._frozen_time = None
44+
self._offset = 0.0
45+
self._base_time = None
46+
47+
def set_time(self, timestamp: float):
48+
self._frozen = True
49+
self._frozen_time = timestamp
50+
51+
def travel_to(self, timestamp: float):
52+
self.set_time(timestamp)
53+
54+
def get_offset(self) -> float:
55+
if self._frozen and self._frozen_time is not None:
56+
return self._frozen_time - time.time()
57+
else:
58+
return self._offset
59+
60+
def is_frozen(self) -> bool:
61+
return self._frozen
62+
63+
def __enter__(self):
64+
return self
65+
66+
def __exit__(self, exc_type, exc_val, exc_tb):
67+
self.reset()
68+
return False
69+
70+
class MonkeyPatchedTimeMachine(TimeMachine):
71+
def __init__(self):
72+
super().__init__()
73+
self._original_time = None
74+
75+
def __enter__(self):
76+
import time as time_module
77+
self._original_time = time_module.time
78+
time_module.time = self.time
79+
return self
80+
81+
def __exit__(self, exc_type, exc_val, exc_tb):
82+
if self._original_time is not None:
83+
import time as time_module
84+
time_module.time = self._original_time
85+
86+
self.reset()
87+
return False
88+
89+
def freeze_time(at: Optional[float] = None):
90+
tm = TimeMachine()
91+
tm.freeze(at)
92+
return tm
93+
94+
def advance_time(seconds: float):
95+
tm = TimeMachine()
96+
tm.freeze()
97+
tm.advance(seconds)
98+
return tm

0 commit comments

Comments
 (0)