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,394 @@
1
+ """CLI commands for quality analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ import sys
8
+ from typing import Any
9
+
10
+ import click
11
+
12
+ from .runner import QualityRunner
13
+
14
+
15
+ @click.group(name="quality")
16
+ @click.pass_context
17
+ def quality_cli(ctx: click.Context) -> None:
18
+ """Quality analysis commands for provide-testkit."""
19
+ ctx.ensure_object(dict)
20
+
21
+
22
+ @quality_cli.command("analyze")
23
+ @click.argument("path", type=click.Path(exists=True, path_type=Path))
24
+ @click.option(
25
+ "--tool",
26
+ multiple=True,
27
+ type=click.Choice(["coverage", "security", "complexity", "documentation", "profiling"]),
28
+ help="Specific tools to run (default: all available)",
29
+ )
30
+ @click.option(
31
+ "--artifact-dir",
32
+ type=click.Path(path_type=Path),
33
+ default=".quality-artifacts",
34
+ help="Directory for output artifacts",
35
+ )
36
+ @click.option(
37
+ "--format", type=click.Choice(["terminal", "json", "summary"]), default="terminal", help="Output format"
38
+ )
39
+ @click.option("--fail-fast", is_flag=True, help="Stop on first failure")
40
+ @click.option("--config", type=click.Path(exists=True, path_type=Path), help="Configuration file (JSON)")
41
+ @click.option("--verbose", "-v", is_flag=True, help="Verbose output")
42
+ def analyze_command(
43
+ path: Path,
44
+ tool: tuple[str, ...],
45
+ artifact_dir: Path,
46
+ format: str,
47
+ fail_fast: bool,
48
+ config: Path | None,
49
+ verbose: bool,
50
+ ) -> None:
51
+ """Analyze code quality for the given path."""
52
+ try:
53
+ # Load configuration
54
+ config_data = {}
55
+ if config:
56
+ config_data = json.loads(config.read_text())
57
+
58
+ # Determine which tools to run
59
+ if tool:
60
+ tools_to_run = list(tool)
61
+ else:
62
+ tools_to_run = ["coverage", "security", "complexity", "documentation"]
63
+
64
+ if verbose:
65
+ click.echo(f"Analyzing {path} with tools: {', '.join(tools_to_run)}")
66
+ click.echo(f"Artifacts will be saved to: {artifact_dir}")
67
+
68
+ # Run quality analysis
69
+ runner = QualityRunner()
70
+ results = runner.run_tools(path, tools_to_run, artifact_dir=artifact_dir, tool_configs=config_data)
71
+
72
+ # Output results
73
+ if format == "json":
74
+ output = {
75
+ "path": str(path),
76
+ "tools": tools_to_run,
77
+ "results": {
78
+ tool: {
79
+ "passed": result.passed,
80
+ "score": result.score,
81
+ "details": result.details,
82
+ "execution_time": result.execution_time,
83
+ }
84
+ for tool, result in results.items()
85
+ },
86
+ }
87
+ click.echo(json.dumps(output, indent=2))
88
+ elif format == "summary":
89
+ _print_summary(results, verbose)
90
+ else:
91
+ _print_terminal_results(results, verbose)
92
+
93
+ # Exit with error code if any tool failed
94
+ if any(not result.passed for result in results.values()):
95
+ sys.exit(1)
96
+
97
+ except Exception as e:
98
+ click.echo(f"Error: {e}", err=True)
99
+ sys.exit(1)
100
+
101
+
102
+ @quality_cli.command("gates")
103
+ @click.argument("path", type=click.Path(exists=True, path_type=Path))
104
+ @click.option("--coverage", type=float, help="Minimum coverage percentage required")
105
+ @click.option("--security", type=float, help="Minimum security score required")
106
+ @click.option("--complexity", type=int, help="Maximum complexity allowed")
107
+ @click.option("--documentation", type=float, help="Minimum documentation coverage required")
108
+ @click.option("--config", type=click.Path(exists=True, path_type=Path), help="Gates configuration file (JSON)")
109
+ @click.option(
110
+ "--artifact-dir",
111
+ type=click.Path(path_type=Path),
112
+ default=".quality-artifacts",
113
+ help="Directory for output artifacts",
114
+ )
115
+ @click.option("--verbose", "-v", is_flag=True, help="Verbose output")
116
+ def gates_command(
117
+ path: Path,
118
+ coverage: float | None,
119
+ security: float | None,
120
+ complexity: int | None,
121
+ documentation: float | None,
122
+ config: Path | None,
123
+ artifact_dir: Path,
124
+ verbose: bool,
125
+ ) -> None:
126
+ """Run quality gates on the given path."""
127
+ try:
128
+ # Build and validate gates configuration
129
+ gates = _build_gates_config(coverage, security, complexity, documentation, config)
130
+
131
+ if verbose:
132
+ _print_gate_info(path, gates)
133
+
134
+ # Run quality gates and handle results
135
+ results = _execute_quality_gates(path, gates, artifact_dir)
136
+ _handle_gate_results(results, verbose)
137
+
138
+ except Exception as e:
139
+ click.echo(f"Error: {e}", err=True)
140
+ sys.exit(1)
141
+
142
+
143
+ def _build_gates_config(
144
+ coverage: float | None,
145
+ security: float | None,
146
+ complexity: int | None,
147
+ documentation: float | None,
148
+ config: Path | None,
149
+ ) -> dict[str, Any]:
150
+ """Build gates configuration from CLI arguments and config file."""
151
+ gates = {}
152
+
153
+ # Add CLI-specified gates
154
+ if coverage is not None:
155
+ gates["coverage"] = coverage
156
+ if security is not None:
157
+ gates["security"] = security
158
+ if complexity is not None:
159
+ gates["complexity"] = {"max_complexity": complexity}
160
+ if documentation is not None:
161
+ gates["documentation"] = documentation
162
+
163
+ # Load from config file if provided
164
+ if config:
165
+ config_gates = json.loads(config.read_text())
166
+ gates.update(config_gates)
167
+
168
+ if not gates:
169
+ click.echo("Error: No quality gates specified", err=True)
170
+ sys.exit(1)
171
+
172
+ return gates
173
+
174
+
175
+ def _print_gate_info(path: Path, gates: dict[str, Any]) -> None:
176
+ """Print verbose information about gates being run."""
177
+ click.echo(f"Running quality gates on {path}")
178
+ click.echo(f"Gates: {gates}")
179
+
180
+
181
+ def _execute_quality_gates(path: Path, gates: dict[str, Any], artifact_dir: Path) -> Any:
182
+ """Execute quality gates and return results."""
183
+ runner = QualityRunner()
184
+ return runner.run_with_gates(path, gates, artifact_dir=artifact_dir)
185
+
186
+
187
+ def _handle_gate_results(results: Any, verbose: bool) -> None:
188
+ """Handle and display gate results, exit on failure."""
189
+ # Print summary
190
+ if results.passed:
191
+ click.echo("✅ All quality gates passed!", fg="green")
192
+ else:
193
+ click.echo("❌ Quality gates failed!", fg="red")
194
+
195
+ # Print detailed results if verbose
196
+ if verbose:
197
+ _print_detailed_results(results)
198
+
199
+ # Exit with error code if gates failed
200
+ if not results.passed:
201
+ sys.exit(1)
202
+
203
+
204
+ def _print_detailed_results(results: Any) -> None:
205
+ """Print detailed results for each tool."""
206
+ click.echo("\nDetailed Results:")
207
+ for tool, result in results.results.items():
208
+ status = "✅ PASS" if result.passed else "❌ FAIL"
209
+ score_text = f" (Score: {result.score:.1f}%)" if result.score is not None else ""
210
+ click.echo(f" {tool}: {status}{score_text}")
211
+
212
+
213
+ @quality_cli.command("coverage")
214
+ @click.argument("path", type=click.Path(exists=True, path_type=Path))
215
+ @click.option("--min-coverage", type=float, default=80.0, help="Minimum coverage percentage")
216
+ @click.option(
217
+ "--artifact-dir",
218
+ type=click.Path(path_type=Path),
219
+ default=".coverage-artifacts",
220
+ help="Directory for coverage artifacts",
221
+ )
222
+ @click.option("--html", is_flag=True, help="Generate HTML report")
223
+ @click.option("--xml", is_flag=True, help="Generate XML report")
224
+ @click.option("--verbose", "-v", is_flag=True, help="Verbose output")
225
+ def coverage_command(
226
+ path: Path, min_coverage: float, artifact_dir: Path, html: bool, xml: bool, verbose: bool
227
+ ) -> None:
228
+ """Run coverage analysis on the given path."""
229
+ try:
230
+ from .coverage.tracker import CoverageTracker
231
+
232
+ config = {"min_coverage": min_coverage, "generate_html": html, "generate_xml": xml}
233
+
234
+ tracker = CoverageTracker(config)
235
+ result = tracker.analyze(path, artifact_dir=artifact_dir)
236
+
237
+ status = "✅ PASSED" if result.passed else "❌ FAILED"
238
+ coverage_pct = result.details.get("coverage_percentage", 0)
239
+
240
+ click.echo(f"Coverage Analysis: {status}")
241
+ click.echo(f"Coverage: {coverage_pct:.1f}% (required: {min_coverage}%)")
242
+
243
+ if verbose and result.details.get("missing_files"):
244
+ click.echo("\nFiles with missing coverage:")
245
+ for file_info in result.details["missing_files"][:10]: # Top 10
246
+ click.echo(f" {file_info['filename']}: {file_info['coverage']:.1f}%")
247
+
248
+ if not result.passed:
249
+ sys.exit(1)
250
+
251
+ except Exception as e:
252
+ click.echo(f"Error: {e}", err=True)
253
+ sys.exit(1)
254
+
255
+
256
+ @quality_cli.command("security")
257
+ @click.argument("path", type=click.Path(exists=True, path_type=Path))
258
+ @click.option("--min-score", type=float, default=90.0, help="Minimum security score")
259
+ @click.option(
260
+ "--artifact-dir",
261
+ type=click.Path(path_type=Path),
262
+ default=".security-artifacts",
263
+ help="Directory for security artifacts",
264
+ )
265
+ @click.option("--verbose", "-v", is_flag=True, help="Verbose output")
266
+ def security_command(path: Path, min_score: float, artifact_dir: Path, verbose: bool) -> None:
267
+ """Run security analysis on the given path."""
268
+ try:
269
+ from .security.scanner import SecurityScanner
270
+
271
+ config = {"min_score": min_score}
272
+ scanner = SecurityScanner(config)
273
+ result = scanner.analyze(path, artifact_dir=artifact_dir)
274
+
275
+ status = "✅ PASSED" if result.passed else "❌ FAILED"
276
+ score = result.score or 0
277
+
278
+ click.echo(f"Security Analysis: {status}")
279
+ click.echo(f"Score: {score:.1f}% (required: {min_score}%)")
280
+
281
+ if verbose and result.details.get("issues"):
282
+ issues = result.details["issues"]
283
+ click.echo(f"\nFound {len(issues)} security issues:")
284
+ for issue in issues[:5]: # Top 5
285
+ severity = issue.get("severity", "unknown")
286
+ test_id = issue.get("test_id", "unknown")
287
+ filename = issue.get("filename", "unknown")
288
+ click.echo(f" [{severity.upper()}] {test_id} in {filename}")
289
+
290
+ if not result.passed:
291
+ sys.exit(1)
292
+
293
+ except Exception as e:
294
+ click.echo(f"Error: {e}", err=True)
295
+ sys.exit(1)
296
+
297
+
298
+ @quality_cli.command("complexity")
299
+ @click.argument("path", type=click.Path(exists=True, path_type=Path))
300
+ @click.option("--max-complexity", type=int, default=10, help="Maximum complexity allowed")
301
+ @click.option(
302
+ "--min-grade",
303
+ type=click.Choice(["A", "B", "C", "D", "F"]),
304
+ default="C",
305
+ help="Minimum complexity grade required",
306
+ )
307
+ @click.option(
308
+ "--artifact-dir",
309
+ type=click.Path(path_type=Path),
310
+ default=".complexity-artifacts",
311
+ help="Directory for complexity artifacts",
312
+ )
313
+ @click.option("--verbose", "-v", is_flag=True, help="Verbose output")
314
+ def complexity_command(
315
+ path: Path, max_complexity: int, min_grade: str, artifact_dir: Path, verbose: bool
316
+ ) -> None:
317
+ """Run complexity analysis on the given path."""
318
+ try:
319
+ from .complexity.analyzer import ComplexityAnalyzer
320
+
321
+ config = {"max_complexity": max_complexity, "min_grade": min_grade}
322
+
323
+ analyzer = ComplexityAnalyzer(config)
324
+ result = analyzer.analyze(path, artifact_dir=artifact_dir)
325
+
326
+ status = "✅ PASSED" if result.passed else "❌ FAILED"
327
+ grade = result.details.get("overall_grade", "N/A")
328
+ avg_complexity = result.details.get("average_complexity", 0)
329
+
330
+ click.echo(f"Complexity Analysis: {status}")
331
+ click.echo(f"Grade: {grade} (Average complexity: {avg_complexity:.1f})")
332
+
333
+ if verbose and result.details.get("most_complex_functions"):
334
+ click.echo("\nMost complex functions:")
335
+ for func in result.details["most_complex_functions"][:5]:
336
+ click.echo(f" {func['name']}: {func['complexity']} (Grade {func['rank']})")
337
+
338
+ if not result.passed:
339
+ sys.exit(1)
340
+
341
+ except Exception as e:
342
+ click.echo(f"Error: {e}", err=True)
343
+ sys.exit(1)
344
+
345
+
346
+ def _print_terminal_results(results: dict[str, Any], verbose: bool) -> None:
347
+ """Print results in terminal format."""
348
+ click.echo("Quality Analysis Results")
349
+ click.echo("=" * 50)
350
+
351
+ for tool, result in results.items():
352
+ status = "✅ PASSED" if result.passed else "❌ FAILED"
353
+ score_text = f" ({result.score:.1f}%)" if result.score is not None else ""
354
+
355
+ click.echo(f"{tool.title()}: {status}{score_text}")
356
+
357
+ if verbose:
358
+ if hasattr(result, "execution_time") and result.execution_time:
359
+ click.echo(f" Execution time: {result.execution_time:.2f}s")
360
+
361
+ # Show key details
362
+ details = result.details
363
+ if tool == "coverage" and "coverage_percentage" in details:
364
+ click.echo(f" Coverage: {details['coverage_percentage']:.1f}%")
365
+ elif tool == "security" and "total_issues" in details:
366
+ click.echo(f" Issues found: {details['total_issues']}")
367
+ elif tool == "complexity" and "average_complexity" in details:
368
+ click.echo(f" Average complexity: {details['average_complexity']:.1f}")
369
+ click.echo(f" Grade: {details.get('overall_grade', 'N/A')}")
370
+ elif tool == "documentation" and "total_coverage" in details:
371
+ click.echo(f" Documentation: {details['total_coverage']:.1f}%")
372
+
373
+ click.echo()
374
+
375
+
376
+ def _print_summary(results: dict[str, Any], verbose: bool) -> None:
377
+ """Print summary of results."""
378
+ passed = sum(1 for result in results.values() if result.passed)
379
+ total = len(results)
380
+
381
+ if passed == total:
382
+ click.echo(f"✅ All {total} quality checks passed!", fg="green")
383
+ else:
384
+ failed = total - passed
385
+ click.echo(f"❌ {failed}/{total} quality checks failed!", fg="red")
386
+
387
+ if verbose:
388
+ for tool, result in results.items():
389
+ status_icon = "✅" if result.passed else "❌"
390
+ click.echo(f" {status_icon} {tool}")
391
+
392
+
393
+ if __name__ == "__main__":
394
+ quality_cli()
@@ -0,0 +1,30 @@
1
+ """Complexity analysis integration for provide-testkit.
2
+
3
+ Provides code complexity analysis using radon and other complexity tools.
4
+ Integrates with the quality framework for comprehensive complexity analysis.
5
+
6
+ Features:
7
+ - Cyclomatic complexity analysis with radon
8
+ - Maintainability index calculation
9
+ - Raw metrics (lines of code, etc.)
10
+ - Integration with quality gates
11
+ - Grade-based reporting (A, B, C, D, F)
12
+
13
+ Usage:
14
+ # Basic complexity analysis
15
+ def test_with_complexity(complexity_analyzer):
16
+ result = complexity_analyzer.analyze(path)
17
+ assert result.passed
18
+
19
+ # Complexity with quality gates
20
+ runner = QualityRunner()
21
+ results = runner.run_with_gates(path, {"complexity": "B"})
22
+ """
23
+
24
+ from .analyzer import ComplexityAnalyzer
25
+ from .fixture import ComplexityFixture
26
+
27
+ __all__ = [
28
+ "ComplexityAnalyzer",
29
+ "ComplexityFixture",
30
+ ]