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,392 @@
|
|
|
1
|
+
"""Complexity analysis implementation using radon."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from provide.foundation.file import atomic_write_text, ensure_dir
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import radon # type: ignore[import-untyped]
|
|
14
|
+
from radon.complexity import cc_rank, cc_visit # type: ignore[import-untyped]
|
|
15
|
+
from radon.metrics import mi_visit # type: ignore[import-untyped]
|
|
16
|
+
from radon.raw import analyze # type: ignore[import-untyped]
|
|
17
|
+
|
|
18
|
+
RADON_AVAILABLE = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
RADON_AVAILABLE = False
|
|
21
|
+
radon = None
|
|
22
|
+
cc_visit = None
|
|
23
|
+
cc_rank = None
|
|
24
|
+
mi_visit = None
|
|
25
|
+
analyze = None
|
|
26
|
+
|
|
27
|
+
from ..base import QualityResult, QualityToolError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ComplexityAnalyzer:
|
|
31
|
+
"""Code complexity analyzer using radon and other tools.
|
|
32
|
+
|
|
33
|
+
Provides high-level interface for complexity analysis with automatic
|
|
34
|
+
artifact management and integration with the quality framework.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
38
|
+
"""Initialize complexity analyzer.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
config: Complexity analyzer configuration options
|
|
42
|
+
"""
|
|
43
|
+
if not RADON_AVAILABLE:
|
|
44
|
+
raise QualityToolError("Radon not available. Install with: pip install radon", tool="complexity")
|
|
45
|
+
|
|
46
|
+
self.config = config or {}
|
|
47
|
+
self.artifact_dir: Path | None = None
|
|
48
|
+
|
|
49
|
+
def analyze(self, path: Path, **kwargs: Any) -> QualityResult:
|
|
50
|
+
"""Run complexity analysis on the given path.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
path: Path to analyze
|
|
54
|
+
**kwargs: Additional options including artifact_dir
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
QualityResult with complexity analysis data
|
|
58
|
+
"""
|
|
59
|
+
self.artifact_dir = kwargs.get("artifact_dir", Path(".complexity"))
|
|
60
|
+
start_time = time.time()
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# Run radon complexity analysis
|
|
64
|
+
result = self._run_radon_analysis(path)
|
|
65
|
+
result.execution_time = time.time() - start_time
|
|
66
|
+
|
|
67
|
+
# Generate artifacts
|
|
68
|
+
self._generate_artifacts(result)
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return QualityResult(
|
|
74
|
+
tool="complexity",
|
|
75
|
+
passed=False,
|
|
76
|
+
details={"error": str(e), "error_type": type(e).__name__},
|
|
77
|
+
execution_time=time.time() - start_time,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def _run_radon_analysis(self, path: Path) -> QualityResult:
|
|
81
|
+
"""Run radon complexity analysis."""
|
|
82
|
+
if not RADON_AVAILABLE:
|
|
83
|
+
raise QualityToolError("Radon not available", tool="complexity")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Discover Python files
|
|
87
|
+
python_files = self._discover_python_files(path)
|
|
88
|
+
|
|
89
|
+
if not python_files:
|
|
90
|
+
return QualityResult(
|
|
91
|
+
tool="complexity",
|
|
92
|
+
passed=True,
|
|
93
|
+
score=100.0,
|
|
94
|
+
details={"message": "No Python files found to analyze", "grade": "A"},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Analyze each file
|
|
98
|
+
all_complexity = []
|
|
99
|
+
all_raw_metrics = []
|
|
100
|
+
all_maintainability = []
|
|
101
|
+
|
|
102
|
+
for file_path in python_files:
|
|
103
|
+
try:
|
|
104
|
+
content = file_path.read_text()
|
|
105
|
+
|
|
106
|
+
# Cyclomatic complexity
|
|
107
|
+
complexity_data = cc_visit(content)
|
|
108
|
+
for item in complexity_data:
|
|
109
|
+
all_complexity.append(
|
|
110
|
+
{
|
|
111
|
+
"file": str(file_path),
|
|
112
|
+
"name": item.name,
|
|
113
|
+
"complexity": item.complexity,
|
|
114
|
+
"rank": cc_rank(item.complexity),
|
|
115
|
+
"lineno": item.lineno,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Raw metrics
|
|
120
|
+
raw_data = analyze(content)
|
|
121
|
+
all_raw_metrics.append(
|
|
122
|
+
{
|
|
123
|
+
"file": str(file_path),
|
|
124
|
+
"loc": raw_data.loc,
|
|
125
|
+
"lloc": raw_data.lloc,
|
|
126
|
+
"sloc": raw_data.sloc,
|
|
127
|
+
"comments": raw_data.comments,
|
|
128
|
+
"multi": raw_data.multi,
|
|
129
|
+
"blank": raw_data.blank,
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Maintainability index
|
|
134
|
+
try:
|
|
135
|
+
mi_data = mi_visit(content, multi=True)
|
|
136
|
+
if hasattr(mi_data, "mi"):
|
|
137
|
+
all_maintainability.append(
|
|
138
|
+
{"file": str(file_path), "maintainability_index": mi_data.mi}
|
|
139
|
+
)
|
|
140
|
+
except Exception:
|
|
141
|
+
# MI calculation can fail on some files
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
except Exception:
|
|
145
|
+
# Skip files that can't be analyzed
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Process results
|
|
149
|
+
return self._process_complexity_results(all_complexity, all_raw_metrics, all_maintainability)
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
raise QualityToolError(f"Radon analysis failed: {e}", tool="complexity")
|
|
153
|
+
|
|
154
|
+
def _discover_python_files(self, path: Path) -> list[Path]:
|
|
155
|
+
"""Discover Python files to analyze."""
|
|
156
|
+
excludes = self.config.get(
|
|
157
|
+
"exclude", ["*/tests/*", "*/test_*", "*/.venv/*", "*/venv/*", "*/__pycache__/*"]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
files = []
|
|
161
|
+
if path.is_file() and path.suffix == ".py":
|
|
162
|
+
files.append(path)
|
|
163
|
+
else:
|
|
164
|
+
for py_file in path.rglob("*.py"):
|
|
165
|
+
# Check if file should be excluded
|
|
166
|
+
if any(py_file.match(pattern) for pattern in excludes):
|
|
167
|
+
continue
|
|
168
|
+
files.append(py_file)
|
|
169
|
+
|
|
170
|
+
return files
|
|
171
|
+
|
|
172
|
+
def _process_complexity_results(
|
|
173
|
+
self,
|
|
174
|
+
complexity_data: list[dict[str, Any]],
|
|
175
|
+
raw_metrics: list[dict[str, Any]],
|
|
176
|
+
maintainability_data: list[dict[str, Any]],
|
|
177
|
+
) -> QualityResult:
|
|
178
|
+
"""Process complexity analysis results into QualityResult."""
|
|
179
|
+
|
|
180
|
+
# Calculate overall metrics
|
|
181
|
+
total_files = len(raw_metrics)
|
|
182
|
+
total_functions = len(complexity_data)
|
|
183
|
+
|
|
184
|
+
# Complexity statistics
|
|
185
|
+
complexities = [item["complexity"] for item in complexity_data]
|
|
186
|
+
avg_complexity = sum(complexities) / len(complexities) if complexities else 0
|
|
187
|
+
max_complexity = max(complexities) if complexities else 0
|
|
188
|
+
|
|
189
|
+
# Count by complexity grades
|
|
190
|
+
grade_counts = {"A": 0, "B": 0, "C": 0, "D": 0, "E": 0, "F": 0}
|
|
191
|
+
for item in complexity_data:
|
|
192
|
+
grade_counts[item["rank"]] += 1
|
|
193
|
+
|
|
194
|
+
# Raw metrics totals
|
|
195
|
+
total_loc = sum(item["loc"] for item in raw_metrics)
|
|
196
|
+
total_lloc = sum(item["lloc"] for item in raw_metrics)
|
|
197
|
+
total_comments = sum(item["comments"] for item in raw_metrics)
|
|
198
|
+
|
|
199
|
+
# Maintainability statistics
|
|
200
|
+
if maintainability_data:
|
|
201
|
+
mi_scores = [item["maintainability_index"] for item in maintainability_data]
|
|
202
|
+
avg_maintainability = sum(mi_scores) / len(mi_scores)
|
|
203
|
+
else:
|
|
204
|
+
avg_maintainability = None
|
|
205
|
+
|
|
206
|
+
# Calculate overall grade based on average complexity
|
|
207
|
+
if avg_complexity <= 5:
|
|
208
|
+
overall_grade = "A"
|
|
209
|
+
score = 100.0
|
|
210
|
+
elif avg_complexity <= 10:
|
|
211
|
+
overall_grade = "B"
|
|
212
|
+
score = 85.0
|
|
213
|
+
elif avg_complexity <= 20:
|
|
214
|
+
overall_grade = "C"
|
|
215
|
+
score = 70.0
|
|
216
|
+
elif avg_complexity <= 30:
|
|
217
|
+
overall_grade = "D"
|
|
218
|
+
score = 55.0
|
|
219
|
+
else:
|
|
220
|
+
overall_grade = "F"
|
|
221
|
+
score = 40.0
|
|
222
|
+
|
|
223
|
+
# Determine if passed based on configuration
|
|
224
|
+
required_grade = self.config.get("min_grade", "C")
|
|
225
|
+
max_complexity_threshold = self.config.get("max_complexity", 20)
|
|
226
|
+
min_score = self.config.get("min_score", 70.0)
|
|
227
|
+
|
|
228
|
+
grade_values = {"A": 5, "B": 4, "C": 3, "D": 2, "E": 1, "F": 0}
|
|
229
|
+
|
|
230
|
+
passed = (
|
|
231
|
+
grade_values.get(overall_grade, 0) >= grade_values.get(required_grade, 0)
|
|
232
|
+
and max_complexity <= max_complexity_threshold
|
|
233
|
+
and score >= min_score
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Create detailed results
|
|
237
|
+
details = {
|
|
238
|
+
"total_files": total_files,
|
|
239
|
+
"total_functions": total_functions,
|
|
240
|
+
"average_complexity": round(avg_complexity, 2),
|
|
241
|
+
"max_complexity": max_complexity,
|
|
242
|
+
"overall_grade": overall_grade,
|
|
243
|
+
"grade_breakdown": grade_counts,
|
|
244
|
+
"lines_of_code": total_loc,
|
|
245
|
+
"logical_lines": total_lloc,
|
|
246
|
+
"comment_lines": total_comments,
|
|
247
|
+
"grade": overall_grade, # For grade-based gate checking
|
|
248
|
+
"thresholds": {
|
|
249
|
+
"min_grade": required_grade,
|
|
250
|
+
"max_complexity": max_complexity_threshold,
|
|
251
|
+
"min_score": min_score,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if avg_maintainability is not None:
|
|
256
|
+
details["average_maintainability"] = round(avg_maintainability, 2)
|
|
257
|
+
|
|
258
|
+
# Add detailed complexity data (limited for readability)
|
|
259
|
+
if complexity_data:
|
|
260
|
+
# Sort by complexity (highest first) and take top 10
|
|
261
|
+
sorted_complexity = sorted(complexity_data, key=lambda x: x["complexity"], reverse=True)[:10]
|
|
262
|
+
details["most_complex_functions"] = sorted_complexity
|
|
263
|
+
|
|
264
|
+
return QualityResult(tool="complexity", passed=passed, score=score, details=details)
|
|
265
|
+
|
|
266
|
+
def _generate_artifacts(self, result: QualityResult) -> None:
|
|
267
|
+
"""Generate complexity analysis artifacts.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
result: Result to add artifacts to
|
|
271
|
+
"""
|
|
272
|
+
if not self.artifact_dir:
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
ensure_dir(self.artifact_dir)
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
# Generate JSON report
|
|
279
|
+
json_file = self.artifact_dir / "complexity.json"
|
|
280
|
+
json_data = {
|
|
281
|
+
"tool": result.tool,
|
|
282
|
+
"passed": result.passed,
|
|
283
|
+
"score": result.score,
|
|
284
|
+
"details": result.details,
|
|
285
|
+
"execution_time": result.execution_time,
|
|
286
|
+
}
|
|
287
|
+
atomic_write_text(json_file, json.dumps(json_data, indent=2))
|
|
288
|
+
result.artifacts.append(json_file)
|
|
289
|
+
|
|
290
|
+
# Generate text summary
|
|
291
|
+
summary_file = self.artifact_dir / "complexity_summary.txt"
|
|
292
|
+
summary_report = self._generate_text_report(result)
|
|
293
|
+
atomic_write_text(summary_file, summary_report)
|
|
294
|
+
result.artifacts.append(summary_file)
|
|
295
|
+
|
|
296
|
+
# Generate detailed complexity report
|
|
297
|
+
if result.details.get("most_complex_functions"):
|
|
298
|
+
detail_file = self.artifact_dir / "complexity_details.txt"
|
|
299
|
+
detail_report = self._generate_detail_report(result)
|
|
300
|
+
atomic_write_text(detail_file, detail_report)
|
|
301
|
+
result.artifacts.append(detail_file)
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
# Add error to result details but don't fail
|
|
305
|
+
result.details["artifact_error"] = str(e)
|
|
306
|
+
|
|
307
|
+
def _generate_text_report(self, result: QualityResult) -> str:
|
|
308
|
+
"""Generate text summary report."""
|
|
309
|
+
lines = [
|
|
310
|
+
f"Complexity Analysis Report - {result.tool}",
|
|
311
|
+
"=" * 50,
|
|
312
|
+
f"Status: {'✅ PASSED' if result.passed else '❌ FAILED'}",
|
|
313
|
+
f"Overall Grade: {result.details.get('overall_grade', 'N/A')}",
|
|
314
|
+
f"Score: {result.score}%",
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
details = result.details
|
|
318
|
+
if "total_files" in details:
|
|
319
|
+
lines.extend(
|
|
320
|
+
[
|
|
321
|
+
f"Files Analyzed: {details['total_files']}",
|
|
322
|
+
f"Total Functions: {details['total_functions']}",
|
|
323
|
+
f"Average Complexity: {details['average_complexity']}",
|
|
324
|
+
f"Max Complexity: {details['max_complexity']}",
|
|
325
|
+
"",
|
|
326
|
+
"Grade Breakdown:",
|
|
327
|
+
]
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
grade_breakdown = details.get("grade_breakdown", {})
|
|
331
|
+
for grade, count in grade_breakdown.items():
|
|
332
|
+
if count > 0:
|
|
333
|
+
lines.append(f" {grade}: {count} functions")
|
|
334
|
+
|
|
335
|
+
lines.extend(
|
|
336
|
+
[
|
|
337
|
+
"",
|
|
338
|
+
f"Lines of Code: {details['lines_of_code']}",
|
|
339
|
+
f"Logical Lines: {details['logical_lines']}",
|
|
340
|
+
f"Comment Lines: {details['comment_lines']}",
|
|
341
|
+
]
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if "average_maintainability" in details:
|
|
345
|
+
lines.append(f"Average Maintainability Index: {details['average_maintainability']}")
|
|
346
|
+
|
|
347
|
+
if result.execution_time:
|
|
348
|
+
lines.append(f"\nExecution Time: {result.execution_time:.2f}s")
|
|
349
|
+
|
|
350
|
+
return "\n".join(lines)
|
|
351
|
+
|
|
352
|
+
def _generate_detail_report(self, result: QualityResult) -> str:
|
|
353
|
+
"""Generate detailed complexity report."""
|
|
354
|
+
lines = ["Most Complex Functions", "=" * 50, ""]
|
|
355
|
+
|
|
356
|
+
functions = result.details.get("most_complex_functions", [])
|
|
357
|
+
for i, func in enumerate(functions, 1):
|
|
358
|
+
lines.extend(
|
|
359
|
+
[
|
|
360
|
+
f"{i}. {func['name']} (Grade {func['rank']})",
|
|
361
|
+
f" File: {func['file']}:{func['lineno']}",
|
|
362
|
+
f" Complexity: {func['complexity']}",
|
|
363
|
+
"",
|
|
364
|
+
]
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return "\n".join(lines)
|
|
368
|
+
|
|
369
|
+
def report(self, result: QualityResult, format: str = "terminal") -> str:
|
|
370
|
+
"""Generate report from QualityResult (implements QualityTool protocol).
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
result: Complexity result
|
|
374
|
+
format: Report format
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Formatted report
|
|
378
|
+
"""
|
|
379
|
+
if format == "terminal":
|
|
380
|
+
return self._generate_text_report(result)
|
|
381
|
+
elif format == "json":
|
|
382
|
+
return json.dumps(
|
|
383
|
+
{
|
|
384
|
+
"tool": result.tool,
|
|
385
|
+
"passed": result.passed,
|
|
386
|
+
"score": result.score,
|
|
387
|
+
"details": result.details,
|
|
388
|
+
},
|
|
389
|
+
indent=2,
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
return str(result.details)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Pytest fixtures for complexity analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Generator
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from ..base import BaseQualityFixture
|
|
12
|
+
from .analyzer import RADON_AVAILABLE, ComplexityAnalyzer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ComplexityFixture(BaseQualityFixture):
|
|
16
|
+
"""Pytest fixture for complexity analysis integration."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config: dict[str, Any] | None = None, artifact_dir: Path | None = None):
|
|
19
|
+
"""Initialize complexity fixture.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
config: Complexity analyzer configuration
|
|
23
|
+
artifact_dir: Directory for artifacts
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(config, artifact_dir)
|
|
26
|
+
self.analyzer: ComplexityAnalyzer | None = None
|
|
27
|
+
|
|
28
|
+
def setup(self) -> None:
|
|
29
|
+
"""Setup complexity analysis."""
|
|
30
|
+
if not RADON_AVAILABLE:
|
|
31
|
+
pytest.skip("Radon not available")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
self.analyzer = ComplexityAnalyzer(self.config)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
pytest.skip(f"Failed to initialize complexity analyzer: {e}")
|
|
37
|
+
|
|
38
|
+
def teardown(self) -> None:
|
|
39
|
+
"""Cleanup complexity analyzer."""
|
|
40
|
+
# No cleanup needed for complexity analyzer
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def analyze(self, path: Path) -> dict[str, Any]:
|
|
44
|
+
"""Perform complexity analysis.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
path: Path to analyze
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Complexity analysis results
|
|
51
|
+
"""
|
|
52
|
+
self.ensure_setup()
|
|
53
|
+
if not self.analyzer:
|
|
54
|
+
return {"error": "Analyzer not available"}
|
|
55
|
+
|
|
56
|
+
result = self.analyzer.analyze(path, artifact_dir=self.artifact_dir)
|
|
57
|
+
self.add_result(result)
|
|
58
|
+
return {
|
|
59
|
+
"passed": result.passed,
|
|
60
|
+
"score": result.score,
|
|
61
|
+
"grade": result.details.get("overall_grade", "F"),
|
|
62
|
+
"average_complexity": result.details.get("average_complexity", 0),
|
|
63
|
+
"max_complexity": result.details.get("max_complexity", 0),
|
|
64
|
+
"details": result.details,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def generate_report(self, format: str = "terminal") -> str:
|
|
68
|
+
"""Generate complexity report.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
format: Report format (terminal, json)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Formatted report
|
|
75
|
+
"""
|
|
76
|
+
if not self.analyzer:
|
|
77
|
+
return "No complexity analyzer available"
|
|
78
|
+
|
|
79
|
+
results = self.get_results_by_tool()
|
|
80
|
+
if "complexity" not in results:
|
|
81
|
+
return "No complexity results available"
|
|
82
|
+
|
|
83
|
+
return self.analyzer.report(results["complexity"], format)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.fixture
|
|
87
|
+
def complexity_analyzer(request, tmp_path) -> Generator[ComplexityFixture, None, None]:
|
|
88
|
+
"""Pytest fixture for complexity analysis.
|
|
89
|
+
|
|
90
|
+
Provides a ComplexityFixture instance for code complexity analysis.
|
|
91
|
+
|
|
92
|
+
Usage:
|
|
93
|
+
def test_complexity_analysis(complexity_analyzer):
|
|
94
|
+
result = complexity_analyzer.analyze(Path('./src'))
|
|
95
|
+
assert result['passed']
|
|
96
|
+
assert result['grade'] in ['A', 'B', 'C']
|
|
97
|
+
"""
|
|
98
|
+
# Get configuration from pytest request
|
|
99
|
+
config = getattr(request, "param", {})
|
|
100
|
+
|
|
101
|
+
# Create artifact directory for this test
|
|
102
|
+
artifact_dir = tmp_path / "complexity"
|
|
103
|
+
|
|
104
|
+
# Initialize fixture
|
|
105
|
+
fixture = ComplexityFixture(config=config, artifact_dir=artifact_dir)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
fixture.setup()
|
|
109
|
+
yield fixture
|
|
110
|
+
finally:
|
|
111
|
+
fixture.teardown()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.fixture
|
|
115
|
+
def complexity_config():
|
|
116
|
+
"""Default complexity configuration fixture.
|
|
117
|
+
|
|
118
|
+
Returns standard complexity configuration that can be customized
|
|
119
|
+
per test or project.
|
|
120
|
+
|
|
121
|
+
Usage:
|
|
122
|
+
def test_custom_complexity(complexity_config):
|
|
123
|
+
complexity_config["min_grade"] = "A"
|
|
124
|
+
complexity_config["max_complexity"] = 10
|
|
125
|
+
# Use with parametrized complexity_analyzer
|
|
126
|
+
"""
|
|
127
|
+
return {
|
|
128
|
+
"min_grade": "C",
|
|
129
|
+
"max_complexity": 20,
|
|
130
|
+
"min_score": 70.0,
|
|
131
|
+
"exclude": ["*/tests/*", "*/test_*", "*/.venv/*", "*/venv/*", "*/__pycache__/*", "*/migrations/*"],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Parametrized fixtures for different complexity configurations
|
|
136
|
+
@pytest.fixture(
|
|
137
|
+
params=[
|
|
138
|
+
{"min_grade": "A", "max_complexity": 10, "min_score": 95.0}, # Strict
|
|
139
|
+
{"min_grade": "B", "max_complexity": 15, "min_score": 80.0}, # Normal
|
|
140
|
+
{"min_grade": "C", "max_complexity": 25, "min_score": 60.0}, # Lenient
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
def parametrized_complexity(request, tmp_path) -> Generator[ComplexityFixture, None, None]:
|
|
144
|
+
"""Parametrized complexity fixture for testing different configurations.
|
|
145
|
+
|
|
146
|
+
Automatically runs tests with different complexity thresholds
|
|
147
|
+
to validate behavior under various settings.
|
|
148
|
+
|
|
149
|
+
Usage:
|
|
150
|
+
def test_complexity_configs(parametrized_complexity):
|
|
151
|
+
# Test runs multiple times with different configs
|
|
152
|
+
result = parametrized_complexity.analyze(Path('./src'))
|
|
153
|
+
# Behavior will vary based on configuration
|
|
154
|
+
"""
|
|
155
|
+
config = request.param
|
|
156
|
+
artifact_dir = tmp_path / f"complexity_{id(config)}"
|
|
157
|
+
|
|
158
|
+
fixture = ComplexityFixture(config=config, artifact_dir=artifact_dir)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
fixture.setup()
|
|
162
|
+
yield fixture
|
|
163
|
+
finally:
|
|
164
|
+
fixture.teardown()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Pytest hooks for automatic complexity integration
|
|
168
|
+
def pytest_configure(config):
|
|
169
|
+
"""Configure pytest with complexity markers."""
|
|
170
|
+
config.addinivalue_line("markers", "complexity: mark test to run with complexity analysis")
|
|
171
|
+
config.addinivalue_line("markers", "no_complexity: mark test to skip complexity analysis")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@pytest.fixture(autouse=True)
|
|
175
|
+
def auto_complexity_marker(request):
|
|
176
|
+
"""Automatically apply complexity analysis to marked tests.
|
|
177
|
+
|
|
178
|
+
Tests marked with @pytest.mark.complexity will automatically
|
|
179
|
+
get complexity analysis without needing to explicitly use fixtures.
|
|
180
|
+
"""
|
|
181
|
+
if request.node.get_closest_marker("complexity"):
|
|
182
|
+
# Test is marked for complexity - enable automatic analysis
|
|
183
|
+
if not request.node.get_closest_marker("no_complexity"):
|
|
184
|
+
# Create temporary complexity fixture
|
|
185
|
+
complexity_fixture = ComplexityFixture()
|
|
186
|
+
try:
|
|
187
|
+
complexity_fixture.setup()
|
|
188
|
+
# Complexity analysis would be applied here in a real implementation
|
|
189
|
+
# For now, we just yield to continue the test
|
|
190
|
+
yield
|
|
191
|
+
finally:
|
|
192
|
+
complexity_fixture.teardown()
|
|
193
|
+
else:
|
|
194
|
+
yield
|
|
195
|
+
else:
|
|
196
|
+
yield
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Coverage analysis integration for provide-testkit.
|
|
2
|
+
|
|
3
|
+
Provides pytest fixtures and utilities for tracking code coverage during tests.
|
|
4
|
+
Integrates with the coverage.py library for comprehensive coverage analysis.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Automatic coverage tracking during test runs
|
|
8
|
+
- Coverage reporting in multiple formats
|
|
9
|
+
- Integration with quality gates
|
|
10
|
+
- Artifact management for CI/CD
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
# Basic coverage tracking
|
|
14
|
+
def test_with_coverage(coverage_tracker):
|
|
15
|
+
result = coverage_tracker.start()
|
|
16
|
+
# ... run tests
|
|
17
|
+
coverage_tracker.stop()
|
|
18
|
+
assert coverage_tracker.get_coverage() > 90
|
|
19
|
+
|
|
20
|
+
# Session-wide coverage
|
|
21
|
+
def test_example(session_coverage):
|
|
22
|
+
# Coverage automatically tracked across all tests
|
|
23
|
+
pass
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .fixture import CoverageFixture, coverage_tracker, session_coverage
|
|
27
|
+
from .reporter import CoverageReporter
|
|
28
|
+
from .tracker import CoverageTracker
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"CoverageFixture",
|
|
32
|
+
"CoverageReporter",
|
|
33
|
+
"CoverageTracker",
|
|
34
|
+
"coverage_tracker",
|
|
35
|
+
"session_coverage",
|
|
36
|
+
]
|