provide-foundation 0.0.0.dev0__py3-none-any.whl → 0.0.0.dev2__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/foundation/__init__.py +41 -23
- provide/foundation/archive/__init__.py +23 -0
- provide/foundation/archive/base.py +70 -0
- provide/foundation/archive/bzip2.py +157 -0
- provide/foundation/archive/gzip.py +159 -0
- provide/foundation/archive/operations.py +334 -0
- provide/foundation/archive/tar.py +164 -0
- provide/foundation/archive/zip.py +203 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +13 -7
- provide/foundation/cli/commands/logs/__init__.py +1 -1
- provide/foundation/cli/commands/logs/query.py +1 -1
- provide/foundation/cli/commands/logs/send.py +1 -1
- provide/foundation/cli/commands/logs/tail.py +1 -1
- provide/foundation/cli/decorators.py +11 -10
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -35
- provide/foundation/cli/utils.py +21 -17
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +479 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +4 -19
- provide/foundation/config/loader.py +9 -3
- provide/foundation/config/sync.py +19 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +35 -13
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +85 -109
- provide/foundation/core.py +1 -2
- provide/foundation/crypto/__init__.py +2 -0
- provide/foundation/crypto/certificates/__init__.py +34 -0
- provide/foundation/crypto/certificates/base.py +173 -0
- provide/foundation/crypto/certificates/certificate.py +290 -0
- provide/foundation/crypto/certificates/factory.py +213 -0
- provide/foundation/crypto/certificates/generator.py +138 -0
- provide/foundation/crypto/certificates/loader.py +130 -0
- provide/foundation/crypto/certificates/operations.py +198 -0
- provide/foundation/crypto/certificates/trust.py +107 -0
- provide/foundation/errors/__init__.py +2 -3
- provide/foundation/errors/decorators.py +0 -231
- provide/foundation/errors/types.py +0 -97
- provide/foundation/eventsets/__init__.py +0 -0
- provide/foundation/eventsets/display.py +84 -0
- provide/foundation/eventsets/registry.py +160 -0
- provide/foundation/eventsets/resolver.py +192 -0
- provide/foundation/eventsets/sets/das.py +128 -0
- provide/foundation/eventsets/sets/database.py +125 -0
- provide/foundation/eventsets/sets/http.py +153 -0
- provide/foundation/eventsets/sets/llm.py +139 -0
- provide/foundation/eventsets/sets/task_queue.py +107 -0
- provide/foundation/eventsets/types.py +70 -0
- provide/foundation/file/directory.py +13 -22
- provide/foundation/file/lock.py +3 -1
- provide/foundation/hub/components.py +77 -515
- provide/foundation/hub/config.py +151 -0
- provide/foundation/hub/discovery.py +62 -0
- provide/foundation/hub/handlers.py +81 -0
- provide/foundation/hub/lifecycle.py +194 -0
- provide/foundation/hub/manager.py +4 -4
- provide/foundation/hub/processors.py +44 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
- provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
- provide/foundation/logger/__init__.py +3 -10
- provide/foundation/logger/config/logging.py +68 -298
- provide/foundation/logger/config/telemetry.py +41 -121
- provide/foundation/logger/core.py +0 -2
- provide/foundation/logger/custom_processors.py +1 -0
- provide/foundation/logger/factories.py +11 -2
- provide/foundation/logger/processors/main.py +20 -84
- provide/foundation/logger/setup/__init__.py +5 -1
- provide/foundation/logger/setup/coordinator.py +76 -24
- provide/foundation/logger/setup/processors.py +2 -9
- provide/foundation/logger/trace.py +27 -0
- provide/foundation/metrics/otel.py +10 -10
- provide/foundation/observability/__init__.py +2 -2
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +47 -0
- provide/foundation/process/lifecycle.py +115 -59
- provide/foundation/resilience/__init__.py +35 -0
- provide/foundation/resilience/circuit.py +164 -0
- provide/foundation/resilience/decorators.py +220 -0
- provide/foundation/resilience/fallback.py +193 -0
- provide/foundation/resilience/retry.py +325 -0
- provide/foundation/streams/config.py +79 -0
- provide/foundation/streams/console.py +7 -8
- provide/foundation/streams/core.py +6 -3
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +84 -2
- provide/foundation/testing/archive/__init__.py +24 -0
- provide/foundation/testing/archive/fixtures.py +217 -0
- provide/foundation/testing/cli.py +30 -17
- provide/foundation/testing/common/__init__.py +32 -0
- provide/foundation/testing/common/fixtures.py +236 -0
- provide/foundation/testing/file/__init__.py +40 -0
- provide/foundation/testing/file/content_fixtures.py +316 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +52 -0
- provide/foundation/testing/file/special_fixtures.py +153 -0
- provide/foundation/testing/logger.py +117 -11
- provide/foundation/testing/mocking/__init__.py +46 -0
- provide/foundation/testing/mocking/fixtures.py +331 -0
- provide/foundation/testing/process/__init__.py +48 -0
- provide/foundation/testing/process/async_fixtures.py +405 -0
- provide/foundation/testing/process/fixtures.py +56 -0
- provide/foundation/testing/process/subprocess_fixtures.py +209 -0
- provide/foundation/testing/threading/__init__.py +38 -0
- provide/foundation/testing/threading/basic_fixtures.py +101 -0
- provide/foundation/testing/threading/data_fixtures.py +99 -0
- provide/foundation/testing/threading/execution_fixtures.py +263 -0
- provide/foundation/testing/threading/fixtures.py +54 -0
- provide/foundation/testing/threading/sync_fixtures.py +97 -0
- provide/foundation/testing/time/__init__.py +32 -0
- provide/foundation/testing/time/fixtures.py +409 -0
- provide/foundation/testing/transport/__init__.py +30 -0
- provide/foundation/testing/transport/fixtures.py +280 -0
- provide/foundation/tools/__init__.py +58 -0
- provide/foundation/tools/base.py +348 -0
- provide/foundation/tools/cache.py +268 -0
- provide/foundation/tools/downloader.py +224 -0
- provide/foundation/tools/installer.py +254 -0
- provide/foundation/tools/registry.py +223 -0
- provide/foundation/tools/resolver.py +321 -0
- provide/foundation/tools/verifier.py +186 -0
- provide/foundation/tracer/otel.py +7 -11
- provide/foundation/tracer/spans.py +2 -2
- provide/foundation/transport/__init__.py +155 -0
- provide/foundation/transport/base.py +171 -0
- provide/foundation/transport/client.py +266 -0
- provide/foundation/transport/config.py +140 -0
- provide/foundation/transport/errors.py +79 -0
- provide/foundation/transport/http.py +232 -0
- provide/foundation/transport/middleware.py +360 -0
- provide/foundation/transport/registry.py +167 -0
- provide/foundation/transport/types.py +45 -0
- provide/foundation/utils/deps.py +14 -12
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
- provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
- provide/foundation/cli/commands/logs/generate_old.py +0 -569
- provide/foundation/crypto/certificates.py +0 -896
- provide/foundation/logger/emoji/__init__.py +0 -44
- provide/foundation/logger/emoji/matrix.py +0 -209
- provide/foundation/logger/emoji/sets.py +0 -458
- provide/foundation/logger/emoji/types.py +0 -56
- provide/foundation/logger/setup/emoji_resolver.py +0 -64
- provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,409 @@
|
|
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
|
+
import time
|
9
|
+
import datetime
|
10
|
+
from typing import Any, Callable
|
11
|
+
from collections.abc import Generator
|
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
|
+
class FrozenTime:
|
26
|
+
def __init__(self, frozen_time: datetime.datetime | None = None):
|
27
|
+
self.frozen_time = frozen_time or datetime.datetime.now()
|
28
|
+
self.original_time = time.time
|
29
|
+
self.original_datetime = datetime.datetime
|
30
|
+
self.patches = []
|
31
|
+
|
32
|
+
def __enter__(self):
|
33
|
+
# Patch time.time()
|
34
|
+
time_patch = patch('time.time', return_value=self.frozen_time.timestamp())
|
35
|
+
self.patches.append(time_patch)
|
36
|
+
time_patch.start()
|
37
|
+
|
38
|
+
# Patch datetime.datetime.now()
|
39
|
+
datetime_patch = patch('datetime.datetime', wraps=datetime.datetime)
|
40
|
+
mock_datetime = datetime_patch.start()
|
41
|
+
mock_datetime.now.return_value = self.frozen_time
|
42
|
+
mock_datetime.utcnow.return_value = self.frozen_time
|
43
|
+
self.patches.append(datetime_patch)
|
44
|
+
|
45
|
+
return self
|
46
|
+
|
47
|
+
def __exit__(self, *args):
|
48
|
+
for p in self.patches:
|
49
|
+
p.stop()
|
50
|
+
|
51
|
+
def tick(self, seconds: float = 1.0):
|
52
|
+
"""Advance the frozen time by the specified seconds."""
|
53
|
+
self.frozen_time += datetime.timedelta(seconds=seconds)
|
54
|
+
# Update mocks
|
55
|
+
for p in self.patches:
|
56
|
+
if hasattr(p, 'return_value'):
|
57
|
+
p.return_value = self.frozen_time.timestamp()
|
58
|
+
|
59
|
+
def _freeze(at: datetime.datetime | None = None) -> FrozenTime:
|
60
|
+
"""
|
61
|
+
Freeze time at a specific point.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
at: Optional datetime to freeze at (defaults to now)
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
FrozenTime context manager
|
68
|
+
"""
|
69
|
+
return FrozenTime(at)
|
70
|
+
|
71
|
+
return _freeze
|
72
|
+
|
73
|
+
|
74
|
+
@pytest.fixture
|
75
|
+
def mock_sleep():
|
76
|
+
"""
|
77
|
+
Mock time.sleep to speed up tests.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Mock object that replaces time.sleep.
|
81
|
+
"""
|
82
|
+
with patch('time.sleep') as mock:
|
83
|
+
# Make sleep instant by default
|
84
|
+
mock.return_value = None
|
85
|
+
yield mock
|
86
|
+
|
87
|
+
|
88
|
+
@pytest.fixture
|
89
|
+
def mock_sleep_with_callback():
|
90
|
+
"""
|
91
|
+
Mock time.sleep with a callback for each sleep call.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
Function to set up sleep mock with callback.
|
95
|
+
"""
|
96
|
+
def _mock_sleep(callback: Callable[[float], None] = None):
|
97
|
+
"""
|
98
|
+
Create a mock sleep with optional callback.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
callback: Function called with sleep duration
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
Mock sleep object
|
105
|
+
"""
|
106
|
+
def sleep_side_effect(seconds):
|
107
|
+
if callback:
|
108
|
+
callback(seconds)
|
109
|
+
return None
|
110
|
+
|
111
|
+
mock = Mock(side_effect=sleep_side_effect)
|
112
|
+
return mock
|
113
|
+
|
114
|
+
return _mock_sleep
|
115
|
+
|
116
|
+
|
117
|
+
@pytest.fixture
|
118
|
+
def time_machine():
|
119
|
+
"""
|
120
|
+
Advanced time manipulation fixture.
|
121
|
+
|
122
|
+
Provides methods to:
|
123
|
+
- Freeze time
|
124
|
+
- Speed up/slow down time
|
125
|
+
- Jump to specific times
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
TimeMachine instance for time manipulation.
|
129
|
+
"""
|
130
|
+
class TimeMachine:
|
131
|
+
def __init__(self):
|
132
|
+
self.current_time = time.time()
|
133
|
+
self.speed_multiplier = 1.0
|
134
|
+
self.patches = []
|
135
|
+
self.is_frozen = False
|
136
|
+
|
137
|
+
def freeze(self, at: float | None = None):
|
138
|
+
"""Freeze time at a specific timestamp."""
|
139
|
+
self.is_frozen = True
|
140
|
+
self.current_time = at or time.time()
|
141
|
+
|
142
|
+
patcher = patch('time.time', return_value=self.current_time)
|
143
|
+
mock = patcher.start()
|
144
|
+
self.patches.append(patcher)
|
145
|
+
return self
|
146
|
+
|
147
|
+
def unfreeze(self):
|
148
|
+
"""Unfreeze time."""
|
149
|
+
self.is_frozen = False
|
150
|
+
for p in self.patches:
|
151
|
+
p.stop()
|
152
|
+
self.patches.clear()
|
153
|
+
|
154
|
+
def jump(self, seconds: float):
|
155
|
+
"""Jump forward or backward in time."""
|
156
|
+
self.current_time += seconds
|
157
|
+
if self.is_frozen:
|
158
|
+
for p in self.patches:
|
159
|
+
if hasattr(p, 'return_value'):
|
160
|
+
p.return_value = self.current_time
|
161
|
+
|
162
|
+
def speed_up(self, factor: float):
|
163
|
+
"""Speed up time by a factor."""
|
164
|
+
self.speed_multiplier = factor
|
165
|
+
|
166
|
+
def slow_down(self, factor: float):
|
167
|
+
"""Slow down time by a factor."""
|
168
|
+
self.speed_multiplier = 1.0 / factor
|
169
|
+
|
170
|
+
def cleanup(self):
|
171
|
+
"""Clean up all patches."""
|
172
|
+
for p in self.patches:
|
173
|
+
p.stop()
|
174
|
+
|
175
|
+
machine = TimeMachine()
|
176
|
+
yield machine
|
177
|
+
machine.cleanup()
|
178
|
+
|
179
|
+
|
180
|
+
@pytest.fixture
|
181
|
+
def timer():
|
182
|
+
"""
|
183
|
+
Timer fixture for measuring execution time.
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
Timer instance for measuring durations.
|
187
|
+
"""
|
188
|
+
class Timer:
|
189
|
+
def __init__(self):
|
190
|
+
self.start_time = None
|
191
|
+
self.end_time = None
|
192
|
+
self.durations = []
|
193
|
+
|
194
|
+
def start(self):
|
195
|
+
"""Start the timer."""
|
196
|
+
self.start_time = time.perf_counter()
|
197
|
+
return self
|
198
|
+
|
199
|
+
def stop(self) -> float:
|
200
|
+
"""Stop the timer and return duration."""
|
201
|
+
self.end_time = time.perf_counter()
|
202
|
+
if self.start_time is None:
|
203
|
+
raise RuntimeError("Timer not started")
|
204
|
+
duration = self.end_time - self.start_time
|
205
|
+
self.durations.append(duration)
|
206
|
+
return duration
|
207
|
+
|
208
|
+
def __enter__(self):
|
209
|
+
"""Context manager entry."""
|
210
|
+
self.start()
|
211
|
+
return self
|
212
|
+
|
213
|
+
def __exit__(self, *args):
|
214
|
+
"""Context manager exit."""
|
215
|
+
self.stop()
|
216
|
+
|
217
|
+
@property
|
218
|
+
def elapsed(self) -> float:
|
219
|
+
"""Get elapsed time since start."""
|
220
|
+
if self.start_time is None:
|
221
|
+
raise RuntimeError("Timer not started")
|
222
|
+
if self.end_time is None:
|
223
|
+
return time.perf_counter() - self.start_time
|
224
|
+
return self.end_time - self.start_time
|
225
|
+
|
226
|
+
@property
|
227
|
+
def average(self) -> float:
|
228
|
+
"""Get average duration from all measurements."""
|
229
|
+
if not self.durations:
|
230
|
+
return 0.0
|
231
|
+
return sum(self.durations) / len(self.durations)
|
232
|
+
|
233
|
+
def reset(self):
|
234
|
+
"""Reset the timer."""
|
235
|
+
self.start_time = None
|
236
|
+
self.end_time = None
|
237
|
+
self.durations.clear()
|
238
|
+
|
239
|
+
return Timer()
|
240
|
+
|
241
|
+
|
242
|
+
@pytest.fixture
|
243
|
+
def mock_datetime():
|
244
|
+
"""
|
245
|
+
Mock datetime module for testing.
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
Mock datetime module with common methods mocked.
|
249
|
+
"""
|
250
|
+
with patch('datetime.datetime') as mock_dt:
|
251
|
+
# Set up a fake "now"
|
252
|
+
fake_now = datetime.datetime(2024, 1, 1, 12, 0, 0)
|
253
|
+
mock_dt.now.return_value = fake_now
|
254
|
+
mock_dt.utcnow.return_value = fake_now
|
255
|
+
mock_dt.today.return_value = fake_now.date()
|
256
|
+
|
257
|
+
# Allow normal datetime construction
|
258
|
+
mock_dt.side_effect = lambda *args, **kwargs: datetime.datetime(*args, **kwargs)
|
259
|
+
|
260
|
+
yield mock_dt
|
261
|
+
|
262
|
+
|
263
|
+
@pytest.fixture
|
264
|
+
def time_travel():
|
265
|
+
"""
|
266
|
+
Fixture for traveling through time in tests.
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
Function to travel to specific time points.
|
270
|
+
"""
|
271
|
+
original_time = time.time
|
272
|
+
current_offset = 0.0
|
273
|
+
|
274
|
+
def mock_time():
|
275
|
+
return original_time() + current_offset
|
276
|
+
|
277
|
+
def _travel_to(target: datetime.datetime):
|
278
|
+
"""
|
279
|
+
Travel to a specific point in time.
|
280
|
+
|
281
|
+
Args:
|
282
|
+
target: The datetime to travel to
|
283
|
+
"""
|
284
|
+
nonlocal current_offset
|
285
|
+
current_offset = target.timestamp() - original_time()
|
286
|
+
|
287
|
+
with patch('time.time', mock_time):
|
288
|
+
yield _travel_to
|
289
|
+
|
290
|
+
|
291
|
+
@pytest.fixture
|
292
|
+
def rate_limiter_mock():
|
293
|
+
"""
|
294
|
+
Mock for testing rate-limited code.
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
Mock rate limiter that can be controlled in tests.
|
298
|
+
"""
|
299
|
+
class MockRateLimiter:
|
300
|
+
def __init__(self):
|
301
|
+
self.calls = []
|
302
|
+
self.should_limit = False
|
303
|
+
self.limit_after = None
|
304
|
+
self.call_count = 0
|
305
|
+
|
306
|
+
def check(self) -> bool:
|
307
|
+
"""Check if rate limit is exceeded."""
|
308
|
+
self.call_count += 1
|
309
|
+
self.calls.append(time.time())
|
310
|
+
|
311
|
+
if self.limit_after and self.call_count > self.limit_after:
|
312
|
+
return False # Rate limited
|
313
|
+
|
314
|
+
return not self.should_limit
|
315
|
+
|
316
|
+
def reset(self):
|
317
|
+
"""Reset the rate limiter."""
|
318
|
+
self.calls.clear()
|
319
|
+
self.call_count = 0
|
320
|
+
self.should_limit = False
|
321
|
+
self.limit_after = None
|
322
|
+
|
323
|
+
def set_limit(self, after_calls: int):
|
324
|
+
"""Set to limit after N calls."""
|
325
|
+
self.limit_after = after_calls
|
326
|
+
|
327
|
+
return MockRateLimiter()
|
328
|
+
|
329
|
+
|
330
|
+
@pytest.fixture
|
331
|
+
def benchmark_timer():
|
332
|
+
"""
|
333
|
+
Timer specifically for benchmarking code.
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
Benchmark timer with statistics.
|
337
|
+
"""
|
338
|
+
class BenchmarkTimer:
|
339
|
+
def __init__(self):
|
340
|
+
self.measurements = []
|
341
|
+
|
342
|
+
def measure(self, func: Callable, *args, **kwargs) -> tuple[Any, float]:
|
343
|
+
"""
|
344
|
+
Measure execution time of a function.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
func: Function to measure
|
348
|
+
*args: Function arguments
|
349
|
+
**kwargs: Function keyword arguments
|
350
|
+
|
351
|
+
Returns:
|
352
|
+
Tuple of (result, duration)
|
353
|
+
"""
|
354
|
+
start = time.perf_counter()
|
355
|
+
result = func(*args, **kwargs)
|
356
|
+
duration = time.perf_counter() - start
|
357
|
+
self.measurements.append(duration)
|
358
|
+
return result, duration
|
359
|
+
|
360
|
+
@property
|
361
|
+
def min_time(self) -> float:
|
362
|
+
"""Get minimum execution time."""
|
363
|
+
return min(self.measurements) if self.measurements else 0.0
|
364
|
+
|
365
|
+
@property
|
366
|
+
def max_time(self) -> float:
|
367
|
+
"""Get maximum execution time."""
|
368
|
+
return max(self.measurements) if self.measurements else 0.0
|
369
|
+
|
370
|
+
@property
|
371
|
+
def avg_time(self) -> float:
|
372
|
+
"""Get average execution time."""
|
373
|
+
return sum(self.measurements) / len(self.measurements) if self.measurements else 0.0
|
374
|
+
|
375
|
+
def assert_faster_than(self, seconds: float):
|
376
|
+
"""Assert all measurements were faster than threshold."""
|
377
|
+
if not self.measurements:
|
378
|
+
raise AssertionError("No measurements taken")
|
379
|
+
if self.max_time > seconds:
|
380
|
+
raise AssertionError(f"Maximum time {self.max_time:.3f}s exceeded threshold {seconds:.3f}s")
|
381
|
+
|
382
|
+
return BenchmarkTimer()
|
383
|
+
|
384
|
+
|
385
|
+
# Utility functions that can be imported directly
|
386
|
+
def advance_time(mock_time: Mock, seconds: float):
|
387
|
+
"""
|
388
|
+
Advance a mocked time by specified seconds.
|
389
|
+
|
390
|
+
Args:
|
391
|
+
mock_time: The mock time object
|
392
|
+
seconds: Number of seconds to advance
|
393
|
+
"""
|
394
|
+
if hasattr(mock_time, 'return_value'):
|
395
|
+
mock_time.return_value += seconds
|
396
|
+
|
397
|
+
|
398
|
+
__all__ = [
|
399
|
+
"freeze_time",
|
400
|
+
"mock_sleep",
|
401
|
+
"mock_sleep_with_callback",
|
402
|
+
"time_machine",
|
403
|
+
"timer",
|
404
|
+
"mock_datetime",
|
405
|
+
"time_travel",
|
406
|
+
"rate_limiter_mock",
|
407
|
+
"benchmark_timer",
|
408
|
+
"advance_time",
|
409
|
+
]
|
@@ -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.foundation.testing.transport.fixtures import (
|
9
|
+
free_port,
|
10
|
+
mock_server,
|
11
|
+
httpx_mock_responses,
|
12
|
+
mock_websocket,
|
13
|
+
mock_dns_resolver,
|
14
|
+
tcp_client_server,
|
15
|
+
mock_ssl_context,
|
16
|
+
network_timeout,
|
17
|
+
mock_http_headers,
|
18
|
+
)
|
19
|
+
|
20
|
+
__all__ = [
|
21
|
+
"free_port",
|
22
|
+
"mock_server",
|
23
|
+
"httpx_mock_responses",
|
24
|
+
"mock_websocket",
|
25
|
+
"mock_dns_resolver",
|
26
|
+
"tcp_client_server",
|
27
|
+
"mock_ssl_context",
|
28
|
+
"network_timeout",
|
29
|
+
"mock_http_headers",
|
30
|
+
]
|
@@ -0,0 +1,280 @@
|
|
1
|
+
"""
|
2
|
+
Transport and Network Testing Fixtures.
|
3
|
+
|
4
|
+
Fixtures and helpers for testing network operations, including
|
5
|
+
mock servers, free port allocation, and HTTP client mocking.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import socket
|
9
|
+
import threading
|
10
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
11
|
+
from typing import Any
|
12
|
+
from collections.abc import Generator
|
13
|
+
|
14
|
+
import pytest
|
15
|
+
|
16
|
+
|
17
|
+
@pytest.fixture
|
18
|
+
def free_port() -> int:
|
19
|
+
"""
|
20
|
+
Get a free port for testing.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
An available port number on localhost.
|
24
|
+
"""
|
25
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
26
|
+
s.bind(('', 0))
|
27
|
+
s.listen(1)
|
28
|
+
port = s.getsockname()[1]
|
29
|
+
return port
|
30
|
+
|
31
|
+
|
32
|
+
@pytest.fixture
|
33
|
+
def mock_server(free_port) -> Generator[dict[str, Any], None, None]:
|
34
|
+
"""
|
35
|
+
Create a simple mock HTTP server for testing.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
free_port: Free port number from fixture.
|
39
|
+
|
40
|
+
Yields:
|
41
|
+
Dict with server info including url, port, and server instance.
|
42
|
+
"""
|
43
|
+
responses = {}
|
44
|
+
requests_received = []
|
45
|
+
|
46
|
+
class MockHandler(BaseHTTPRequestHandler):
|
47
|
+
"""Handler for mock HTTP server."""
|
48
|
+
|
49
|
+
def do_GET(self):
|
50
|
+
"""Handle GET requests."""
|
51
|
+
requests_received.append({
|
52
|
+
"method": "GET",
|
53
|
+
"path": self.path,
|
54
|
+
"headers": dict(self.headers)
|
55
|
+
})
|
56
|
+
|
57
|
+
response = responses.get(self.path, {"status": 404, "body": b"Not Found"})
|
58
|
+
self.send_response(response["status"])
|
59
|
+
for header, value in response.get("headers", {}).items():
|
60
|
+
self.send_header(header, value)
|
61
|
+
self.end_headers()
|
62
|
+
self.wfile.write(response["body"])
|
63
|
+
|
64
|
+
def do_POST(self):
|
65
|
+
"""Handle POST requests."""
|
66
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
67
|
+
body = self.rfile.read(content_length) if content_length else b""
|
68
|
+
|
69
|
+
requests_received.append({
|
70
|
+
"method": "POST",
|
71
|
+
"path": self.path,
|
72
|
+
"headers": dict(self.headers),
|
73
|
+
"body": body
|
74
|
+
})
|
75
|
+
|
76
|
+
response = responses.get(self.path, {"status": 200, "body": b"OK"})
|
77
|
+
self.send_response(response["status"])
|
78
|
+
for header, value in response.get("headers", {}).items():
|
79
|
+
self.send_header(header, value)
|
80
|
+
self.end_headers()
|
81
|
+
self.wfile.write(response["body"])
|
82
|
+
|
83
|
+
def log_message(self, format, *args):
|
84
|
+
"""Suppress log messages."""
|
85
|
+
pass
|
86
|
+
|
87
|
+
server = HTTPServer(('localhost', free_port), MockHandler)
|
88
|
+
server_thread = threading.Thread(target=server.serve_forever)
|
89
|
+
server_thread.daemon = True
|
90
|
+
server_thread.start()
|
91
|
+
|
92
|
+
yield {
|
93
|
+
"url": f"http://localhost:{free_port}",
|
94
|
+
"port": free_port,
|
95
|
+
"server": server,
|
96
|
+
"responses": responses,
|
97
|
+
"requests": requests_received,
|
98
|
+
}
|
99
|
+
|
100
|
+
server.shutdown()
|
101
|
+
server.server_close()
|
102
|
+
|
103
|
+
|
104
|
+
@pytest.fixture
|
105
|
+
def httpx_mock_responses():
|
106
|
+
"""
|
107
|
+
Pre-configured responses for HTTPX mocking.
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Dict of common mock responses.
|
111
|
+
"""
|
112
|
+
return {
|
113
|
+
"success": {
|
114
|
+
"status_code": 200,
|
115
|
+
"json": {"status": "ok", "data": {}},
|
116
|
+
},
|
117
|
+
"created": {
|
118
|
+
"status_code": 201,
|
119
|
+
"json": {"id": "123", "created": True},
|
120
|
+
},
|
121
|
+
"not_found": {
|
122
|
+
"status_code": 404,
|
123
|
+
"json": {"error": "Not found"},
|
124
|
+
},
|
125
|
+
"server_error": {
|
126
|
+
"status_code": 500,
|
127
|
+
"json": {"error": "Internal server error"},
|
128
|
+
},
|
129
|
+
"unauthorized": {
|
130
|
+
"status_code": 401,
|
131
|
+
"json": {"error": "Unauthorized"},
|
132
|
+
},
|
133
|
+
"rate_limited": {
|
134
|
+
"status_code": 429,
|
135
|
+
"headers": {"Retry-After": "60"},
|
136
|
+
"json": {"error": "Rate limit exceeded"},
|
137
|
+
},
|
138
|
+
}
|
139
|
+
|
140
|
+
|
141
|
+
@pytest.fixture
|
142
|
+
def mock_websocket():
|
143
|
+
"""
|
144
|
+
Mock WebSocket connection for testing.
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
Mock WebSocket with send, receive, close methods.
|
148
|
+
"""
|
149
|
+
from unittest.mock import Mock, AsyncMock
|
150
|
+
|
151
|
+
ws = Mock()
|
152
|
+
ws.send = AsyncMock()
|
153
|
+
ws.receive = AsyncMock(return_value={"type": "text", "data": "message"})
|
154
|
+
ws.close = AsyncMock()
|
155
|
+
ws.accept = AsyncMock()
|
156
|
+
ws.ping = AsyncMock()
|
157
|
+
ws.pong = AsyncMock()
|
158
|
+
|
159
|
+
# State properties
|
160
|
+
ws.closed = False
|
161
|
+
ws.url = "ws://localhost:8000/ws"
|
162
|
+
|
163
|
+
return ws
|
164
|
+
|
165
|
+
|
166
|
+
@pytest.fixture
|
167
|
+
def mock_dns_resolver():
|
168
|
+
"""
|
169
|
+
Mock DNS resolver for testing.
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
Mock resolver with resolve method.
|
173
|
+
"""
|
174
|
+
from unittest.mock import Mock
|
175
|
+
|
176
|
+
resolver = Mock()
|
177
|
+
resolver.resolve = Mock(return_value=["127.0.0.1", "::1"])
|
178
|
+
resolver.reverse = Mock(return_value="localhost")
|
179
|
+
resolver.clear_cache = Mock()
|
180
|
+
|
181
|
+
return resolver
|
182
|
+
|
183
|
+
|
184
|
+
@pytest.fixture
|
185
|
+
def tcp_client_server(free_port) -> Generator[dict[str, Any], None, None]:
|
186
|
+
"""
|
187
|
+
Create a TCP client-server pair for testing.
|
188
|
+
|
189
|
+
Yields:
|
190
|
+
Dict with client socket, server socket, and port info.
|
191
|
+
"""
|
192
|
+
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
193
|
+
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
194
|
+
server_socket.bind(('localhost', free_port))
|
195
|
+
server_socket.listen(1)
|
196
|
+
|
197
|
+
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
198
|
+
|
199
|
+
# Run server accept in thread
|
200
|
+
connection = None
|
201
|
+
|
202
|
+
def accept_connection():
|
203
|
+
nonlocal connection
|
204
|
+
connection, _ = server_socket.accept()
|
205
|
+
|
206
|
+
accept_thread = threading.Thread(target=accept_connection)
|
207
|
+
accept_thread.daemon = True
|
208
|
+
accept_thread.start()
|
209
|
+
|
210
|
+
# Connect client
|
211
|
+
client_socket.connect(('localhost', free_port))
|
212
|
+
accept_thread.join(timeout=1)
|
213
|
+
|
214
|
+
yield {
|
215
|
+
"client": client_socket,
|
216
|
+
"server": connection,
|
217
|
+
"server_socket": server_socket,
|
218
|
+
"port": free_port,
|
219
|
+
}
|
220
|
+
|
221
|
+
# Cleanup
|
222
|
+
client_socket.close()
|
223
|
+
if connection:
|
224
|
+
connection.close()
|
225
|
+
server_socket.close()
|
226
|
+
|
227
|
+
|
228
|
+
@pytest.fixture
|
229
|
+
def mock_ssl_context():
|
230
|
+
"""
|
231
|
+
Mock SSL context for testing secure connections.
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
Mock SSL context with common methods.
|
235
|
+
"""
|
236
|
+
from unittest.mock import Mock
|
237
|
+
|
238
|
+
context = Mock()
|
239
|
+
context.load_cert_chain = Mock()
|
240
|
+
context.load_verify_locations = Mock()
|
241
|
+
context.set_ciphers = Mock()
|
242
|
+
context.wrap_socket = Mock()
|
243
|
+
context.check_hostname = True
|
244
|
+
context.verify_mode = 2 # ssl.CERT_REQUIRED
|
245
|
+
|
246
|
+
return context
|
247
|
+
|
248
|
+
|
249
|
+
@pytest.fixture
|
250
|
+
def network_timeout():
|
251
|
+
"""
|
252
|
+
Provide network timeout configuration for tests.
|
253
|
+
|
254
|
+
Returns:
|
255
|
+
Dict with timeout values for different operations.
|
256
|
+
"""
|
257
|
+
return {
|
258
|
+
"connect": 5.0,
|
259
|
+
"read": 10.0,
|
260
|
+
"write": 10.0,
|
261
|
+
"total": 30.0,
|
262
|
+
}
|
263
|
+
|
264
|
+
|
265
|
+
@pytest.fixture
|
266
|
+
def mock_http_headers():
|
267
|
+
"""
|
268
|
+
Common HTTP headers for testing.
|
269
|
+
|
270
|
+
Returns:
|
271
|
+
Dict of typical HTTP headers.
|
272
|
+
"""
|
273
|
+
return {
|
274
|
+
"User-Agent": "TestClient/1.0",
|
275
|
+
"Accept": "application/json",
|
276
|
+
"Content-Type": "application/json",
|
277
|
+
"Authorization": "Bearer test_token",
|
278
|
+
"X-Request-ID": "test-request-123",
|
279
|
+
"X-Correlation-ID": "test-correlation-456",
|
280
|
+
}
|