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,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
|