provide-testkit 0.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- provide/__init__.py +3 -0
- provide/testkit/__init__.py +248 -0
- provide/testkit/archive/__init__.py +24 -0
- provide/testkit/archive/fixtures.py +217 -0
- provide/testkit/cli.py +229 -0
- provide/testkit/common/__init__.py +32 -0
- provide/testkit/common/fixtures.py +234 -0
- provide/testkit/crypto.py +163 -0
- provide/testkit/environment.py +79 -0
- provide/testkit/file/__init__.py +40 -0
- provide/testkit/file/content_fixtures.py +275 -0
- provide/testkit/file/directory_fixtures.py +105 -0
- provide/testkit/file/fixtures.py +49 -0
- provide/testkit/file/special_fixtures.py +141 -0
- provide/testkit/fixtures.py +52 -0
- provide/testkit/harness.py +122 -0
- provide/testkit/hub.py +22 -0
- provide/testkit/logger/__init__.py +39 -0
- provide/testkit/logger/hooks.py +100 -0
- provide/testkit/logger/reset.py +230 -0
- provide/testkit/main.py +22 -0
- provide/testkit/mocking/__init__.py +46 -0
- provide/testkit/mocking/fixtures.py +340 -0
- provide/testkit/process/__init__.py +48 -0
- provide/testkit/process/async_fixtures.py +410 -0
- provide/testkit/process/fixtures.py +54 -0
- provide/testkit/process/subprocess_fixtures.py +208 -0
- provide/testkit/quality/__init__.py +101 -0
- provide/testkit/quality/artifacts.py +360 -0
- provide/testkit/quality/base.py +158 -0
- provide/testkit/quality/cli.py +394 -0
- provide/testkit/quality/complexity/__init__.py +30 -0
- provide/testkit/quality/complexity/analyzer.py +392 -0
- provide/testkit/quality/complexity/fixture.py +196 -0
- provide/testkit/quality/coverage/__init__.py +36 -0
- provide/testkit/quality/coverage/fixture.py +236 -0
- provide/testkit/quality/coverage/reporter.py +150 -0
- provide/testkit/quality/coverage/tracker.py +313 -0
- provide/testkit/quality/decorators.py +380 -0
- provide/testkit/quality/documentation/__init__.py +29 -0
- provide/testkit/quality/documentation/checker.py +361 -0
- provide/testkit/quality/documentation/fixture.py +187 -0
- provide/testkit/quality/profiling/__init__.py +30 -0
- provide/testkit/quality/profiling/fixture.py +332 -0
- provide/testkit/quality/profiling/profiler.py +428 -0
- provide/testkit/quality/report.py +266 -0
- provide/testkit/quality/runner.py +319 -0
- provide/testkit/quality/security/__init__.py +29 -0
- provide/testkit/quality/security/fixture.py +196 -0
- provide/testkit/quality/security/scanner.py +338 -0
- provide/testkit/streams.py +54 -0
- provide/testkit/threading/__init__.py +38 -0
- provide/testkit/threading/basic_fixtures.py +103 -0
- provide/testkit/threading/data_fixtures.py +101 -0
- provide/testkit/threading/execution_fixtures.py +268 -0
- provide/testkit/threading/fixtures.py +50 -0
- provide/testkit/threading/sync_fixtures.py +98 -0
- provide/testkit/time/__init__.py +32 -0
- provide/testkit/time/fixtures.py +416 -0
- provide/testkit/transport/__init__.py +30 -0
- provide/testkit/transport/fixtures.py +278 -0
- provide_testkit-0.0.0.dev0.dist-info/METADATA +145 -0
- provide_testkit-0.0.0.dev0.dist-info/RECORD +66 -0
- provide_testkit-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_testkit-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_testkit-0.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Time Testing Fixtures and Utilities.
|
|
3
|
+
|
|
4
|
+
Fixtures for mocking time, freezing time, and testing time-dependent code
|
|
5
|
+
across the provide-io ecosystem.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
import datetime
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any
|
|
12
|
+
from unittest.mock import Mock, patch
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def freeze_time():
|
|
19
|
+
"""
|
|
20
|
+
Fixture to freeze time at a specific point.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Function that freezes time and returns a context manager.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
class FrozenTime:
|
|
27
|
+
def __init__(self, frozen_time: datetime.datetime | None = None):
|
|
28
|
+
self.frozen_time = frozen_time or datetime.datetime.now()
|
|
29
|
+
self.original_time = time.time
|
|
30
|
+
self.original_datetime = datetime.datetime
|
|
31
|
+
self.patches = []
|
|
32
|
+
|
|
33
|
+
def __enter__(self):
|
|
34
|
+
# Patch time.time()
|
|
35
|
+
time_patch = patch("time.time", return_value=self.frozen_time.timestamp())
|
|
36
|
+
self.patches.append(time_patch)
|
|
37
|
+
time_patch.start()
|
|
38
|
+
|
|
39
|
+
# Patch datetime.datetime.now()
|
|
40
|
+
datetime_patch = patch("datetime.datetime", wraps=datetime.datetime)
|
|
41
|
+
mock_datetime = datetime_patch.start()
|
|
42
|
+
mock_datetime.now.return_value = self.frozen_time
|
|
43
|
+
mock_datetime.utcnow.return_value = self.frozen_time
|
|
44
|
+
self.patches.append(datetime_patch)
|
|
45
|
+
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def __exit__(self, *args):
|
|
49
|
+
for p in self.patches:
|
|
50
|
+
p.stop()
|
|
51
|
+
|
|
52
|
+
def tick(self, seconds: float = 1.0):
|
|
53
|
+
"""Advance the frozen time by the specified seconds."""
|
|
54
|
+
self.frozen_time += datetime.timedelta(seconds=seconds)
|
|
55
|
+
# Update mocks
|
|
56
|
+
for p in self.patches:
|
|
57
|
+
if hasattr(p, "return_value"):
|
|
58
|
+
p.return_value = self.frozen_time.timestamp()
|
|
59
|
+
|
|
60
|
+
def _freeze(at: datetime.datetime | None = None) -> FrozenTime:
|
|
61
|
+
"""
|
|
62
|
+
Freeze time at a specific point.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
at: Optional datetime to freeze at (defaults to now)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
FrozenTime context manager
|
|
69
|
+
"""
|
|
70
|
+
return FrozenTime(at)
|
|
71
|
+
|
|
72
|
+
return _freeze
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
def mock_sleep():
|
|
77
|
+
"""
|
|
78
|
+
Mock time.sleep to speed up tests.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Mock object that replaces time.sleep.
|
|
82
|
+
"""
|
|
83
|
+
with patch("time.sleep") as mock:
|
|
84
|
+
# Make sleep instant by default
|
|
85
|
+
mock.return_value = None
|
|
86
|
+
yield mock
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.fixture
|
|
90
|
+
def mock_sleep_with_callback():
|
|
91
|
+
"""
|
|
92
|
+
Mock time.sleep with a callback for each sleep call.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Function to set up sleep mock with callback.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def _mock_sleep(callback: Callable[[float], None] = None):
|
|
99
|
+
"""
|
|
100
|
+
Create a mock sleep with optional callback.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
callback: Function called with sleep duration
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Mock sleep object
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def sleep_side_effect(seconds):
|
|
110
|
+
if callback:
|
|
111
|
+
callback(seconds)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
mock = Mock(side_effect=sleep_side_effect)
|
|
115
|
+
return mock
|
|
116
|
+
|
|
117
|
+
return _mock_sleep
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.fixture
|
|
121
|
+
def time_machine():
|
|
122
|
+
"""
|
|
123
|
+
Advanced time manipulation fixture.
|
|
124
|
+
|
|
125
|
+
Provides methods to:
|
|
126
|
+
- Freeze time
|
|
127
|
+
- Speed up/slow down time
|
|
128
|
+
- Jump to specific times
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
TimeMachine instance for time manipulation.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
class TimeMachine:
|
|
135
|
+
def __init__(self):
|
|
136
|
+
self.current_time = time.time()
|
|
137
|
+
self.speed_multiplier = 1.0
|
|
138
|
+
self.patches = []
|
|
139
|
+
self.is_frozen = False
|
|
140
|
+
|
|
141
|
+
def freeze(self, at: float | None = None):
|
|
142
|
+
"""Freeze time at a specific timestamp."""
|
|
143
|
+
self.is_frozen = True
|
|
144
|
+
self.current_time = at or time.time()
|
|
145
|
+
|
|
146
|
+
patcher = patch("time.time", return_value=self.current_time)
|
|
147
|
+
mock = patcher.start()
|
|
148
|
+
self.patches.append(patcher)
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def unfreeze(self):
|
|
152
|
+
"""Unfreeze time."""
|
|
153
|
+
self.is_frozen = False
|
|
154
|
+
for p in self.patches:
|
|
155
|
+
p.stop()
|
|
156
|
+
self.patches.clear()
|
|
157
|
+
|
|
158
|
+
def jump(self, seconds: float):
|
|
159
|
+
"""Jump forward or backward in time."""
|
|
160
|
+
self.current_time += seconds
|
|
161
|
+
if self.is_frozen:
|
|
162
|
+
for p in self.patches:
|
|
163
|
+
if hasattr(p, "return_value"):
|
|
164
|
+
p.return_value = self.current_time
|
|
165
|
+
|
|
166
|
+
def speed_up(self, factor: float):
|
|
167
|
+
"""Speed up time by a factor."""
|
|
168
|
+
self.speed_multiplier = factor
|
|
169
|
+
|
|
170
|
+
def slow_down(self, factor: float):
|
|
171
|
+
"""Slow down time by a factor."""
|
|
172
|
+
self.speed_multiplier = 1.0 / factor
|
|
173
|
+
|
|
174
|
+
def cleanup(self):
|
|
175
|
+
"""Clean up all patches."""
|
|
176
|
+
for p in self.patches:
|
|
177
|
+
p.stop()
|
|
178
|
+
|
|
179
|
+
machine = TimeMachine()
|
|
180
|
+
yield machine
|
|
181
|
+
machine.cleanup()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@pytest.fixture
|
|
185
|
+
def timer():
|
|
186
|
+
"""
|
|
187
|
+
Timer fixture for measuring execution time.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Timer instance for measuring durations.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
class Timer:
|
|
194
|
+
def __init__(self):
|
|
195
|
+
self.start_time = None
|
|
196
|
+
self.end_time = None
|
|
197
|
+
self.durations = []
|
|
198
|
+
|
|
199
|
+
def start(self):
|
|
200
|
+
"""Start the timer."""
|
|
201
|
+
self.start_time = time.perf_counter()
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
def stop(self) -> float:
|
|
205
|
+
"""Stop the timer and return duration."""
|
|
206
|
+
self.end_time = time.perf_counter()
|
|
207
|
+
if self.start_time is None:
|
|
208
|
+
raise RuntimeError("Timer not started")
|
|
209
|
+
duration = self.end_time - self.start_time
|
|
210
|
+
self.durations.append(duration)
|
|
211
|
+
return duration
|
|
212
|
+
|
|
213
|
+
def __enter__(self):
|
|
214
|
+
"""Context manager entry."""
|
|
215
|
+
self.start()
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
def __exit__(self, *args):
|
|
219
|
+
"""Context manager exit."""
|
|
220
|
+
self.stop()
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def elapsed(self) -> float:
|
|
224
|
+
"""Get elapsed time since start."""
|
|
225
|
+
if self.start_time is None:
|
|
226
|
+
raise RuntimeError("Timer not started")
|
|
227
|
+
if self.end_time is None:
|
|
228
|
+
return time.perf_counter() - self.start_time
|
|
229
|
+
return self.end_time - self.start_time
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def average(self) -> float:
|
|
233
|
+
"""Get average duration from all measurements."""
|
|
234
|
+
if not self.durations:
|
|
235
|
+
return 0.0
|
|
236
|
+
return sum(self.durations) / len(self.durations)
|
|
237
|
+
|
|
238
|
+
def reset(self):
|
|
239
|
+
"""Reset the timer."""
|
|
240
|
+
self.start_time = None
|
|
241
|
+
self.end_time = None
|
|
242
|
+
self.durations.clear()
|
|
243
|
+
|
|
244
|
+
return Timer()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@pytest.fixture
|
|
248
|
+
def mock_datetime():
|
|
249
|
+
"""
|
|
250
|
+
Mock datetime module for testing.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Mock datetime module with common methods mocked.
|
|
254
|
+
"""
|
|
255
|
+
with patch("datetime.datetime") as mock_dt:
|
|
256
|
+
# Set up a fake "now"
|
|
257
|
+
fake_now = datetime.datetime(2024, 1, 1, 12, 0, 0)
|
|
258
|
+
mock_dt.now.return_value = fake_now
|
|
259
|
+
mock_dt.utcnow.return_value = fake_now
|
|
260
|
+
mock_dt.today.return_value = fake_now.date()
|
|
261
|
+
|
|
262
|
+
# Allow normal datetime construction
|
|
263
|
+
mock_dt.side_effect = lambda *args, **kwargs: datetime.datetime(*args, **kwargs)
|
|
264
|
+
|
|
265
|
+
yield mock_dt
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@pytest.fixture
|
|
269
|
+
def time_travel():
|
|
270
|
+
"""
|
|
271
|
+
Fixture for traveling through time in tests.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Function to travel to specific time points.
|
|
275
|
+
"""
|
|
276
|
+
original_time = time.time
|
|
277
|
+
current_offset = 0.0
|
|
278
|
+
|
|
279
|
+
def mock_time():
|
|
280
|
+
return original_time() + current_offset
|
|
281
|
+
|
|
282
|
+
def _travel_to(target: datetime.datetime):
|
|
283
|
+
"""
|
|
284
|
+
Travel to a specific point in time.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
target: The datetime to travel to
|
|
288
|
+
"""
|
|
289
|
+
nonlocal current_offset
|
|
290
|
+
current_offset = target.timestamp() - original_time()
|
|
291
|
+
|
|
292
|
+
with patch("time.time", mock_time):
|
|
293
|
+
yield _travel_to
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@pytest.fixture
|
|
297
|
+
def rate_limiter_mock():
|
|
298
|
+
"""
|
|
299
|
+
Mock for testing rate-limited code.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Mock rate limiter that can be controlled in tests.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
class MockRateLimiter:
|
|
306
|
+
def __init__(self):
|
|
307
|
+
self.calls = []
|
|
308
|
+
self.should_limit = False
|
|
309
|
+
self.limit_after = None
|
|
310
|
+
self.call_count = 0
|
|
311
|
+
|
|
312
|
+
def check(self) -> bool:
|
|
313
|
+
"""Check if rate limit is exceeded."""
|
|
314
|
+
self.call_count += 1
|
|
315
|
+
self.calls.append(time.time())
|
|
316
|
+
|
|
317
|
+
if self.limit_after and self.call_count > self.limit_after:
|
|
318
|
+
return False # Rate limited
|
|
319
|
+
|
|
320
|
+
return not self.should_limit
|
|
321
|
+
|
|
322
|
+
def reset(self):
|
|
323
|
+
"""Reset the rate limiter."""
|
|
324
|
+
self.calls.clear()
|
|
325
|
+
self.call_count = 0
|
|
326
|
+
self.should_limit = False
|
|
327
|
+
self.limit_after = None
|
|
328
|
+
|
|
329
|
+
def set_limit(self, after_calls: int):
|
|
330
|
+
"""Set to limit after N calls."""
|
|
331
|
+
self.limit_after = after_calls
|
|
332
|
+
|
|
333
|
+
return MockRateLimiter()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@pytest.fixture
|
|
337
|
+
def benchmark_timer():
|
|
338
|
+
"""
|
|
339
|
+
Timer specifically for benchmarking code.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Benchmark timer with statistics.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
class BenchmarkTimer:
|
|
346
|
+
def __init__(self):
|
|
347
|
+
self.measurements = []
|
|
348
|
+
|
|
349
|
+
def measure(self, func: Callable, *args, **kwargs) -> tuple[Any, float]:
|
|
350
|
+
"""
|
|
351
|
+
Measure execution time of a function.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
func: Function to measure
|
|
355
|
+
*args: Function arguments
|
|
356
|
+
**kwargs: Function keyword arguments
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Tuple of (result, duration)
|
|
360
|
+
"""
|
|
361
|
+
start = time.perf_counter()
|
|
362
|
+
result = func(*args, **kwargs)
|
|
363
|
+
duration = time.perf_counter() - start
|
|
364
|
+
self.measurements.append(duration)
|
|
365
|
+
return result, duration
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def min_time(self) -> float:
|
|
369
|
+
"""Get minimum execution time."""
|
|
370
|
+
return min(self.measurements) if self.measurements else 0.0
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def max_time(self) -> float:
|
|
374
|
+
"""Get maximum execution time."""
|
|
375
|
+
return max(self.measurements) if self.measurements else 0.0
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def avg_time(self) -> float:
|
|
379
|
+
"""Get average execution time."""
|
|
380
|
+
return sum(self.measurements) / len(self.measurements) if self.measurements else 0.0
|
|
381
|
+
|
|
382
|
+
def assert_faster_than(self, seconds: float):
|
|
383
|
+
"""Assert all measurements were faster than threshold."""
|
|
384
|
+
if not self.measurements:
|
|
385
|
+
raise AssertionError("No measurements taken")
|
|
386
|
+
if self.max_time > seconds:
|
|
387
|
+
raise AssertionError(f"Maximum time {self.max_time:.3f}s exceeded threshold {seconds:.3f}s")
|
|
388
|
+
|
|
389
|
+
return BenchmarkTimer()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# Utility functions that can be imported directly
|
|
393
|
+
def advance_time(mock_time: Mock, seconds: float):
|
|
394
|
+
"""
|
|
395
|
+
Advance a mocked time by specified seconds.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
mock_time: The mock time object
|
|
399
|
+
seconds: Number of seconds to advance
|
|
400
|
+
"""
|
|
401
|
+
if hasattr(mock_time, "return_value"):
|
|
402
|
+
mock_time.return_value += seconds
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
__all__ = [
|
|
406
|
+
"advance_time",
|
|
407
|
+
"benchmark_timer",
|
|
408
|
+
"freeze_time",
|
|
409
|
+
"mock_datetime",
|
|
410
|
+
"mock_sleep",
|
|
411
|
+
"mock_sleep_with_callback",
|
|
412
|
+
"rate_limiter_mock",
|
|
413
|
+
"time_machine",
|
|
414
|
+
"time_travel",
|
|
415
|
+
"timer",
|
|
416
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transport and network testing fixtures for the provide-io ecosystem.
|
|
3
|
+
|
|
4
|
+
Standard fixtures for testing HTTP clients, WebSocket connections, and
|
|
5
|
+
network operations across any project that depends on provide.foundation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from provide.testkit.transport.fixtures import (
|
|
9
|
+
free_port,
|
|
10
|
+
httpx_mock_responses,
|
|
11
|
+
mock_dns_resolver,
|
|
12
|
+
mock_http_headers,
|
|
13
|
+
mock_server,
|
|
14
|
+
mock_ssl_context,
|
|
15
|
+
mock_websocket,
|
|
16
|
+
network_timeout,
|
|
17
|
+
tcp_client_server,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"free_port",
|
|
22
|
+
"httpx_mock_responses",
|
|
23
|
+
"mock_dns_resolver",
|
|
24
|
+
"mock_http_headers",
|
|
25
|
+
"mock_server",
|
|
26
|
+
"mock_ssl_context",
|
|
27
|
+
"mock_websocket",
|
|
28
|
+
"network_timeout",
|
|
29
|
+
"tcp_client_server",
|
|
30
|
+
]
|