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,361 @@
|
|
|
1
|
+
"""Documentation coverage checker implementation using interrogate."""
|
|
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 interrogate # type: ignore[import-untyped]
|
|
14
|
+
from interrogate import coverage
|
|
15
|
+
from interrogate.config import InterrogateConfig # type: ignore[import-untyped]
|
|
16
|
+
|
|
17
|
+
INTERROGATE_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
INTERROGATE_AVAILABLE = False
|
|
20
|
+
interrogate = None
|
|
21
|
+
coverage = None
|
|
22
|
+
InterrogateConfig = None
|
|
23
|
+
|
|
24
|
+
from ..base import QualityResult, QualityToolError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DocumentationChecker:
|
|
28
|
+
"""Documentation coverage checker using interrogate.
|
|
29
|
+
|
|
30
|
+
Provides high-level interface for documentation analysis with automatic
|
|
31
|
+
artifact management and integration with the quality framework.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
35
|
+
"""Initialize documentation checker.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: Documentation checker configuration options
|
|
39
|
+
"""
|
|
40
|
+
if not INTERROGATE_AVAILABLE:
|
|
41
|
+
raise QualityToolError(
|
|
42
|
+
"Interrogate not available. Install with: pip install interrogate", tool="documentation"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self.config = config or {}
|
|
46
|
+
self.artifact_dir: Path | None = None
|
|
47
|
+
|
|
48
|
+
def analyze(self, path: Path, **kwargs: Any) -> QualityResult:
|
|
49
|
+
"""Run documentation analysis on the given path.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
path: Path to analyze
|
|
53
|
+
**kwargs: Additional options including artifact_dir
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
QualityResult with documentation analysis data
|
|
57
|
+
"""
|
|
58
|
+
self.artifact_dir = kwargs.get("artifact_dir", Path(".documentation"))
|
|
59
|
+
start_time = time.time()
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Run interrogate documentation analysis
|
|
63
|
+
result = self._run_interrogate_analysis(path)
|
|
64
|
+
result.execution_time = time.time() - start_time
|
|
65
|
+
|
|
66
|
+
# Generate artifacts
|
|
67
|
+
self._generate_artifacts(result)
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return QualityResult(
|
|
73
|
+
tool="documentation",
|
|
74
|
+
passed=False,
|
|
75
|
+
details={"error": str(e), "error_type": type(e).__name__},
|
|
76
|
+
execution_time=time.time() - start_time,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def _run_interrogate_analysis(self, path: Path) -> QualityResult:
|
|
80
|
+
"""Run interrogate documentation analysis."""
|
|
81
|
+
if not INTERROGATE_AVAILABLE:
|
|
82
|
+
raise QualityToolError("Interrogate not available", tool="documentation")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Create interrogate configuration
|
|
86
|
+
config_args = self._build_interrogate_config()
|
|
87
|
+
|
|
88
|
+
# Create config object - filter out unsupported parameters
|
|
89
|
+
supported_args = {}
|
|
90
|
+
for key, value in config_args.items():
|
|
91
|
+
# Only include basic supported parameters for InterrogateConfig
|
|
92
|
+
if key in [
|
|
93
|
+
"ignore_init_method",
|
|
94
|
+
"ignore_magic",
|
|
95
|
+
"ignore_private",
|
|
96
|
+
"verbose",
|
|
97
|
+
"quiet",
|
|
98
|
+
"paths",
|
|
99
|
+
]:
|
|
100
|
+
supported_args[key] = value
|
|
101
|
+
|
|
102
|
+
supported_args["paths"] = [str(path)]
|
|
103
|
+
config = InterrogateConfig(**supported_args)
|
|
104
|
+
|
|
105
|
+
# Run interrogate analysis
|
|
106
|
+
cov = coverage.InterrogateCoverage(config=config)
|
|
107
|
+
results = cov.get_coverage()
|
|
108
|
+
|
|
109
|
+
# Process results
|
|
110
|
+
return self._process_interrogate_results(results, config)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise QualityToolError(f"Interrogate analysis failed: {e}", tool="documentation")
|
|
114
|
+
|
|
115
|
+
def _build_interrogate_config(self) -> dict[str, Any]:
|
|
116
|
+
"""Build interrogate configuration from config."""
|
|
117
|
+
config = {}
|
|
118
|
+
|
|
119
|
+
# Set ignore patterns
|
|
120
|
+
ignore_patterns = self.config.get(
|
|
121
|
+
"ignore", ["__pycache__", "*.pyc", "test_*", "tests/*", "*/.venv/*", "*/venv/*"]
|
|
122
|
+
)
|
|
123
|
+
if ignore_patterns:
|
|
124
|
+
# Convert list to regex pattern for interrogate
|
|
125
|
+
pattern_string = "|".join(str(pattern) for pattern in ignore_patterns)
|
|
126
|
+
config["ignore_regex"] = pattern_string
|
|
127
|
+
|
|
128
|
+
# Set what to check - these are the keys that tests expect
|
|
129
|
+
config["ignore_init_method"] = self.config.get("ignore_init_method", True)
|
|
130
|
+
config["ignore_magic"] = self.config.get("ignore_magic", True)
|
|
131
|
+
config["ignore_private"] = self.config.get("ignore_private", False)
|
|
132
|
+
config["ignore_setters"] = self.config.get("ignore_setters", True)
|
|
133
|
+
|
|
134
|
+
# Verbosity and output
|
|
135
|
+
config["verbose"] = self.config.get("verbose", 0)
|
|
136
|
+
if self.config.get("quiet", False):
|
|
137
|
+
config["quiet"] = True
|
|
138
|
+
|
|
139
|
+
return config
|
|
140
|
+
|
|
141
|
+
def _process_interrogate_results(self, results: Any, config: Any) -> QualityResult:
|
|
142
|
+
"""Process interrogate results into QualityResult."""
|
|
143
|
+
# Extract coverage metrics
|
|
144
|
+
total_coverage = results.perc_covered
|
|
145
|
+
missing_count = results.missing_count
|
|
146
|
+
covered_count = results.covered_count
|
|
147
|
+
total_count = missing_count + covered_count
|
|
148
|
+
|
|
149
|
+
# Calculate grade and score
|
|
150
|
+
grade, score = self._calculate_grade_and_score(total_coverage)
|
|
151
|
+
|
|
152
|
+
# Check if passed based on configuration
|
|
153
|
+
passed = self._check_documentation_requirements(total_coverage, grade, score)
|
|
154
|
+
|
|
155
|
+
# Create detailed results
|
|
156
|
+
details = self._build_documentation_details(
|
|
157
|
+
total_coverage, covered_count, missing_count, total_count, grade
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Add file-level details if available
|
|
161
|
+
self._add_file_coverage_details(results, details)
|
|
162
|
+
|
|
163
|
+
return QualityResult(tool="documentation", passed=passed, score=score, details=details)
|
|
164
|
+
|
|
165
|
+
def _calculate_grade_and_score(self, coverage: float) -> tuple[str, float]:
|
|
166
|
+
"""Calculate grade and score based on coverage percentage."""
|
|
167
|
+
grade_thresholds = [
|
|
168
|
+
(95, "A", 100.0),
|
|
169
|
+
(90, "A-", 95.0),
|
|
170
|
+
(85, "B+", 90.0),
|
|
171
|
+
(80, "B", 85.0),
|
|
172
|
+
(75, "B-", 80.0),
|
|
173
|
+
(70, "C+", 75.0),
|
|
174
|
+
(65, "C", 70.0),
|
|
175
|
+
(60, "C-", 65.0),
|
|
176
|
+
(50, "D", 55.0),
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
for threshold, grade, score in grade_thresholds:
|
|
180
|
+
if coverage >= threshold:
|
|
181
|
+
return grade, score
|
|
182
|
+
|
|
183
|
+
return "F", 40.0
|
|
184
|
+
|
|
185
|
+
def _check_documentation_requirements(self, coverage: float, grade: str, score: float) -> bool:
|
|
186
|
+
"""Check if documentation meets the configured requirements."""
|
|
187
|
+
min_coverage = self.config.get("min_coverage", 80.0)
|
|
188
|
+
min_grade = self.config.get("min_grade", "C")
|
|
189
|
+
required_score = self.config.get("min_score", 70.0)
|
|
190
|
+
|
|
191
|
+
grade_values = {"A": 9, "A-": 8, "B+": 7, "B": 6, "B-": 5, "C+": 4, "C": 3, "C-": 2, "D": 1, "F": 0}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
coverage >= min_coverage
|
|
195
|
+
and grade_values.get(grade, 0) >= grade_values.get(min_grade, 0)
|
|
196
|
+
and score >= required_score
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _build_documentation_details(
|
|
200
|
+
self, coverage: float, covered: int, missing: int, total: int, grade: str
|
|
201
|
+
) -> dict[str, Any]:
|
|
202
|
+
"""Build the details dictionary for documentation results."""
|
|
203
|
+
min_coverage = self.config.get("min_coverage", 80.0)
|
|
204
|
+
min_grade = self.config.get("min_grade", "C")
|
|
205
|
+
required_score = self.config.get("min_score", 70.0)
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
"total_coverage": round(coverage, 2),
|
|
209
|
+
"covered_count": covered,
|
|
210
|
+
"missing_count": missing,
|
|
211
|
+
"total_count": total,
|
|
212
|
+
"grade": grade,
|
|
213
|
+
"thresholds": {"min_coverage": min_coverage, "min_grade": min_grade, "min_score": required_score},
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
def _add_file_coverage_details(self, results: Any, details: dict[str, Any]) -> None:
|
|
217
|
+
"""Add file-level coverage details if available."""
|
|
218
|
+
if not (hasattr(results, "detailed_coverage") and results.detailed_coverage):
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
file_details = []
|
|
222
|
+
try:
|
|
223
|
+
for file_info in results.detailed_coverage:
|
|
224
|
+
file_details.append(
|
|
225
|
+
{
|
|
226
|
+
"file": str(file_info.filename),
|
|
227
|
+
"coverage": file_info.perc_covered,
|
|
228
|
+
"covered": file_info.covered_count,
|
|
229
|
+
"missing": file_info.missing_count,
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
details["file_coverage"] = file_details
|
|
233
|
+
except (TypeError, AttributeError):
|
|
234
|
+
# Skip file details if not properly formed
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
def _generate_artifacts(self, result: QualityResult) -> None:
|
|
238
|
+
"""Generate documentation analysis artifacts.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
result: Result to add artifacts to
|
|
242
|
+
"""
|
|
243
|
+
if not self.artifact_dir:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
ensure_dir(self.artifact_dir)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
# Generate JSON report
|
|
250
|
+
json_file = self.artifact_dir / "documentation.json"
|
|
251
|
+
json_data = {
|
|
252
|
+
"tool": result.tool,
|
|
253
|
+
"passed": result.passed,
|
|
254
|
+
"score": result.score,
|
|
255
|
+
"details": result.details,
|
|
256
|
+
"execution_time": result.execution_time,
|
|
257
|
+
}
|
|
258
|
+
atomic_write_text(json_file, json.dumps(json_data, indent=2))
|
|
259
|
+
result.artifacts.append(json_file)
|
|
260
|
+
|
|
261
|
+
# Generate text summary
|
|
262
|
+
summary_file = self.artifact_dir / "documentation_summary.txt"
|
|
263
|
+
summary_report = self._generate_text_report(result)
|
|
264
|
+
atomic_write_text(summary_file, summary_report)
|
|
265
|
+
result.artifacts.append(summary_file)
|
|
266
|
+
|
|
267
|
+
# Generate detailed coverage report if available
|
|
268
|
+
if result.details.get("file_coverage"):
|
|
269
|
+
detail_file = self.artifact_dir / "documentation_details.txt"
|
|
270
|
+
detail_report = self._generate_detail_report(result)
|
|
271
|
+
atomic_write_text(detail_file, detail_report)
|
|
272
|
+
result.artifacts.append(detail_file)
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
# Add error to result details but don't fail
|
|
276
|
+
result.details["artifact_error"] = str(e)
|
|
277
|
+
|
|
278
|
+
def _generate_text_report(self, result: QualityResult) -> str:
|
|
279
|
+
"""Generate text summary report."""
|
|
280
|
+
lines = [
|
|
281
|
+
f"Documentation Coverage Report - {result.tool}",
|
|
282
|
+
"=" * 50,
|
|
283
|
+
f"Status: {'✅ PASSED' if result.passed else '❌ FAILED'}",
|
|
284
|
+
f"Grade: {result.details.get('grade', 'N/A')}",
|
|
285
|
+
f"Coverage: {result.details.get('total_coverage', 0)}%",
|
|
286
|
+
f"Score: {result.score}%",
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
details = result.details
|
|
290
|
+
if "covered_count" in details:
|
|
291
|
+
covered = details["covered_count"]
|
|
292
|
+
missing = details["missing_count"]
|
|
293
|
+
total = details.get("total_count", covered + missing)
|
|
294
|
+
lines.extend(
|
|
295
|
+
[
|
|
296
|
+
"",
|
|
297
|
+
f"Documented Items: {covered}",
|
|
298
|
+
f"Missing Documentation: {missing}",
|
|
299
|
+
f"Total Items: {total}",
|
|
300
|
+
]
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
thresholds = details.get("thresholds", {})
|
|
304
|
+
if thresholds:
|
|
305
|
+
lines.extend(
|
|
306
|
+
[
|
|
307
|
+
"",
|
|
308
|
+
"Thresholds:",
|
|
309
|
+
f" Minimum Coverage: {thresholds.get('min_coverage', 0)}%",
|
|
310
|
+
f" Minimum Grade: {thresholds.get('min_grade', 'N/A')}",
|
|
311
|
+
f" Minimum Score: {thresholds.get('min_score', 0)}%",
|
|
312
|
+
]
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if result.execution_time:
|
|
316
|
+
lines.append(f"\nExecution Time: {result.execution_time:.2f}s")
|
|
317
|
+
|
|
318
|
+
return "\n".join(lines)
|
|
319
|
+
|
|
320
|
+
def _generate_detail_report(self, result: QualityResult) -> str:
|
|
321
|
+
"""Generate detailed file coverage report."""
|
|
322
|
+
lines = ["Documentation Coverage by File", "=" * 50, ""]
|
|
323
|
+
|
|
324
|
+
file_coverage = result.details.get("file_coverage", [])
|
|
325
|
+
|
|
326
|
+
# Sort by coverage (lowest first to highlight problem files)
|
|
327
|
+
sorted_files = sorted(file_coverage, key=lambda x: x["coverage"])
|
|
328
|
+
|
|
329
|
+
for file_info in sorted_files:
|
|
330
|
+
coverage = file_info["coverage"]
|
|
331
|
+
status = "✅" if coverage >= 80 else "⚠️" if coverage >= 60 else "❌"
|
|
332
|
+
lines.append(
|
|
333
|
+
f"{status} {file_info['file']}: {coverage:.1f}% ({file_info['covered']}/{file_info['covered'] + file_info['missing']})"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return "\n".join(lines)
|
|
337
|
+
|
|
338
|
+
def report(self, result: QualityResult, format: str = "terminal") -> str:
|
|
339
|
+
"""Generate report from QualityResult (implements QualityTool protocol).
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
result: Documentation result
|
|
343
|
+
format: Report format
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Formatted report
|
|
347
|
+
"""
|
|
348
|
+
if format == "terminal":
|
|
349
|
+
return self._generate_text_report(result)
|
|
350
|
+
elif format == "json":
|
|
351
|
+
return json.dumps(
|
|
352
|
+
{
|
|
353
|
+
"tool": result.tool,
|
|
354
|
+
"passed": result.passed,
|
|
355
|
+
"score": result.score,
|
|
356
|
+
"details": result.details,
|
|
357
|
+
},
|
|
358
|
+
indent=2,
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
return str(result.details)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Documentation coverage fixture for pytest integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from ..base import BaseQualityFixture
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from .checker import INTERROGATE_AVAILABLE, DocumentationChecker
|
|
14
|
+
except ImportError:
|
|
15
|
+
DocumentationChecker = None
|
|
16
|
+
INTERROGATE_AVAILABLE = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DocumentationFixture(BaseQualityFixture):
|
|
20
|
+
"""Pytest fixture for documentation coverage analysis.
|
|
21
|
+
|
|
22
|
+
Provides easy access to documentation coverage checking with automatic
|
|
23
|
+
setup and teardown. Integrates with the quality framework fixtures.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: dict[str, Any] | None = None, artifact_dir: Path | None = None):
|
|
27
|
+
"""Initialize documentation fixture.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
config: Documentation checker configuration
|
|
31
|
+
artifact_dir: Directory for artifacts
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(config or {}, artifact_dir)
|
|
34
|
+
self.analyzer: DocumentationChecker | None = None
|
|
35
|
+
|
|
36
|
+
def setup(self) -> None:
|
|
37
|
+
"""Set up documentation analyzer."""
|
|
38
|
+
if not INTERROGATE_AVAILABLE:
|
|
39
|
+
pytest.skip("interrogate not available")
|
|
40
|
+
|
|
41
|
+
self.analyzer = DocumentationChecker(self.config)
|
|
42
|
+
self._setup_complete = True
|
|
43
|
+
|
|
44
|
+
def teardown(self) -> None:
|
|
45
|
+
"""Clean up documentation analyzer."""
|
|
46
|
+
self.analyzer = None
|
|
47
|
+
self._setup_complete = False
|
|
48
|
+
|
|
49
|
+
def analyze(self, path: Path) -> dict[str, Any]:
|
|
50
|
+
"""Run documentation coverage analysis.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
path: Path to analyze
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Analysis results as dict
|
|
57
|
+
"""
|
|
58
|
+
if not self.analyzer:
|
|
59
|
+
return {"error": "Analyzer not available"}
|
|
60
|
+
|
|
61
|
+
result = self.analyzer.analyze(path, artifact_dir=self.artifact_dir)
|
|
62
|
+
self.add_result(result)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"passed": result.passed,
|
|
66
|
+
"score": result.score,
|
|
67
|
+
"grade": result.details.get("grade"),
|
|
68
|
+
"total_coverage": result.details.get("total_coverage"),
|
|
69
|
+
"covered_count": result.details.get("covered_count"),
|
|
70
|
+
"missing_count": result.details.get("missing_count"),
|
|
71
|
+
"total_count": result.details.get("total_count"),
|
|
72
|
+
"file_coverage": result.details.get("file_coverage", []),
|
|
73
|
+
"thresholds": result.details.get("thresholds", {}),
|
|
74
|
+
"execution_time": result.execution_time,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def check(
|
|
78
|
+
self,
|
|
79
|
+
path: Path,
|
|
80
|
+
min_coverage: float | None = None,
|
|
81
|
+
min_grade: str | None = None,
|
|
82
|
+
min_score: float | None = None,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Check documentation coverage with optional thresholds.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
path: Path to check
|
|
88
|
+
min_coverage: Minimum coverage percentage required
|
|
89
|
+
min_grade: Minimum grade required (A, B, C, D, F)
|
|
90
|
+
min_score: Minimum score required
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Check results including pass/fail status
|
|
94
|
+
"""
|
|
95
|
+
if not self._setup_complete:
|
|
96
|
+
self.setup()
|
|
97
|
+
|
|
98
|
+
# Update config with provided thresholds
|
|
99
|
+
if min_coverage is not None:
|
|
100
|
+
self.config["min_coverage"] = min_coverage
|
|
101
|
+
if min_grade is not None:
|
|
102
|
+
self.config["min_grade"] = min_grade
|
|
103
|
+
if min_score is not None:
|
|
104
|
+
self.config["min_score"] = min_score
|
|
105
|
+
|
|
106
|
+
# Recreate analyzer with updated config
|
|
107
|
+
if self.analyzer and any(x is not None for x in [min_coverage, min_grade, min_score]):
|
|
108
|
+
self.analyzer = DocumentationChecker(self.config)
|
|
109
|
+
|
|
110
|
+
return self.analyze(path)
|
|
111
|
+
|
|
112
|
+
def generate_report(self, format: str = "terminal") -> str:
|
|
113
|
+
"""Generate documentation report.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
format: Report format (terminal, json)
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Formatted report
|
|
120
|
+
"""
|
|
121
|
+
if not self.analyzer:
|
|
122
|
+
return "No documentation analyzer available"
|
|
123
|
+
|
|
124
|
+
if not self.results:
|
|
125
|
+
return "No documentation results available"
|
|
126
|
+
|
|
127
|
+
# Use the most recent result
|
|
128
|
+
latest_result = self.results[-1]
|
|
129
|
+
return self.analyzer.report(latest_result, format)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.fixture
|
|
133
|
+
def documentation_checker() -> DocumentationFixture:
|
|
134
|
+
"""Provide documentation coverage checker fixture.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
DocumentationFixture instance
|
|
138
|
+
"""
|
|
139
|
+
fixture = DocumentationFixture()
|
|
140
|
+
fixture.setup()
|
|
141
|
+
yield fixture
|
|
142
|
+
fixture.teardown()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.fixture
|
|
146
|
+
def documentation_config() -> dict[str, Any]:
|
|
147
|
+
"""Provide default documentation configuration.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Default configuration for documentation checking
|
|
151
|
+
"""
|
|
152
|
+
return {
|
|
153
|
+
"min_coverage": 80.0,
|
|
154
|
+
"min_grade": "C",
|
|
155
|
+
"min_score": 70.0,
|
|
156
|
+
"ignore_init_method": True,
|
|
157
|
+
"ignore_magic": True,
|
|
158
|
+
"ignore_setters": True,
|
|
159
|
+
"ignore": ["__pycache__", "*.pyc", "test_*", "tests/*", "*/.venv/*", "*/venv/*"],
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.fixture
|
|
164
|
+
def documentation_checker_strict(documentation_config: dict[str, Any]) -> DocumentationFixture:
|
|
165
|
+
"""Provide strict documentation checker fixture.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
documentation_config: Base configuration
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
DocumentationFixture with strict requirements
|
|
172
|
+
"""
|
|
173
|
+
config = documentation_config.copy()
|
|
174
|
+
config.update(
|
|
175
|
+
{
|
|
176
|
+
"min_coverage": 95.0,
|
|
177
|
+
"min_grade": "A",
|
|
178
|
+
"min_score": 95.0,
|
|
179
|
+
"ignore_init_method": False,
|
|
180
|
+
"ignore_magic": False,
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
fixture = DocumentationFixture(config)
|
|
185
|
+
fixture.setup()
|
|
186
|
+
yield fixture
|
|
187
|
+
fixture.teardown()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Performance profiling and analysis for provide-testkit.
|
|
2
|
+
|
|
3
|
+
Provides performance profiling analysis using memray, cProfile, and other tools.
|
|
4
|
+
Integrates with the quality framework for comprehensive performance analysis.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Memory profiling with memray
|
|
8
|
+
- CPU profiling with cProfile
|
|
9
|
+
- Performance regression detection
|
|
10
|
+
- Integration with quality gates
|
|
11
|
+
- Configurable profiling options
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
# Basic memory profiling
|
|
15
|
+
def test_with_profiling(profiling_fixture):
|
|
16
|
+
result = profiling_fixture.profile_memory(function, *args)
|
|
17
|
+
assert result.passed
|
|
18
|
+
|
|
19
|
+
# CPU profiling with quality gates
|
|
20
|
+
runner = QualityRunner()
|
|
21
|
+
results = runner.run_with_gates(path, {"profiling": {"max_memory_mb": 100}})
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .fixture import ProfilingFixture
|
|
25
|
+
from .profiler import PerformanceProfiler
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"PerformanceProfiler",
|
|
29
|
+
"ProfilingFixture",
|
|
30
|
+
]
|