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,338 @@
1
+ """Security vulnerability scanner implementation."""
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 bandit # type: ignore[import-untyped]
14
+ from bandit.core import (
15
+ config as bandit_config, # type: ignore[import-untyped]
16
+ manager as bandit_manager,
17
+ )
18
+
19
+ BANDIT_AVAILABLE = True
20
+ except ImportError:
21
+ BANDIT_AVAILABLE = False
22
+ bandit = None
23
+ bandit_config = None
24
+ bandit_manager = None
25
+
26
+ from ..base import QualityResult, QualityToolError
27
+
28
+
29
+ class SecurityScanner:
30
+ """Security vulnerability scanner using bandit and other tools.
31
+
32
+ Provides high-level interface for security analysis with automatic
33
+ artifact management and integration with the quality framework.
34
+ """
35
+
36
+ def __init__(self, config: dict[str, Any] | None = None):
37
+ """Initialize security scanner.
38
+
39
+ Args:
40
+ config: Security scanner configuration options
41
+ """
42
+ if not BANDIT_AVAILABLE:
43
+ raise QualityToolError("Bandit not available. Install with: pip install bandit", tool="security")
44
+
45
+ self.config = config or {}
46
+ self.artifact_dir: Path | None = None
47
+
48
+ def analyze(self, path: Path, **kwargs: Any) -> QualityResult:
49
+ """Run security analysis on the given path.
50
+
51
+ Args:
52
+ path: Path to analyze
53
+ **kwargs: Additional options including artifact_dir
54
+
55
+ Returns:
56
+ QualityResult with security analysis data
57
+ """
58
+ self.artifact_dir = kwargs.get("artifact_dir", Path(".security"))
59
+ start_time = time.time()
60
+
61
+ try:
62
+ # Run bandit security scan
63
+ result = self._run_bandit_scan(path)
64
+ result.execution_time = time.time() - start_time
65
+
66
+ # Generate artifacts
67
+ self._generate_artifacts(result)
68
+
69
+ return result
70
+
71
+ except Exception as e:
72
+ return QualityResult(
73
+ tool="security",
74
+ passed=False,
75
+ details={"error": str(e), "error_type": type(e).__name__},
76
+ execution_time=time.time() - start_time,
77
+ )
78
+
79
+ def _run_bandit_scan(self, path: Path) -> QualityResult:
80
+ """Run bandit security scan."""
81
+ if not BANDIT_AVAILABLE:
82
+ raise QualityToolError("Bandit not available", tool="security")
83
+
84
+ try:
85
+ # Create bandit configuration
86
+ conf = bandit_config.BanditConfig()
87
+
88
+ # Apply custom configuration
89
+ self._apply_bandit_config(conf)
90
+
91
+ # Create bandit manager
92
+ b_mgr = bandit_manager.BanditManager(conf, "file")
93
+
94
+ # Discover files to scan
95
+ if path.is_file():
96
+ files_list = [str(path)]
97
+ else:
98
+ files_list = self._discover_python_files(path)
99
+
100
+ if not files_list:
101
+ return QualityResult(
102
+ tool="security",
103
+ passed=True,
104
+ score=100.0,
105
+ details={"message": "No Python files found to scan"},
106
+ )
107
+
108
+ # Run the scan
109
+ b_mgr.discover_files(files_list)
110
+ b_mgr.run_tests()
111
+
112
+ # Process results
113
+ return self._process_bandit_results(b_mgr)
114
+
115
+ except Exception as e:
116
+ raise QualityToolError(f"Bandit scan failed: {e!s}", tool="security")
117
+
118
+ def _apply_bandit_config(self, conf: Any) -> None:
119
+ """Apply custom configuration to bandit."""
120
+ # Bandit configuration is set differently than expected
121
+ # For now, we'll use the defaults and apply filtering later
122
+ pass
123
+
124
+ def _discover_python_files(self, path: Path) -> list[str]:
125
+ """Discover Python files to scan."""
126
+ excludes = self.config.get(
127
+ "exclude", ["*/tests/*", "*/test_*", "*/.venv/*", "*/venv/*", "*/__pycache__/*"]
128
+ )
129
+
130
+ files = []
131
+ for py_file in path.rglob("*.py"):
132
+ # Check if file should be excluded
133
+ if any(py_file.match(pattern) for pattern in excludes):
134
+ continue
135
+ files.append(str(py_file))
136
+
137
+ return files
138
+
139
+ def _process_bandit_results(self, manager: Any) -> QualityResult:
140
+ """Process bandit scan results into QualityResult."""
141
+ # Get issues
142
+ issues = manager.get_issue_list()
143
+
144
+ # Calculate metrics
145
+ total_files = len(manager.files_list)
146
+ issue_count = len(issues)
147
+
148
+ # Categorize by severity
149
+ severity_counts = {"HIGH": 0, "MEDIUM": 0, "LOW": 0}
150
+ confidence_counts = {"HIGH": 0, "MEDIUM": 0, "LOW": 0}
151
+
152
+ for issue in issues:
153
+ severity_counts[issue.severity] += 1
154
+ confidence_counts[issue.confidence] += 1
155
+
156
+ # Calculate security score (0-100)
157
+ # Start at 100, deduct points based on severity
158
+ score = 100.0
159
+ score -= severity_counts["HIGH"] * 10 # High severity: -10 points each
160
+ score -= severity_counts["MEDIUM"] * 5 # Medium severity: -5 points each
161
+ score -= severity_counts["LOW"] * 1 # Low severity: -1 point each
162
+ score = max(0.0, score) # Don't go below 0
163
+
164
+ # Determine if passed based on configuration
165
+ max_high = self.config.get("max_high_severity", 0)
166
+ max_medium = self.config.get("max_medium_severity", 5)
167
+ min_score = self.config.get("min_score", 80.0)
168
+
169
+ passed = (
170
+ severity_counts["HIGH"] <= max_high
171
+ and severity_counts["MEDIUM"] <= max_medium
172
+ and score >= min_score
173
+ )
174
+
175
+ # Create detailed results
176
+ details = {
177
+ "total_files": total_files,
178
+ "total_issues": issue_count,
179
+ "severity_breakdown": severity_counts,
180
+ "confidence_breakdown": confidence_counts,
181
+ "score": score,
182
+ "thresholds": {
183
+ "max_high_severity": max_high,
184
+ "max_medium_severity": max_medium,
185
+ "min_score": min_score,
186
+ },
187
+ }
188
+
189
+ # Add issue details for reporting
190
+ if issues:
191
+ details["issues"] = [
192
+ {
193
+ "filename": issue.fname,
194
+ "line_number": issue.lineno,
195
+ "test_id": issue.test_id,
196
+ "test_name": issue.test,
197
+ "severity": issue.severity,
198
+ "confidence": issue.confidence,
199
+ "text": issue.text.strip(),
200
+ "code": issue.get_code(max_lines=3, tabbed=False),
201
+ }
202
+ for issue in issues[:20] # Limit to first 20 for readability
203
+ ]
204
+
205
+ return QualityResult(tool="security", passed=passed, score=score, details=details)
206
+
207
+ def _generate_artifacts(self, result: QualityResult) -> None:
208
+ """Generate security analysis artifacts.
209
+
210
+ Args:
211
+ result: Result to add artifacts to
212
+ """
213
+ if not self.artifact_dir:
214
+ return
215
+
216
+ ensure_dir(self.artifact_dir)
217
+
218
+ try:
219
+ # Generate JSON report
220
+ json_file = self.artifact_dir / "security.json"
221
+ json_data = {
222
+ "tool": result.tool,
223
+ "passed": result.passed,
224
+ "score": result.score,
225
+ "details": result.details,
226
+ "execution_time": result.execution_time,
227
+ }
228
+ atomic_write_text(json_file, json.dumps(json_data, indent=2))
229
+ result.artifacts.append(json_file)
230
+
231
+ # Generate text summary
232
+ summary_file = self.artifact_dir / "security_summary.txt"
233
+ summary_report = self._generate_text_report(result)
234
+ atomic_write_text(summary_file, summary_report)
235
+ result.artifacts.append(summary_file)
236
+
237
+ # Generate detailed issues report if there are issues
238
+ if result.details.get("issues"):
239
+ issues_file = self.artifact_dir / "security_issues.txt"
240
+ issues_report = self._generate_issues_report(result)
241
+ atomic_write_text(issues_file, issues_report)
242
+ result.artifacts.append(issues_file)
243
+
244
+ except Exception as e:
245
+ # Add error to result details but don't fail
246
+ result.details["artifact_error"] = str(e)
247
+
248
+ def _generate_text_report(self, result: QualityResult) -> str:
249
+ """Generate text summary report."""
250
+ lines = [
251
+ f"Security Analysis Report - {result.tool}",
252
+ "=" * 50,
253
+ f"Status: {'✅ PASSED' if result.passed else '❌ FAILED'}",
254
+ f"Security Score: {result.score}%",
255
+ ]
256
+
257
+ details = result.details
258
+ if "total_files" in details:
259
+ lines.extend(
260
+ [
261
+ f"Files Scanned: {details['total_files']}",
262
+ f"Total Issues: {details['total_issues']}",
263
+ "",
264
+ "Severity Breakdown:",
265
+ ]
266
+ )
267
+
268
+ severity = details.get("severity_breakdown", {})
269
+ for level, count in severity.items():
270
+ lines.append(f" {level}: {count}")
271
+
272
+ lines.extend(
273
+ [
274
+ "",
275
+ "Confidence Breakdown:",
276
+ ]
277
+ )
278
+
279
+ confidence = details.get("confidence_breakdown", {})
280
+ for level, count in confidence.items():
281
+ lines.append(f" {level}: {count}")
282
+
283
+ if result.execution_time:
284
+ lines.append(f"\nExecution Time: {result.execution_time:.2f}s")
285
+
286
+ return "\n".join(lines)
287
+
288
+ def _generate_issues_report(self, result: QualityResult) -> str:
289
+ """Generate detailed issues report."""
290
+ lines = ["Security Issues Report", "=" * 50, ""]
291
+
292
+ issues = result.details.get("issues", [])
293
+ for i, issue in enumerate(issues, 1):
294
+ lines.extend(
295
+ [
296
+ f"Issue #{i}:",
297
+ f" File: {issue['filename']}:{issue['line_number']}",
298
+ f" Test: {issue['test_name']} ({issue['test_id']})",
299
+ f" Severity: {issue['severity']} | Confidence: {issue['confidence']}",
300
+ f" Description: {issue['text']}",
301
+ "",
302
+ " Code:",
303
+ ]
304
+ )
305
+
306
+ # Add code snippet with indentation
307
+ for code_line in issue["code"].split("\n"):
308
+ if code_line.strip():
309
+ lines.append(f" {code_line}")
310
+
311
+ lines.append("")
312
+
313
+ return "\n".join(lines)
314
+
315
+ def report(self, result: QualityResult, format: str = "terminal") -> str:
316
+ """Generate report from QualityResult (implements QualityTool protocol).
317
+
318
+ Args:
319
+ result: Security result
320
+ format: Report format
321
+
322
+ Returns:
323
+ Formatted report
324
+ """
325
+ if format == "terminal":
326
+ return self._generate_text_report(result)
327
+ elif format == "json":
328
+ return json.dumps(
329
+ {
330
+ "tool": result.tool,
331
+ "passed": result.passed,
332
+ "score": result.score,
333
+ "details": result.details,
334
+ },
335
+ indent=2,
336
+ )
337
+ else:
338
+ return str(result.details)
@@ -0,0 +1,54 @@
1
+ #
2
+ # streams.py
3
+ #
4
+ """
5
+ Stream Testing Utilities for Foundation.
6
+
7
+ Provides utilities for redirecting and managing streams during testing,
8
+ allowing tests to capture and control Foundation's output streams.
9
+ """
10
+
11
+ from typing import TextIO
12
+
13
+ # Import the actual stream management variables
14
+ from provide.foundation.streams.core import get_log_stream
15
+
16
+
17
+ def set_log_stream_for_testing(stream: TextIO | None) -> None:
18
+ """
19
+ Set the log stream for testing purposes.
20
+
21
+ This allows tests to redirect Foundation's log output to a custom stream
22
+ (like StringIO) for capturing and verifying log messages.
23
+
24
+ Args:
25
+ stream: Stream to redirect to, or None to reset to stderr
26
+ """
27
+ # Import the actual implementation from streams.core
28
+ from provide.foundation.streams.core import (
29
+ set_log_stream_for_testing as _set_stream,
30
+ )
31
+
32
+ _set_stream(stream)
33
+
34
+
35
+ def get_current_log_stream() -> TextIO:
36
+ """
37
+ Get the currently active log stream.
38
+
39
+ Returns:
40
+ The current log stream being used by Foundation
41
+ """
42
+ return get_log_stream()
43
+
44
+
45
+ def reset_log_stream() -> None:
46
+ """Reset log stream back to stderr."""
47
+ set_log_stream_for_testing(None)
48
+
49
+
50
+ __all__ = [
51
+ "get_current_log_stream",
52
+ "reset_log_stream",
53
+ "set_log_stream_for_testing",
54
+ ]
@@ -0,0 +1,38 @@
1
+ """
2
+ Threading testing utilities for the provide-io ecosystem.
3
+
4
+ Fixtures and utilities for testing multi-threaded code, thread synchronization,
5
+ and concurrent operations across any project that depends on provide.foundation.
6
+ """
7
+
8
+ from provide.testkit.threading.fixtures import (
9
+ concurrent_executor,
10
+ deadlock_detector,
11
+ mock_thread,
12
+ test_thread,
13
+ thread_barrier,
14
+ thread_condition,
15
+ thread_event,
16
+ thread_exception_handler,
17
+ thread_local_storage,
18
+ thread_pool,
19
+ thread_safe_counter,
20
+ thread_safe_list,
21
+ thread_synchronizer,
22
+ )
23
+
24
+ __all__ = [
25
+ "concurrent_executor",
26
+ "deadlock_detector",
27
+ "mock_thread",
28
+ "test_thread",
29
+ "thread_barrier",
30
+ "thread_condition",
31
+ "thread_event",
32
+ "thread_exception_handler",
33
+ "thread_local_storage",
34
+ "thread_pool",
35
+ "thread_safe_counter",
36
+ "thread_safe_list",
37
+ "thread_synchronizer",
38
+ ]
@@ -0,0 +1,103 @@
1
+ """
2
+ Basic threading test fixtures.
3
+
4
+ Core fixtures for creating threads, thread pools, mocks, and thread-local storage.
5
+ """
6
+
7
+ from collections.abc import Callable
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ import threading
10
+ from unittest.mock import Mock
11
+
12
+ import pytest
13
+
14
+
15
+ @pytest.fixture
16
+ def test_thread():
17
+ """
18
+ Create a test thread with automatic cleanup.
19
+
20
+ Returns:
21
+ Function to create and manage test threads.
22
+ """
23
+ threads = []
24
+
25
+ def _create_thread(
26
+ target: Callable, args: tuple = (), kwargs: dict = None, daemon: bool = True
27
+ ) -> threading.Thread:
28
+ """
29
+ Create a test thread.
30
+
31
+ Args:
32
+ target: Function to run in thread
33
+ args: Positional arguments for target
34
+ kwargs: Keyword arguments for target
35
+ daemon: Whether thread should be daemon
36
+
37
+ Returns:
38
+ Started thread instance
39
+ """
40
+ kwargs = kwargs or {}
41
+ thread = threading.Thread(target=target, args=args, kwargs=kwargs, daemon=daemon)
42
+ threads.append(thread)
43
+ thread.start()
44
+ return thread
45
+
46
+ yield _create_thread
47
+
48
+ # Cleanup: wait for all threads to complete
49
+ for thread in threads:
50
+ if thread.is_alive():
51
+ thread.join(timeout=1.0)
52
+
53
+
54
+ @pytest.fixture
55
+ def thread_pool():
56
+ """
57
+ Create a thread pool executor for testing.
58
+
59
+ Returns:
60
+ ThreadPoolExecutor instance with automatic cleanup.
61
+ """
62
+ executor = ThreadPoolExecutor(max_workers=4)
63
+ yield executor
64
+ executor.shutdown(wait=True, cancel_futures=True)
65
+
66
+
67
+ @pytest.fixture
68
+ def mock_thread():
69
+ """
70
+ Create a mock thread for testing without actual threading.
71
+
72
+ Returns:
73
+ Mock thread object.
74
+ """
75
+ mock = Mock(spec=threading.Thread)
76
+ mock.is_alive.return_value = False
77
+ mock.daemon = False
78
+ mock.name = "MockThread"
79
+ mock.ident = 12345
80
+ mock.start = Mock()
81
+ mock.join = Mock()
82
+ mock.run = Mock()
83
+
84
+ return mock
85
+
86
+
87
+ @pytest.fixture
88
+ def thread_local_storage():
89
+ """
90
+ Create thread-local storage for testing.
91
+
92
+ Returns:
93
+ Thread-local storage object.
94
+ """
95
+ return threading.local()
96
+
97
+
98
+ __all__ = [
99
+ "mock_thread",
100
+ "test_thread",
101
+ "thread_local_storage",
102
+ "thread_pool",
103
+ ]
@@ -0,0 +1,101 @@
1
+ """
2
+ Thread-safe data structure test fixtures.
3
+
4
+ Fixtures for thread-safe lists, counters, and other data structures for testing.
5
+ """
6
+
7
+ import threading
8
+ from typing import Any
9
+
10
+ import pytest
11
+
12
+
13
+ @pytest.fixture
14
+ def thread_safe_list():
15
+ """
16
+ Create a thread-safe list for collecting results.
17
+
18
+ Returns:
19
+ Thread-safe list implementation.
20
+ """
21
+
22
+ class ThreadSafeList:
23
+ def __init__(self):
24
+ self._list = []
25
+ self._lock = threading.Lock()
26
+
27
+ def append(self, item: Any):
28
+ """Thread-safe append."""
29
+ with self._lock:
30
+ self._list.append(item)
31
+
32
+ def extend(self, items):
33
+ """Thread-safe extend."""
34
+ with self._lock:
35
+ self._list.extend(items)
36
+
37
+ def get_all(self) -> list:
38
+ """Get copy of all items."""
39
+ with self._lock:
40
+ return self._list.copy()
41
+
42
+ def clear(self):
43
+ """Clear the list."""
44
+ with self._lock:
45
+ self._list.clear()
46
+
47
+ def __len__(self) -> int:
48
+ with self._lock:
49
+ return len(self._list)
50
+
51
+ def __getitem__(self, index):
52
+ with self._lock:
53
+ return self._list[index]
54
+
55
+ return ThreadSafeList()
56
+
57
+
58
+ @pytest.fixture
59
+ def thread_safe_counter():
60
+ """
61
+ Create a thread-safe counter.
62
+
63
+ Returns:
64
+ Thread-safe counter implementation.
65
+ """
66
+
67
+ class ThreadSafeCounter:
68
+ def __init__(self, initial: int = 0):
69
+ self._value = initial
70
+ self._lock = threading.Lock()
71
+
72
+ def increment(self, amount: int = 1) -> int:
73
+ """Thread-safe increment."""
74
+ with self._lock:
75
+ self._value += amount
76
+ return self._value
77
+
78
+ def decrement(self, amount: int = 1) -> int:
79
+ """Thread-safe decrement."""
80
+ with self._lock:
81
+ self._value -= amount
82
+ return self._value
83
+
84
+ @property
85
+ def value(self) -> int:
86
+ """Get current value."""
87
+ with self._lock:
88
+ return self._value
89
+
90
+ def reset(self, value: int = 0):
91
+ """Reset counter."""
92
+ with self._lock:
93
+ self._value = value
94
+
95
+ return ThreadSafeCounter()
96
+
97
+
98
+ __all__ = [
99
+ "thread_safe_counter",
100
+ "thread_safe_list",
101
+ ]