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,319 @@
1
+ """Quality runner for orchestrating multiple quality tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
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
+ from .base import QualityError, QualityResult, QualityTool
13
+
14
+
15
+ class QualityRunner:
16
+ """Orchestrates multiple quality analysis tools.
17
+
18
+ Manages the execution of quality tools, artifact collection,
19
+ and result aggregation with configurable quality gates.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ artifact_root: Path | None = None,
25
+ tools: list[str] | None = None,
26
+ config: dict[str, Any] | None = None,
27
+ ):
28
+ """Initialize the quality runner.
29
+
30
+ Args:
31
+ artifact_root: Root directory for storing artifacts (defaults to .quality-artifacts)
32
+ tools: List of tool names to run (None for default set)
33
+ config: Configuration for tools and runner
34
+ """
35
+ self.artifact_root = Path(artifact_root) if artifact_root else Path(".quality-artifacts")
36
+ self.config = config or {}
37
+ self.tools = tools or self._get_default_tools()
38
+ self.tool_instances: dict[str, QualityTool] = {}
39
+ self._initialize_tools()
40
+
41
+ def _get_default_tools(self) -> list[str]:
42
+ """Get default set of quality tools."""
43
+ return ["coverage", "security", "complexity"]
44
+
45
+ def _initialize_tools(self) -> None:
46
+ """Initialize quality tool instances based on configuration."""
47
+ for tool_name in self.tools:
48
+ try:
49
+ self.tool_instances[tool_name] = self._create_tool(tool_name)
50
+ except (ImportError, Exception) as e:
51
+ # Tool dependencies not available - skip gracefully
52
+ print(f"Warning: {tool_name} tool not available: {e}")
53
+ continue
54
+
55
+ def _create_tool(self, tool_name: str) -> QualityTool:
56
+ """Create a tool instance by name."""
57
+ if tool_name == "coverage":
58
+ from .coverage import CoverageTracker
59
+
60
+ return CoverageTracker(self.config.get("coverage", {}))
61
+ elif tool_name == "security":
62
+ from .security import SecurityScanner
63
+
64
+ return SecurityScanner(self.config.get("security", {}))
65
+ elif tool_name == "complexity":
66
+ from .complexity import ComplexityAnalyzer
67
+
68
+ return ComplexityAnalyzer(self.config.get("complexity", {}))
69
+ elif tool_name == "profiling":
70
+ from .profiling import PerformanceProfiler
71
+
72
+ return PerformanceProfiler(self.config.get("profiling", {}))
73
+ elif tool_name == "documentation":
74
+ from .documentation import DocumentationChecker
75
+
76
+ return DocumentationChecker(self.config.get("documentation", {}))
77
+ else:
78
+ raise QualityError(f"Unknown tool: {tool_name}")
79
+
80
+ def run_all(self, target: Path, **kwargs: Any) -> dict[str, QualityResult]:
81
+ """Run all configured quality tools on the target.
82
+
83
+ Args:
84
+ target: Path to analyze
85
+ **kwargs: Additional arguments passed to tools
86
+
87
+ Returns:
88
+ Dictionary mapping tool names to their results
89
+ """
90
+ results = {}
91
+ target = Path(target)
92
+
93
+ for tool_name, tool in self.tool_instances.items():
94
+ artifact_dir = self.artifact_root / tool_name
95
+ ensure_dir(artifact_dir)
96
+
97
+ try:
98
+ start_time = time.time()
99
+ result = tool.analyze(target, artifact_dir=artifact_dir, **kwargs)
100
+ result.execution_time = time.time() - start_time
101
+
102
+ # Save artifacts
103
+ self._save_tool_artifacts(result, artifact_dir)
104
+ results[tool_name] = result
105
+
106
+ except Exception as e:
107
+ # Create failed result for tool
108
+ results[tool_name] = QualityResult(
109
+ tool=tool_name, passed=False, details={"error": str(e), "error_type": type(e).__name__}
110
+ )
111
+
112
+ return results
113
+
114
+ def run_with_gates(
115
+ self, target: Path, gates: dict[str, Any], **kwargs: Any
116
+ ) -> tuple[bool, dict[str, QualityResult]]:
117
+ """Run quality tools and check against quality gates.
118
+
119
+ Args:
120
+ target: Path to analyze
121
+ gates: Quality gate requirements
122
+ **kwargs: Additional arguments passed to tools
123
+
124
+ Returns:
125
+ Tuple of (all_gates_passed, results)
126
+ """
127
+ results = self.run_all(target, **kwargs)
128
+ passed = self._check_gates(results, gates)
129
+ return passed, results
130
+
131
+ def _check_gates(self, results: dict[str, QualityResult], gates: dict[str, Any]) -> bool:
132
+ """Check if results meet quality gate requirements.
133
+
134
+ Args:
135
+ results: Tool results to check
136
+ gates: Gate requirements
137
+
138
+ Returns:
139
+ True if all gates pass
140
+ """
141
+ for gate_name, requirement in gates.items():
142
+ if gate_name not in results:
143
+ # Tool didn't run - gate fails
144
+ return False
145
+
146
+ result = results[gate_name]
147
+ if not self._check_single_gate(result, requirement):
148
+ return False
149
+
150
+ return True
151
+
152
+ def _check_single_gate(self, result: QualityResult, requirement: Any) -> bool:
153
+ """Check a single gate requirement against a result.
154
+
155
+ Args:
156
+ result: Result to check
157
+ requirement: Gate requirement to validate against
158
+
159
+ Returns:
160
+ True if gate requirement is met
161
+ """
162
+ if isinstance(requirement, dict):
163
+ return self._check_dict_requirement(result, requirement)
164
+ elif isinstance(requirement, bool):
165
+ return self._check_bool_requirement(result, requirement)
166
+ elif isinstance(requirement, (int, float)):
167
+ return self._check_score_requirement(result, requirement)
168
+ elif isinstance(requirement, str):
169
+ return self._check_grade_requirement(result, requirement)
170
+ else:
171
+ return True
172
+
173
+ def _check_dict_requirement(self, result: QualityResult, requirement: dict[str, Any]) -> bool:
174
+ """Check dictionary-based gate requirements."""
175
+ if not requirement.get("enabled", True):
176
+ return True
177
+
178
+ if not result.passed:
179
+ return False
180
+
181
+ if "min_score" in requirement and result.score is not None:
182
+ return result.score >= requirement["min_score"]
183
+
184
+ return True
185
+
186
+ def _check_bool_requirement(self, result: QualityResult, requirement: bool) -> bool:
187
+ """Check boolean gate requirements."""
188
+ return not requirement or result.passed
189
+
190
+ def _check_score_requirement(self, result: QualityResult, requirement: float) -> bool:
191
+ """Check numeric score requirements."""
192
+ return result.score is not None and result.score >= requirement
193
+
194
+ def _check_grade_requirement(self, result: QualityResult, requirement: str) -> bool:
195
+ """Check grade-based requirements (A, B, C, etc.).
196
+
197
+ Args:
198
+ result: Result to check
199
+ requirement: Required grade
200
+
201
+ Returns:
202
+ True if requirement is met
203
+ """
204
+ if "grade" not in result.details:
205
+ return False
206
+
207
+ grade_order = {"A": 5, "B": 4, "C": 3, "D": 2, "E": 1, "F": 0}
208
+ actual_grade = result.details["grade"]
209
+ required_grade = requirement
210
+
211
+ return grade_order.get(actual_grade, 0) >= grade_order.get(required_grade, 0)
212
+
213
+ def _save_tool_artifacts(self, result: QualityResult, artifact_dir: Path) -> None:
214
+ """Save tool artifacts and update result.
215
+
216
+ Args:
217
+ result: Result to save artifacts for
218
+ artifact_dir: Directory to save artifacts in
219
+ """
220
+ # Save result summary
221
+ summary_file = artifact_dir / "summary.txt"
222
+ atomic_write_text(summary_file, result.summary)
223
+ result.artifacts.append(summary_file)
224
+
225
+ # Save detailed results if available
226
+ if result.details:
227
+ import json
228
+
229
+ details_file = artifact_dir / "details.json"
230
+ atomic_write_text(details_file, json.dumps(result.details, indent=2, default=str))
231
+ result.artifacts.append(details_file)
232
+
233
+ def get_available_tools(self) -> list[str]:
234
+ """Get list of available tool names."""
235
+ return list(self.tool_instances.keys())
236
+
237
+ def generate_summary_report(self, results: dict[str, QualityResult]) -> str:
238
+ """Generate a summary report of all results.
239
+
240
+ Args:
241
+ results: Results to summarize
242
+
243
+ Returns:
244
+ Summary report string
245
+ """
246
+ lines = ["Quality Analysis Summary", "=" * 30, ""]
247
+
248
+ total_tools = len(results)
249
+ passed_tools = sum(1 for r in results.values() if r.passed)
250
+
251
+ lines.append(f"Tools Run: {total_tools}")
252
+ lines.append(f"Passed: {passed_tools}")
253
+ lines.append(f"Failed: {total_tools - passed_tools}")
254
+ lines.append("")
255
+
256
+ for tool_name, result in results.items():
257
+ lines.append(result.summary)
258
+
259
+ return "\n".join(lines)
260
+
261
+ def run_tools(
262
+ self,
263
+ target: Path,
264
+ tools: list[str] | None = None,
265
+ artifact_dir: Path | None = None,
266
+ tool_configs: dict[str, Any] | None = None,
267
+ ) -> dict[str, QualityResult]:
268
+ """Run specific quality tools on the target.
269
+
270
+ Args:
271
+ target: Path to analyze
272
+ tools: List of tool names to run (None for all available)
273
+ artifact_dir: Directory for artifacts (overrides default)
274
+ tool_configs: Configuration for tools
275
+
276
+ Returns:
277
+ Dictionary mapping tool names to their results
278
+ """
279
+ if artifact_dir:
280
+ original_artifact_root = self.artifact_root
281
+ self.artifact_root = artifact_dir
282
+
283
+ if tool_configs:
284
+ original_config = self.config
285
+ self.config = tool_configs
286
+ # Re-initialize tools with new config
287
+ self._initialize_tools()
288
+
289
+ # Filter tools if specified
290
+ if tools:
291
+ filtered_instances = {name: tool for name, tool in self.tool_instances.items() if name in tools}
292
+ original_instances = self.tool_instances
293
+ self.tool_instances = filtered_instances
294
+
295
+ try:
296
+ results = self.run_all(target)
297
+ return results
298
+ finally:
299
+ # Restore original state
300
+ if artifact_dir:
301
+ self.artifact_root = original_artifact_root
302
+ if tool_configs:
303
+ self.config = original_config
304
+ self._initialize_tools()
305
+ if tools:
306
+ self.tool_instances = original_instances
307
+
308
+
309
+ @dataclass
310
+ class QualityGateResults:
311
+ """Results from running quality gates."""
312
+
313
+ passed: bool
314
+ results: dict[str, QualityResult]
315
+ failed_gates: list[str] = None
316
+
317
+ def __post_init__(self):
318
+ if self.failed_gates is None:
319
+ self.failed_gates = []
@@ -0,0 +1,29 @@
1
+ """Security analysis integration for provide-testkit.
2
+
3
+ Provides security vulnerability scanning using bandit and other security tools.
4
+ Integrates with the quality framework for comprehensive security analysis.
5
+
6
+ Features:
7
+ - Vulnerability scanning with bandit
8
+ - Security issue reporting and classification
9
+ - Integration with quality gates
10
+ - Artifact management for CI/CD
11
+
12
+ Usage:
13
+ # Basic security scanning
14
+ def test_with_security(security_scanner):
15
+ result = security_scanner.scan(path)
16
+ assert result.passed
17
+
18
+ # Security with quality gates
19
+ runner = QualityRunner()
20
+ results = runner.run_with_gates(path, {"security": True})
21
+ """
22
+
23
+ from .fixture import SecurityFixture
24
+ from .scanner import SecurityScanner
25
+
26
+ __all__ = [
27
+ "SecurityFixture",
28
+ "SecurityScanner",
29
+ ]
@@ -0,0 +1,196 @@
1
+ """Pytest fixtures for security scanning."""
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 .scanner import BANDIT_AVAILABLE, SecurityScanner
13
+
14
+
15
+ class SecurityFixture(BaseQualityFixture):
16
+ """Pytest fixture for security scanning integration."""
17
+
18
+ def __init__(self, config: dict[str, Any] | None = None, artifact_dir: Path | None = None):
19
+ """Initialize security fixture.
20
+
21
+ Args:
22
+ config: Security scanner configuration
23
+ artifact_dir: Directory for artifacts
24
+ """
25
+ super().__init__(config, artifact_dir)
26
+ self.scanner: SecurityScanner | None = None
27
+
28
+ def setup(self) -> None:
29
+ """Setup security scanning."""
30
+ if not BANDIT_AVAILABLE:
31
+ pytest.skip("Bandit not available")
32
+
33
+ try:
34
+ self.scanner = SecurityScanner(self.config)
35
+ except Exception as e:
36
+ pytest.skip(f"Failed to initialize security scanner: {e}")
37
+
38
+ def teardown(self) -> None:
39
+ """Cleanup security scanner."""
40
+ # No cleanup needed for security scanner
41
+ pass
42
+
43
+ def scan(self, path: Path) -> dict[str, Any]:
44
+ """Perform security scan.
45
+
46
+ Args:
47
+ path: Path to scan
48
+
49
+ Returns:
50
+ Security scan results
51
+ """
52
+ self.ensure_setup()
53
+ if not self.scanner:
54
+ return {"error": "Scanner not available"}
55
+
56
+ result = self.scanner.analyze(path, artifact_dir=self.artifact_dir)
57
+ self.add_result(result)
58
+ return {
59
+ "passed": result.passed,
60
+ "score": result.score,
61
+ "issues": result.details.get("total_issues", 0),
62
+ "details": result.details,
63
+ }
64
+
65
+ def generate_report(self, format: str = "terminal") -> str:
66
+ """Generate security report.
67
+
68
+ Args:
69
+ format: Report format (terminal, json)
70
+
71
+ Returns:
72
+ Formatted report
73
+ """
74
+ if not self.scanner:
75
+ return "No security scanner available"
76
+
77
+ results = self.get_results_by_tool()
78
+ if "security" not in results:
79
+ return "No security results available"
80
+
81
+ return self.scanner.report(results["security"], format)
82
+
83
+
84
+ @pytest.fixture
85
+ def security_scanner(request, tmp_path) -> Generator[SecurityFixture, None, None]:
86
+ """Pytest fixture for security scanning.
87
+
88
+ Provides a SecurityFixture instance for security vulnerability scanning.
89
+
90
+ Usage:
91
+ def test_security_scan(security_scanner):
92
+ result = security_scanner.scan(Path('./src'))
93
+ assert result['passed']
94
+ assert result['issues'] == 0
95
+ """
96
+ # Get configuration from pytest request
97
+ config = getattr(request, "param", {})
98
+
99
+ # Create artifact directory for this test
100
+ artifact_dir = tmp_path / "security"
101
+
102
+ # Initialize fixture
103
+ fixture = SecurityFixture(config=config, artifact_dir=artifact_dir)
104
+
105
+ try:
106
+ fixture.setup()
107
+ yield fixture
108
+ finally:
109
+ fixture.teardown()
110
+
111
+
112
+ @pytest.fixture
113
+ def security_config():
114
+ """Default security configuration fixture.
115
+
116
+ Returns standard security configuration that can be customized
117
+ per test or project.
118
+
119
+ Usage:
120
+ def test_custom_security(security_config):
121
+ security_config["max_high_severity"] = 0
122
+ security_config["min_score"] = 95.0
123
+ # Use with parametrized security_scanner
124
+ """
125
+ return {
126
+ "confidence": "medium",
127
+ "severity": "medium",
128
+ "max_high_severity": 0,
129
+ "max_medium_severity": 5,
130
+ "min_score": 80.0,
131
+ "exclude": ["*/tests/*", "*/test_*", "*/.venv/*", "*/venv/*", "*/__pycache__/*", "*/migrations/*"],
132
+ }
133
+
134
+
135
+ # Parametrized fixtures for different security configurations
136
+ @pytest.fixture(
137
+ params=[
138
+ {"max_high_severity": 0, "max_medium_severity": 0, "min_score": 100.0}, # Strict
139
+ {"max_high_severity": 1, "max_medium_severity": 5, "min_score": 80.0}, # Normal
140
+ {"max_high_severity": 5, "max_medium_severity": 10, "min_score": 60.0}, # Lenient
141
+ ]
142
+ )
143
+ def parametrized_security(request, tmp_path) -> Generator[SecurityFixture, None, None]:
144
+ """Parametrized security fixture for testing different configurations.
145
+
146
+ Automatically runs tests with different security thresholds
147
+ to validate behavior under various settings.
148
+
149
+ Usage:
150
+ def test_security_configs(parametrized_security):
151
+ # Test runs multiple times with different configs
152
+ result = parametrized_security.scan(Path('./src'))
153
+ # Behavior will vary based on configuration
154
+ """
155
+ config = request.param
156
+ artifact_dir = tmp_path / f"security_{id(config)}"
157
+
158
+ fixture = SecurityFixture(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 security integration
168
+ def pytest_configure(config):
169
+ """Configure pytest with security markers."""
170
+ config.addinivalue_line("markers", "security: mark test to run with security scanning")
171
+ config.addinivalue_line("markers", "no_security: mark test to skip security scanning")
172
+
173
+
174
+ @pytest.fixture(autouse=True)
175
+ def auto_security_marker(request):
176
+ """Automatically apply security scanning to marked tests.
177
+
178
+ Tests marked with @pytest.mark.security will automatically
179
+ get security scanning without needing to explicitly use fixtures.
180
+ """
181
+ if request.node.get_closest_marker("security"):
182
+ # Test is marked for security - enable automatic scanning
183
+ if not request.node.get_closest_marker("no_security"):
184
+ # Create temporary security fixture
185
+ security_fixture = SecurityFixture()
186
+ try:
187
+ security_fixture.setup()
188
+ # Security scan would be applied here in a real implementation
189
+ # For now, we just yield to continue the test
190
+ yield
191
+ finally:
192
+ security_fixture.teardown()
193
+ else:
194
+ yield
195
+ else:
196
+ yield