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,332 @@
|
|
|
1
|
+
"""Performance profiling fixture for pytest integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from ..base import BaseQualityFixture
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from .profiler import MEMRAY_AVAILABLE, PerformanceProfiler
|
|
15
|
+
except ImportError:
|
|
16
|
+
PerformanceProfiler = None
|
|
17
|
+
MEMRAY_AVAILABLE = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProfilingFixture(BaseQualityFixture):
|
|
21
|
+
"""Pytest fixture for performance profiling.
|
|
22
|
+
|
|
23
|
+
Provides easy access to performance profiling with automatic
|
|
24
|
+
setup and teardown. Integrates with the quality framework fixtures.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: dict[str, Any] | None = None, artifact_dir: Path | None = None):
|
|
28
|
+
"""Initialize profiling fixture.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config: Profiler configuration
|
|
32
|
+
artifact_dir: Directory for artifacts
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(config or {}, artifact_dir)
|
|
35
|
+
self.profiler: PerformanceProfiler | None = None
|
|
36
|
+
|
|
37
|
+
def setup(self) -> None:
|
|
38
|
+
"""Set up performance profiler."""
|
|
39
|
+
self.profiler = PerformanceProfiler(self.config)
|
|
40
|
+
self._setup_complete = True
|
|
41
|
+
|
|
42
|
+
def profile_function(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
43
|
+
"""Profile a function's performance.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
func: Function to profile
|
|
47
|
+
*args: Function arguments
|
|
48
|
+
**kwargs: Function keyword arguments
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Profiling results as dict
|
|
52
|
+
"""
|
|
53
|
+
if not self.profiler:
|
|
54
|
+
return {"error": "Profiler not available"}
|
|
55
|
+
|
|
56
|
+
result = self.profiler.profile_function(func, *args, **kwargs)
|
|
57
|
+
self.add_result(result)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"passed": result.passed,
|
|
61
|
+
"score": result.score,
|
|
62
|
+
"memory": result.details.get("memory", {}),
|
|
63
|
+
"cpu": result.details.get("cpu", {}),
|
|
64
|
+
"scores": result.details.get("scores", {}),
|
|
65
|
+
"thresholds": result.details.get("thresholds", {}),
|
|
66
|
+
"execution_time": result.execution_time,
|
|
67
|
+
"function_result": (
|
|
68
|
+
result.details.get("memory", {}).get("function_result")
|
|
69
|
+
or result.details.get("cpu", {}).get("function_result")
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def profile_memory(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
74
|
+
"""Profile memory usage only.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
func: Function to profile
|
|
78
|
+
*args: Function arguments
|
|
79
|
+
**kwargs: Function keyword arguments
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Memory profiling results
|
|
83
|
+
"""
|
|
84
|
+
if not self._setup_complete:
|
|
85
|
+
self.setup()
|
|
86
|
+
|
|
87
|
+
# Configure for memory-only profiling
|
|
88
|
+
original_config = self.config.copy()
|
|
89
|
+
self.config.update({"profile_memory": True, "profile_cpu": False})
|
|
90
|
+
|
|
91
|
+
# Recreate profiler with updated config
|
|
92
|
+
self.profiler = PerformanceProfiler(self.config)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
return self.profile_function(func, *args, **kwargs)
|
|
96
|
+
finally:
|
|
97
|
+
# Restore original config
|
|
98
|
+
self.config = original_config
|
|
99
|
+
self.profiler = PerformanceProfiler(self.config)
|
|
100
|
+
|
|
101
|
+
def profile_cpu(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
102
|
+
"""Profile CPU usage only.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
func: Function to profile
|
|
106
|
+
*args: Function arguments
|
|
107
|
+
**kwargs: Function keyword arguments
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
CPU profiling results
|
|
111
|
+
"""
|
|
112
|
+
if not self._setup_complete:
|
|
113
|
+
self.setup()
|
|
114
|
+
|
|
115
|
+
# Configure for CPU-only profiling
|
|
116
|
+
original_config = self.config.copy()
|
|
117
|
+
self.config.update({"profile_memory": False, "profile_cpu": True})
|
|
118
|
+
|
|
119
|
+
# Recreate profiler with updated config
|
|
120
|
+
self.profiler = PerformanceProfiler(self.config)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
return self.profile_function(func, *args, **kwargs)
|
|
124
|
+
finally:
|
|
125
|
+
# Restore original config
|
|
126
|
+
self.config = original_config
|
|
127
|
+
self.profiler = PerformanceProfiler(self.config)
|
|
128
|
+
|
|
129
|
+
def benchmark_function(
|
|
130
|
+
self, func: Callable[..., Any], iterations: int = 100, *args: Any, **kwargs: Any
|
|
131
|
+
) -> dict[str, Any]:
|
|
132
|
+
"""Benchmark a function over multiple iterations.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
func: Function to benchmark
|
|
136
|
+
iterations: Number of iterations to run
|
|
137
|
+
*args: Function arguments
|
|
138
|
+
**kwargs: Function keyword arguments
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Benchmark results with statistics
|
|
142
|
+
"""
|
|
143
|
+
if not self._setup_complete:
|
|
144
|
+
self.setup()
|
|
145
|
+
|
|
146
|
+
import statistics
|
|
147
|
+
|
|
148
|
+
execution_times = []
|
|
149
|
+
memory_peaks = []
|
|
150
|
+
|
|
151
|
+
for _ in range(iterations):
|
|
152
|
+
result = self.profile_function(func, *args, **kwargs)
|
|
153
|
+
|
|
154
|
+
if result.get("cpu", {}).get("execution_time"):
|
|
155
|
+
execution_times.append(result["cpu"]["execution_time"])
|
|
156
|
+
|
|
157
|
+
if result.get("memory", {}).get("peak_memory_mb"):
|
|
158
|
+
memory_peaks.append(result["memory"]["peak_memory_mb"])
|
|
159
|
+
|
|
160
|
+
# Calculate statistics
|
|
161
|
+
benchmark_stats = {}
|
|
162
|
+
|
|
163
|
+
if execution_times:
|
|
164
|
+
benchmark_stats["execution_time"] = {
|
|
165
|
+
"mean": statistics.mean(execution_times),
|
|
166
|
+
"median": statistics.median(execution_times),
|
|
167
|
+
"min": min(execution_times),
|
|
168
|
+
"max": max(execution_times),
|
|
169
|
+
"stdev": statistics.stdev(execution_times) if len(execution_times) > 1 else 0,
|
|
170
|
+
"iterations": len(execution_times),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if memory_peaks:
|
|
174
|
+
benchmark_stats["memory_usage"] = {
|
|
175
|
+
"mean_mb": statistics.mean(memory_peaks),
|
|
176
|
+
"median_mb": statistics.median(memory_peaks),
|
|
177
|
+
"min_mb": min(memory_peaks),
|
|
178
|
+
"max_mb": max(memory_peaks),
|
|
179
|
+
"stdev_mb": statistics.stdev(memory_peaks) if len(memory_peaks) > 1 else 0,
|
|
180
|
+
"iterations": len(memory_peaks),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
"benchmark_stats": benchmark_stats,
|
|
185
|
+
"iterations": iterations,
|
|
186
|
+
"total_profiling_runs": len(self.results),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def assert_performance(
|
|
190
|
+
self,
|
|
191
|
+
func: Callable[..., Any],
|
|
192
|
+
max_memory_mb: float | None = None,
|
|
193
|
+
max_execution_time: float | None = None,
|
|
194
|
+
min_score: float | None = None,
|
|
195
|
+
*args: Any,
|
|
196
|
+
**kwargs: Any,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Assert performance requirements for a function.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
func: Function to test
|
|
202
|
+
max_memory_mb: Maximum memory usage in MB
|
|
203
|
+
max_execution_time: Maximum execution time in seconds
|
|
204
|
+
min_score: Minimum performance score
|
|
205
|
+
*args: Function arguments
|
|
206
|
+
**kwargs: Function keyword arguments
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
AssertionError: If performance requirements are not met
|
|
210
|
+
"""
|
|
211
|
+
if not self._setup_complete:
|
|
212
|
+
self.setup()
|
|
213
|
+
|
|
214
|
+
# Update config with assertion requirements
|
|
215
|
+
if max_memory_mb is not None:
|
|
216
|
+
self.config["max_memory_mb"] = max_memory_mb
|
|
217
|
+
if max_execution_time is not None:
|
|
218
|
+
self.config["max_execution_time"] = max_execution_time
|
|
219
|
+
if min_score is not None:
|
|
220
|
+
self.config["min_score"] = min_score
|
|
221
|
+
|
|
222
|
+
# Recreate profiler with updated config
|
|
223
|
+
self.profiler = PerformanceProfiler(self.config)
|
|
224
|
+
|
|
225
|
+
result = self.profile_function(func, *args, **kwargs)
|
|
226
|
+
|
|
227
|
+
# Check assertions
|
|
228
|
+
if not result["passed"]:
|
|
229
|
+
failure_reasons = []
|
|
230
|
+
|
|
231
|
+
if max_memory_mb and result.get("memory", {}).get("peak_memory_mb", 0) > max_memory_mb:
|
|
232
|
+
actual_mb = result["memory"]["peak_memory_mb"]
|
|
233
|
+
failure_reasons.append(f"Memory usage {actual_mb:.2f}MB exceeds limit {max_memory_mb}MB")
|
|
234
|
+
|
|
235
|
+
if max_execution_time and result.get("cpu", {}).get("execution_time", 0) > max_execution_time:
|
|
236
|
+
actual_time = result["cpu"]["execution_time"]
|
|
237
|
+
failure_reasons.append(
|
|
238
|
+
f"Execution time {actual_time:.4f}s exceeds limit {max_execution_time}s"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if min_score and result.get("score", 0) < min_score:
|
|
242
|
+
actual_score = result["score"]
|
|
243
|
+
failure_reasons.append(f"Performance score {actual_score:.1f}% below minimum {min_score}%")
|
|
244
|
+
|
|
245
|
+
raise AssertionError(f"Performance requirements not met: {'; '.join(failure_reasons)}")
|
|
246
|
+
|
|
247
|
+
def generate_report(self, format: str = "terminal") -> str:
|
|
248
|
+
"""Generate profiling report.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
format: Report format (terminal, json)
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Formatted report
|
|
255
|
+
"""
|
|
256
|
+
if not self.profiler:
|
|
257
|
+
return "No performance profiler available"
|
|
258
|
+
|
|
259
|
+
if not self.results:
|
|
260
|
+
return "No profiling results available"
|
|
261
|
+
|
|
262
|
+
# Use the most recent result
|
|
263
|
+
latest_result = self.results[-1]
|
|
264
|
+
return self.profiler.report(latest_result, format)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@pytest.fixture
|
|
268
|
+
def profiling_fixture() -> ProfilingFixture:
|
|
269
|
+
"""Provide performance profiling fixture.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
ProfilingFixture instance
|
|
273
|
+
"""
|
|
274
|
+
fixture = ProfilingFixture()
|
|
275
|
+
fixture.setup()
|
|
276
|
+
yield fixture
|
|
277
|
+
fixture.teardown()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@pytest.fixture
|
|
281
|
+
def profiling_config() -> dict[str, Any]:
|
|
282
|
+
"""Provide default profiling configuration.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Default configuration for profiling
|
|
286
|
+
"""
|
|
287
|
+
return {
|
|
288
|
+
"profile_memory": True,
|
|
289
|
+
"profile_cpu": True,
|
|
290
|
+
"use_memray": MEMRAY_AVAILABLE,
|
|
291
|
+
"min_score": 70.0,
|
|
292
|
+
"max_memory_mb": 100.0,
|
|
293
|
+
"max_execution_time": 1.0,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@pytest.fixture
|
|
298
|
+
def memory_profiler(profiling_config: dict[str, Any]) -> ProfilingFixture:
|
|
299
|
+
"""Provide memory-only profiling fixture.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
profiling_config: Base configuration
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
ProfilingFixture configured for memory profiling only
|
|
306
|
+
"""
|
|
307
|
+
config = profiling_config.copy()
|
|
308
|
+
config.update({"profile_memory": True, "profile_cpu": False})
|
|
309
|
+
|
|
310
|
+
fixture = ProfilingFixture(config)
|
|
311
|
+
fixture.setup()
|
|
312
|
+
yield fixture
|
|
313
|
+
fixture.teardown()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@pytest.fixture
|
|
317
|
+
def cpu_profiler(profiling_config: dict[str, Any]) -> ProfilingFixture:
|
|
318
|
+
"""Provide CPU-only profiling fixture.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
profiling_config: Base configuration
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
ProfilingFixture configured for CPU profiling only
|
|
325
|
+
"""
|
|
326
|
+
config = profiling_config.copy()
|
|
327
|
+
config.update({"profile_memory": False, "profile_cpu": True})
|
|
328
|
+
|
|
329
|
+
fixture = ProfilingFixture(config)
|
|
330
|
+
fixture.setup()
|
|
331
|
+
yield fixture
|
|
332
|
+
fixture.teardown()
|