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.
Files changed (66) hide show
  1. provide/__init__.py +3 -0
  2. provide/testkit/__init__.py +248 -0
  3. provide/testkit/archive/__init__.py +24 -0
  4. provide/testkit/archive/fixtures.py +217 -0
  5. provide/testkit/cli.py +229 -0
  6. provide/testkit/common/__init__.py +32 -0
  7. provide/testkit/common/fixtures.py +234 -0
  8. provide/testkit/crypto.py +163 -0
  9. provide/testkit/environment.py +79 -0
  10. provide/testkit/file/__init__.py +40 -0
  11. provide/testkit/file/content_fixtures.py +275 -0
  12. provide/testkit/file/directory_fixtures.py +105 -0
  13. provide/testkit/file/fixtures.py +49 -0
  14. provide/testkit/file/special_fixtures.py +141 -0
  15. provide/testkit/fixtures.py +52 -0
  16. provide/testkit/harness.py +122 -0
  17. provide/testkit/hub.py +22 -0
  18. provide/testkit/logger/__init__.py +39 -0
  19. provide/testkit/logger/hooks.py +100 -0
  20. provide/testkit/logger/reset.py +230 -0
  21. provide/testkit/main.py +22 -0
  22. provide/testkit/mocking/__init__.py +46 -0
  23. provide/testkit/mocking/fixtures.py +340 -0
  24. provide/testkit/process/__init__.py +48 -0
  25. provide/testkit/process/async_fixtures.py +410 -0
  26. provide/testkit/process/fixtures.py +54 -0
  27. provide/testkit/process/subprocess_fixtures.py +208 -0
  28. provide/testkit/quality/__init__.py +101 -0
  29. provide/testkit/quality/artifacts.py +360 -0
  30. provide/testkit/quality/base.py +158 -0
  31. provide/testkit/quality/cli.py +394 -0
  32. provide/testkit/quality/complexity/__init__.py +30 -0
  33. provide/testkit/quality/complexity/analyzer.py +392 -0
  34. provide/testkit/quality/complexity/fixture.py +196 -0
  35. provide/testkit/quality/coverage/__init__.py +36 -0
  36. provide/testkit/quality/coverage/fixture.py +236 -0
  37. provide/testkit/quality/coverage/reporter.py +150 -0
  38. provide/testkit/quality/coverage/tracker.py +313 -0
  39. provide/testkit/quality/decorators.py +380 -0
  40. provide/testkit/quality/documentation/__init__.py +29 -0
  41. provide/testkit/quality/documentation/checker.py +361 -0
  42. provide/testkit/quality/documentation/fixture.py +187 -0
  43. provide/testkit/quality/profiling/__init__.py +30 -0
  44. provide/testkit/quality/profiling/fixture.py +332 -0
  45. provide/testkit/quality/profiling/profiler.py +428 -0
  46. provide/testkit/quality/report.py +266 -0
  47. provide/testkit/quality/runner.py +319 -0
  48. provide/testkit/quality/security/__init__.py +29 -0
  49. provide/testkit/quality/security/fixture.py +196 -0
  50. provide/testkit/quality/security/scanner.py +338 -0
  51. provide/testkit/streams.py +54 -0
  52. provide/testkit/threading/__init__.py +38 -0
  53. provide/testkit/threading/basic_fixtures.py +103 -0
  54. provide/testkit/threading/data_fixtures.py +101 -0
  55. provide/testkit/threading/execution_fixtures.py +268 -0
  56. provide/testkit/threading/fixtures.py +50 -0
  57. provide/testkit/threading/sync_fixtures.py +98 -0
  58. provide/testkit/time/__init__.py +32 -0
  59. provide/testkit/time/fixtures.py +416 -0
  60. provide/testkit/transport/__init__.py +30 -0
  61. provide/testkit/transport/fixtures.py +278 -0
  62. provide_testkit-0.0.0.dev0.dist-info/METADATA +145 -0
  63. provide_testkit-0.0.0.dev0.dist-info/RECORD +66 -0
  64. provide_testkit-0.0.0.dev0.dist-info/WHEEL +5 -0
  65. provide_testkit-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  66. 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
+ ]