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,236 @@
1
+ """Pytest fixtures for coverage tracking."""
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 .tracker import COVERAGE_AVAILABLE, CoverageTracker
13
+
14
+
15
+ class CoverageFixture(BaseQualityFixture):
16
+ """Pytest fixture for coverage tracking integration."""
17
+
18
+ def __init__(self, config: dict[str, Any] | None = None, artifact_dir: Path | None = None):
19
+ """Initialize coverage fixture.
20
+
21
+ Args:
22
+ config: Coverage configuration
23
+ artifact_dir: Directory for artifacts
24
+ """
25
+ super().__init__(config, artifact_dir)
26
+ self.tracker: CoverageTracker | None = None
27
+
28
+ def setup(self) -> None:
29
+ """Setup coverage tracking."""
30
+ if not COVERAGE_AVAILABLE:
31
+ pytest.skip("Coverage.py not available")
32
+
33
+ try:
34
+ self.tracker = CoverageTracker(self.config)
35
+ except Exception as e:
36
+ pytest.skip(f"Failed to initialize coverage: {e}")
37
+
38
+ def teardown(self) -> None:
39
+ """Stop coverage and generate reports."""
40
+ if self.tracker and self.tracker.is_running:
41
+ self.tracker.stop()
42
+
43
+ def start_tracking(self) -> None:
44
+ """Start coverage tracking."""
45
+ self.ensure_setup()
46
+ if self.tracker:
47
+ self.tracker.start()
48
+
49
+ def stop_tracking(self) -> None:
50
+ """Stop coverage tracking."""
51
+ if self.tracker:
52
+ self.tracker.stop()
53
+
54
+ def get_coverage(self) -> float:
55
+ """Get current coverage percentage."""
56
+ if not self.tracker:
57
+ return 0.0
58
+ return self.tracker.get_coverage()
59
+
60
+ def generate_report(self, format: str = "terminal") -> str:
61
+ """Generate coverage report."""
62
+ if not self.tracker:
63
+ return "No coverage data"
64
+ return self.tracker.generate_report(format)
65
+
66
+
67
+ @pytest.fixture
68
+ def coverage_tracker(request, tmp_path) -> Generator[CoverageFixture, None, None]:
69
+ """Pytest fixture for coverage tracking.
70
+
71
+ Provides a CoverageFixture instance that automatically starts and stops
72
+ coverage tracking around individual tests.
73
+
74
+ Usage:
75
+ def test_with_coverage(coverage_tracker):
76
+ coverage_tracker.start_tracking()
77
+ # ... test code
78
+ coverage_tracker.stop_tracking()
79
+ assert coverage_tracker.get_coverage() > 80
80
+ """
81
+ # Get configuration from pytest request
82
+ config = getattr(request, "param", {})
83
+
84
+ # Create artifact directory for this test
85
+ artifact_dir = tmp_path / "coverage"
86
+
87
+ # Initialize fixture
88
+ fixture = CoverageFixture(config=config, artifact_dir=artifact_dir)
89
+
90
+ try:
91
+ fixture.setup()
92
+ yield fixture
93
+ finally:
94
+ fixture.teardown()
95
+
96
+
97
+ @pytest.fixture
98
+ def auto_coverage(coverage_tracker) -> Generator[CoverageFixture, None, None]:
99
+ """Automatic coverage tracking fixture.
100
+
101
+ Automatically starts coverage at the beginning of the test and stops
102
+ at the end. Ideal for tests that want zero-configuration coverage.
103
+
104
+ Usage:
105
+ def test_automatic_coverage(auto_coverage):
106
+ # Coverage automatically tracked
107
+ result = some_function()
108
+ assert result is not None
109
+ # Coverage automatically stopped and saved
110
+ """
111
+ coverage_tracker.start_tracking()
112
+ try:
113
+ yield coverage_tracker
114
+ finally:
115
+ coverage_tracker.stop_tracking()
116
+
117
+
118
+ @pytest.fixture(scope="session")
119
+ def session_coverage(tmp_path_factory) -> Generator[CoverageFixture, None, None]:
120
+ """Session-wide coverage tracking.
121
+
122
+ Tracks coverage across all tests in the session. Useful for getting
123
+ overall coverage metrics for the entire test suite.
124
+
125
+ Usage:
126
+ def test_part_one(session_coverage):
127
+ # Coverage tracked across all tests
128
+ pass
129
+
130
+ def test_part_two(session_coverage):
131
+ # Same coverage instance
132
+ pass
133
+ """
134
+ # Create session-wide artifact directory
135
+ artifact_dir = tmp_path_factory.mktemp("session_coverage")
136
+
137
+ # Initialize session fixture
138
+ fixture = CoverageFixture(artifact_dir=artifact_dir)
139
+
140
+ try:
141
+ fixture.setup()
142
+ fixture.start_tracking()
143
+ yield fixture
144
+ finally:
145
+ fixture.stop_tracking()
146
+ fixture.teardown()
147
+
148
+
149
+ @pytest.fixture
150
+ def coverage_config():
151
+ """Default coverage configuration fixture.
152
+
153
+ Returns standard coverage configuration that can be customized
154
+ per test or project.
155
+
156
+ Usage:
157
+ def test_with_custom_coverage(coverage_config):
158
+ coverage_config["fail_under"] = 95
159
+ # Use with parametrized coverage_tracker
160
+ """
161
+ return {
162
+ "branch": True,
163
+ "source": ["src"],
164
+ "omit": [
165
+ "*/tests/*",
166
+ "*/test_*",
167
+ "*/.venv/*",
168
+ "*/venv/*",
169
+ ],
170
+ "fail_under": 80,
171
+ "show_missing": True,
172
+ "skip_covered": False,
173
+ }
174
+
175
+
176
+ # Parametrized fixtures for different coverage configurations
177
+ @pytest.fixture(
178
+ params=[
179
+ {"fail_under": 80, "branch": True},
180
+ {"fail_under": 90, "branch": False},
181
+ {"fail_under": 95, "branch": True, "show_missing": True},
182
+ ]
183
+ )
184
+ def parametrized_coverage(request, tmp_path) -> Generator[CoverageFixture, None, None]:
185
+ """Parametrized coverage fixture for testing different configurations.
186
+
187
+ Automatically runs tests with different coverage configurations
188
+ to validate behavior under various settings.
189
+
190
+ Usage:
191
+ def test_coverage_configs(parametrized_coverage):
192
+ # Test runs multiple times with different configs
193
+ pass
194
+ """
195
+ config = request.param
196
+ artifact_dir = tmp_path / f"coverage_{id(config)}"
197
+
198
+ fixture = CoverageFixture(config=config, artifact_dir=artifact_dir)
199
+
200
+ try:
201
+ fixture.setup()
202
+ yield fixture
203
+ finally:
204
+ fixture.teardown()
205
+
206
+
207
+ # Pytest hooks for automatic coverage integration
208
+ def pytest_configure(config):
209
+ """Configure pytest with coverage markers."""
210
+ config.addinivalue_line("markers", "coverage: mark test to run with coverage tracking")
211
+ config.addinivalue_line("markers", "no_coverage: mark test to skip coverage tracking")
212
+
213
+
214
+ @pytest.fixture(autouse=True)
215
+ def auto_coverage_marker(request):
216
+ """Automatically apply coverage to marked tests.
217
+
218
+ Tests marked with @pytest.mark.coverage will automatically
219
+ get coverage tracking without needing to explicitly use fixtures.
220
+ """
221
+ if request.node.get_closest_marker("coverage"):
222
+ # Test is marked for coverage - enable automatic tracking
223
+ if not request.node.get_closest_marker("no_coverage"):
224
+ # Create temporary coverage fixture
225
+ coverage_fixture = CoverageFixture()
226
+ try:
227
+ coverage_fixture.setup()
228
+ coverage_fixture.start_tracking()
229
+ yield
230
+ finally:
231
+ coverage_fixture.stop_tracking()
232
+ coverage_fixture.teardown()
233
+ else:
234
+ yield
235
+ else:
236
+ yield
@@ -0,0 +1,150 @@
1
+ """Coverage reporting utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..base import QualityResult
8
+
9
+
10
+ class CoverageReporter:
11
+ """Specialized reporter for coverage results."""
12
+
13
+ def __init__(self, config: dict[str, Any] | None = None):
14
+ """Initialize coverage reporter.
15
+
16
+ Args:
17
+ config: Reporter configuration
18
+ """
19
+ self.config = config or {}
20
+
21
+ def format_terminal_report(self, result: QualityResult) -> str:
22
+ """Format coverage result for terminal output.
23
+
24
+ Args:
25
+ result: Coverage result to format
26
+
27
+ Returns:
28
+ Formatted terminal report
29
+ """
30
+ lines = [
31
+ f"Coverage Report - {result.tool}",
32
+ "=" * 40,
33
+ f"Status: {'✅ PASSED' if result.passed else '❌ FAILED'}",
34
+ ]
35
+
36
+ if result.score is not None:
37
+ lines.append(f"Coverage: {result.score}%")
38
+
39
+ details = result.details
40
+ if "total_statements" in details:
41
+ lines.extend(
42
+ [
43
+ f"Total Statements: {details.get('total_statements', 0)}",
44
+ f"Missing Statements: {details.get('missing_statements', 0)}",
45
+ ]
46
+ )
47
+
48
+ if "branch_coverage" in details and details["branch_coverage"] is not None:
49
+ lines.append(f"Branch Coverage: {details['branch_coverage']}%")
50
+
51
+ if "threshold" in details:
52
+ lines.append(f"Threshold: {details['threshold']}%")
53
+
54
+ if result.execution_time:
55
+ lines.append(f"Execution Time: {result.execution_time:.2f}s")
56
+
57
+ return "\n".join(lines)
58
+
59
+ def format_json_report(self, result: QualityResult) -> dict[str, Any]:
60
+ """Format coverage result as JSON data.
61
+
62
+ Args:
63
+ result: Coverage result to format
64
+
65
+ Returns:
66
+ JSON-serializable report data
67
+ """
68
+ return {
69
+ "tool": result.tool,
70
+ "passed": result.passed,
71
+ "score": result.score,
72
+ "details": result.details,
73
+ "execution_time": result.execution_time,
74
+ "artifacts": [str(p) for p in result.artifacts],
75
+ }
76
+
77
+ def format_html_summary(self, result: QualityResult) -> str:
78
+ """Format coverage result as HTML summary.
79
+
80
+ Args:
81
+ result: Coverage result to format
82
+
83
+ Returns:
84
+ HTML summary
85
+ """
86
+ status_color = "green" if result.passed else "red"
87
+ status_text = "PASSED" if result.passed else "FAILED"
88
+
89
+ html_parts = [
90
+ '<div class="coverage-summary">',
91
+ "<h3>Coverage Report</h3>",
92
+ f'<p><span style="color: {status_color}">Status: {status_text}</span></p>',
93
+ ]
94
+
95
+ if result.score is not None:
96
+ html_parts.append(f"<p>Coverage: <strong>{result.score}%</strong></p>")
97
+
98
+ details = result.details
99
+ if "total_statements" in details:
100
+ html_parts.extend(
101
+ [
102
+ f"<p>Total Statements: {details.get('total_statements', 0)}</p>",
103
+ f"<p>Missing Statements: {details.get('missing_statements', 0)}</p>",
104
+ ]
105
+ )
106
+
107
+ html_parts.append("</div>")
108
+ return "\n".join(html_parts)
109
+
110
+ def generate_dashboard_data(self, result: QualityResult) -> dict[str, Any]:
111
+ """Generate data for coverage dashboard.
112
+
113
+ Args:
114
+ result: Coverage result
115
+
116
+ Returns:
117
+ Dashboard data structure
118
+ """
119
+ dashboard_data: dict[str, Any] = {
120
+ "title": "Code Coverage",
121
+ "status": "passed" if result.passed else "failed",
122
+ "primary_metric": {
123
+ "label": "Coverage",
124
+ "value": result.score,
125
+ "unit": "%",
126
+ "threshold": result.details.get("threshold", 0),
127
+ },
128
+ "secondary_metrics": [],
129
+ }
130
+
131
+ details = result.details
132
+ secondary_metrics = dashboard_data["secondary_metrics"]
133
+ if "total_statements" in details and isinstance(secondary_metrics, list):
134
+ secondary_metrics.extend(
135
+ [
136
+ {"label": "Total Statements", "value": details.get("total_statements", 0)},
137
+ {"label": "Missing Statements", "value": details.get("missing_statements", 0)},
138
+ ]
139
+ )
140
+
141
+ if (
142
+ "branch_coverage" in details
143
+ and details["branch_coverage"] is not None
144
+ and isinstance(secondary_metrics, list)
145
+ ):
146
+ secondary_metrics.append(
147
+ {"label": "Branch Coverage", "value": details["branch_coverage"], "unit": "%"}
148
+ )
149
+
150
+ return dashboard_data
@@ -0,0 +1,313 @@
1
+ """Coverage tracking implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import time
7
+ from typing import Any
8
+
9
+ try:
10
+ from coverage import Coverage
11
+
12
+ COVERAGE_AVAILABLE = True
13
+ except ImportError:
14
+ COVERAGE_AVAILABLE = False
15
+ Coverage = None
16
+
17
+ from ..base import QualityResult, QualityToolError
18
+
19
+
20
+ class CoverageTracker:
21
+ """Wrapper for coverage.py library with testkit integration.
22
+
23
+ Provides high-level interface for coverage tracking with automatic
24
+ artifact management and integration with the quality framework.
25
+ """
26
+
27
+ def __init__(self, config: dict[str, Any] | None = None):
28
+ """Initialize coverage tracker.
29
+
30
+ Args:
31
+ config: Coverage configuration options
32
+ """
33
+ if not COVERAGE_AVAILABLE:
34
+ raise QualityToolError(
35
+ "Coverage.py not available. Install with: pip install coverage", tool="coverage"
36
+ )
37
+
38
+ self.config = config or {}
39
+ self.coverage: Coverage | None = None
40
+ self.is_running = False
41
+ self.artifact_dir: Path | None = None
42
+
43
+ def analyze(self, path: Path, **kwargs: Any) -> QualityResult:
44
+ """Run coverage analysis on the given path.
45
+
46
+ Args:
47
+ path: Path to analyze
48
+ **kwargs: Additional options including artifact_dir
49
+
50
+ Returns:
51
+ QualityResult with coverage data
52
+ """
53
+ self.artifact_dir = kwargs.get("artifact_dir", Path(".coverage"))
54
+ start_time = time.time()
55
+
56
+ try:
57
+ # If coverage is already running, get current data
58
+ if self.is_running and self.coverage:
59
+ self.stop()
60
+
61
+ # Start fresh coverage analysis
62
+ self.start()
63
+
64
+ # For analysis mode, we need to combine existing coverage data
65
+ # This is typically used when coverage was collected during test runs
66
+ self._load_existing_data()
67
+
68
+ # Generate report
69
+ result = self._create_result()
70
+ result.execution_time = time.time() - start_time
71
+
72
+ # Generate artifacts
73
+ self._generate_artifacts(result)
74
+
75
+ return result
76
+
77
+ except Exception as e:
78
+ return QualityResult(
79
+ tool="coverage",
80
+ passed=False,
81
+ details={"error": str(e), "error_type": type(e).__name__},
82
+ execution_time=time.time() - start_time,
83
+ )
84
+
85
+ def start(self) -> None:
86
+ """Start coverage tracking."""
87
+ if self.is_running:
88
+ return
89
+
90
+ coverage_config = self._build_coverage_config()
91
+ self.coverage = Coverage(**coverage_config)
92
+ self.coverage.start()
93
+ self.is_running = True
94
+
95
+ def stop(self) -> None:
96
+ """Stop coverage tracking and save data."""
97
+ if not self.is_running or not self.coverage:
98
+ return
99
+
100
+ self.coverage.stop()
101
+ self.coverage.save()
102
+ self.is_running = False
103
+
104
+ def get_coverage(self) -> float:
105
+ """Get current coverage percentage.
106
+
107
+ Returns:
108
+ Coverage percentage (0-100)
109
+ """
110
+ if not self.coverage:
111
+ return 0.0
112
+
113
+ try:
114
+ # Get coverage data
115
+ total = self.coverage.report(file=None, show_missing=False)
116
+ return round(total, 2)
117
+ except Exception:
118
+ return 0.0
119
+
120
+ def generate_report(self, format: str = "terminal") -> str:
121
+ """Generate coverage report.
122
+
123
+ Args:
124
+ format: Report format (terminal, html, xml, json)
125
+
126
+ Returns:
127
+ Report content (for terminal/json) or path (for html/xml)
128
+ """
129
+ if not self.coverage:
130
+ return "No coverage data available"
131
+
132
+ if format == "terminal":
133
+ return self._generate_terminal_report()
134
+ elif format == "html" and self.artifact_dir:
135
+ html_dir = self.artifact_dir / "htmlcov"
136
+ self.coverage.html_report(directory=str(html_dir))
137
+ return str(html_dir / "index.html")
138
+ elif format == "xml" and self.artifact_dir:
139
+ xml_file = self.artifact_dir / "coverage.xml"
140
+ self.coverage.xml_report(outfile=str(xml_file))
141
+ return str(xml_file)
142
+ elif format == "json" and self.artifact_dir:
143
+ json_file = self.artifact_dir / "coverage.json"
144
+ self.coverage.json_report(outfile=str(json_file))
145
+ return json_file.read_text()
146
+ else:
147
+ return f"Unsupported format: {format}"
148
+
149
+ def report(self, result: QualityResult, format: str = "terminal") -> str:
150
+ """Generate report from QualityResult (implements QualityTool protocol).
151
+
152
+ Args:
153
+ result: Coverage result
154
+ format: Report format
155
+
156
+ Returns:
157
+ Formatted report
158
+ """
159
+ if format == "terminal":
160
+ lines = [
161
+ f"Coverage Report - {result.tool}",
162
+ "=" * 40,
163
+ f"Status: {'✅ PASSED' if result.passed else '❌ FAILED'}",
164
+ ]
165
+
166
+ if result.score is not None:
167
+ lines.append(f"Coverage: {result.score}%")
168
+
169
+ if "total_statements" in result.details:
170
+ details = result.details
171
+ lines.extend(
172
+ [
173
+ f"Total Statements: {details.get('total_statements', 0)}",
174
+ f"Missing Statements: {details.get('missing_statements', 0)}",
175
+ f"Branch Coverage: {details.get('branch_coverage', 'N/A')}%",
176
+ ]
177
+ )
178
+
179
+ return "\n".join(lines)
180
+
181
+ return str(result.details)
182
+
183
+ def _build_coverage_config(self) -> dict[str, Any]:
184
+ """Build coverage.py configuration."""
185
+ config = {
186
+ "branch": self.config.get("branch", True),
187
+ "source": self.config.get("source", ["src"]),
188
+ "omit": self.config.get(
189
+ "omit", ["*/tests/*", "*/test_*", "*/.venv/*", "*/venv/*", "*/__pycache__/*"]
190
+ ),
191
+ }
192
+
193
+ # Add data file configuration if artifact directory is set
194
+ if self.artifact_dir:
195
+ config["data_file"] = str(self.artifact_dir / ".coverage")
196
+
197
+ return config
198
+
199
+ def _load_existing_data(self) -> None:
200
+ """Load existing coverage data if available."""
201
+ if not self.coverage or not self.artifact_dir:
202
+ return
203
+
204
+ data_file = self.artifact_dir / ".coverage"
205
+ if data_file.exists():
206
+ try:
207
+ self.coverage.load()
208
+ except Exception:
209
+ # If loading fails, continue with fresh data
210
+ pass
211
+
212
+ def _create_result(self) -> QualityResult:
213
+ """Create QualityResult from current coverage data."""
214
+ if not self.coverage:
215
+ return QualityResult(tool="coverage", passed=False, details={"error": "No coverage instance"})
216
+
217
+ try:
218
+ # Get coverage percentage
219
+ coverage_percent = self.get_coverage()
220
+
221
+ # Get detailed metrics
222
+ total_statements = 0
223
+ missing_statements = 0
224
+ branch_coverage = None
225
+
226
+ # Access coverage data for detailed metrics
227
+ data = self.coverage.get_data()
228
+ if data:
229
+ try:
230
+ # Count statements across all files
231
+ for filename in data.measured_files():
232
+ file_data = data.lines(filename)
233
+ if file_data:
234
+ total_statements += len(file_data)
235
+
236
+ # Get missing statements
237
+ if hasattr(self.coverage, "_analyze"):
238
+ for filename in data.measured_files():
239
+ try:
240
+ analysis = self.coverage._analyze(filename)
241
+ missing_statements += len(analysis.missing)
242
+ except Exception:
243
+ continue
244
+ except Exception:
245
+ # Handle mock objects or other issues gracefully
246
+ pass
247
+
248
+ # Calculate pass/fail based on configured threshold
249
+ threshold = self.config.get("fail_under", 0)
250
+ passed = coverage_percent >= threshold
251
+
252
+ return QualityResult(
253
+ tool="coverage",
254
+ passed=passed,
255
+ score=coverage_percent,
256
+ details={
257
+ "total_statements": total_statements,
258
+ "missing_statements": missing_statements,
259
+ "branch_coverage": branch_coverage,
260
+ "threshold": threshold,
261
+ },
262
+ )
263
+
264
+ except Exception as e:
265
+ return QualityResult(tool="coverage", passed=False, details={"error": str(e)})
266
+
267
+ def _generate_terminal_report(self) -> str:
268
+ """Generate terminal coverage report."""
269
+ if not self.coverage:
270
+ return "No coverage data available"
271
+
272
+ try:
273
+ from io import StringIO
274
+
275
+ output = StringIO()
276
+ self.coverage.report(file=output, show_missing=True)
277
+ return output.getvalue()
278
+ except Exception as e:
279
+ return f"Error generating report: {e}"
280
+
281
+ def _generate_artifacts(self, result: QualityResult) -> None:
282
+ """Generate coverage artifacts.
283
+
284
+ Args:
285
+ result: Result to add artifacts to
286
+ """
287
+ if not self.artifact_dir:
288
+ return
289
+
290
+ self.artifact_dir.mkdir(parents=True, exist_ok=True)
291
+
292
+ try:
293
+ # Generate HTML report
294
+ html_dir = self.artifact_dir / "htmlcov"
295
+ self.coverage.html_report(directory=str(html_dir))
296
+ if html_dir.exists():
297
+ result.artifacts.append(html_dir / "index.html")
298
+
299
+ # Generate XML report
300
+ xml_file = self.artifact_dir / "coverage.xml"
301
+ self.coverage.xml_report(outfile=str(xml_file))
302
+ if xml_file.exists():
303
+ result.artifacts.append(xml_file)
304
+
305
+ # Generate terminal report
306
+ terminal_file = self.artifact_dir / "coverage.txt"
307
+ terminal_report = self._generate_terminal_report()
308
+ terminal_file.write_text(terminal_report)
309
+ result.artifacts.append(terminal_file)
310
+
311
+ except Exception as e:
312
+ # Add error to result details but don't fail
313
+ result.details["artifact_error"] = str(e)