aws-test-plugin 0.1.0__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 (35) hide show
  1. aws_test_plugin/.claude-plugin/plugin.json +25 -0
  2. aws_test_plugin/AGENTS.md +66 -0
  3. aws_test_plugin/__init__.py +3 -0
  4. aws_test_plugin/agents/aws-test-engineer.md +95 -0
  5. aws_test_plugin/cli.py +199 -0
  6. aws_test_plugin/scripts/analyze_results.py +208 -0
  7. aws_test_plugin/scripts/run_tests.py +187 -0
  8. aws_test_plugin/scripts/scaffold.py +316 -0
  9. aws_test_plugin/skills/aws-contract-testing/SKILL.md +170 -0
  10. aws_test_plugin/skills/aws-contract-testing/references/openapi-patterns.md +249 -0
  11. aws_test_plugin/skills/aws-e2e-testing/SKILL.md +206 -0
  12. aws_test_plugin/skills/aws-e2e-testing/references/api-gateway-patterns.md +294 -0
  13. aws_test_plugin/skills/aws-e2e-testing/references/batch-patterns.md +184 -0
  14. aws_test_plugin/skills/aws-e2e-testing/references/step-function-patterns.md +210 -0
  15. aws_test_plugin/skills/aws-integration-testing/SKILL.md +206 -0
  16. aws_test_plugin/skills/aws-integration-testing/references/lambda-dynamodb-patterns.md +240 -0
  17. aws_test_plugin/skills/aws-integration-testing/references/lambda-eventbridge-patterns.md +337 -0
  18. aws_test_plugin/skills/aws-integration-testing/references/lambda-kinesis-patterns.md +431 -0
  19. aws_test_plugin/skills/aws-integration-testing/references/lambda-rds-patterns.md +227 -0
  20. aws_test_plugin/skills/aws-integration-testing/references/lambda-s3-patterns.md +174 -0
  21. aws_test_plugin/skills/aws-integration-testing/references/lambda-sns-patterns.md +350 -0
  22. aws_test_plugin/skills/aws-integration-testing/references/lambda-sqs-patterns.md +406 -0
  23. aws_test_plugin/skills/aws-perf-load-testing/SKILL.md +203 -0
  24. aws_test_plugin/skills/aws-perf-load-testing/references/benchmark-patterns.md +325 -0
  25. aws_test_plugin/skills/aws-perf-load-testing/references/locust-patterns.md +370 -0
  26. aws_test_plugin/skills/aws-test-orchestrator/SKILL.md +320 -0
  27. aws_test_plugin/skills/aws-test-orchestrator/references/secrets-and-config.md +644 -0
  28. aws_test_plugin/skills/aws-unit-testing/SKILL.md +520 -0
  29. aws_test_plugin/skills/aws-unit-testing/references/boundary-branch-patterns.md +308 -0
  30. aws_test_plugin/skills/aws-unit-testing/references/edge-case-patterns.md +543 -0
  31. aws_test_plugin-0.1.0.dist-info/METADATA +342 -0
  32. aws_test_plugin-0.1.0.dist-info/RECORD +35 -0
  33. aws_test_plugin-0.1.0.dist-info/WHEEL +4 -0
  34. aws_test_plugin-0.1.0.dist-info/entry_points.txt +2 -0
  35. aws_test_plugin-0.1.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "aws-test-plugin",
3
+ "description": "AI-powered test generation skills for AWS Python projects — Lambda, API Gateway, Step Functions, and Batch. Generates E2E, integration, contract, performance, and load tests by reading actual handler code.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "whitewhiteqq"
7
+ },
8
+ "homepage": "https://github.com/whitewhiteqq/aws-test-plugin",
9
+ "repository": "https://github.com/whitewhiteqq/aws-test-plugin",
10
+ "license": "Apache-2.0",
11
+ "keywords": [
12
+ "aws",
13
+ "testing",
14
+ "lambda",
15
+ "api-gateway",
16
+ "step-functions",
17
+ "batch",
18
+ "pytest",
19
+ "moto",
20
+ "locust",
21
+ "e2e",
22
+ "integration",
23
+ "contract"
24
+ ]
25
+ }
@@ -0,0 +1,66 @@
1
+ # AWS Test Plugin — Agent Instructions
2
+
3
+ This project uses `aws-test-plugin` to generate comprehensive tests
4
+ for AWS Python projects (Lambda, API Gateway, Step Functions, Batch).
5
+
6
+ ## Available Test Types
7
+
8
+ | Type | Tool | Local | Deployed |
9
+ |------|------|-------|----------|
10
+ | Unit | pytest + mocks | Yes | No |
11
+ | Integration | moto + pytest | Yes | No |
12
+ | Contract | jsonschema + pytest | Yes | No |
13
+ | Performance | pytest-benchmark | Yes | No |
14
+ | E2E | requests + pytest | No | Yes |
15
+ | Load | Locust | No | Yes |
16
+
17
+ ## Testing Workflow
18
+
19
+ 1. **Discover** — scan for Lambda handlers (`main.py`, `handler.py`), Batch jobs
20
+ (`Dockerfile`), Step Functions (`*.asl.json`), API specs (`swagger.yml`)
21
+ 2. **Read code** — analyze handler source for branches, AWS calls, validation
22
+ 3. **Generate tests** — create test files covering every code path, branch, and boundary
23
+ 4. **Run** — `python scripts/run_tests.py all` (1-click for all offline tests)
24
+ 5. **Fix** — read failures, adjust test logic, re-run
25
+
26
+ ## Secrets Management
27
+
28
+ - **Never** hardcode API keys, tokens, or passwords in test files
29
+ - Two strategies available (see secrets-and-config.md for full details):
30
+ - **Strategy A**: `.env` files (gitignored) — secrets loaded via `os.getenv()` with `pytest.skip()` fallback
31
+ - **Strategy B**: AWS Secrets Manager + SSM Parameter Store — secrets retrieved via `boto3` with `pytest.skip()` fallback
32
+ - **AWS credentials**: use the default AWS profile (`~/.aws/credentials`) — boto3 picks it up automatically
33
+ - Unit/integration/contract tests work with fake `"testing"` credentials (moto) — no real AWS access needed
34
+ - E2E/load tests load real credentials from `.env` or Secrets Manager depending on chosen strategy
35
+
36
+ ## Commands
37
+
38
+ ```bash
39
+ # Scaffold test directories + .env.example
40
+ aws-test-plugin scaffold .
41
+
42
+ # 1-click: run ALL offline tests (unit + integration + contract + performance)
43
+ python scripts/run_tests.py all
44
+
45
+ # 1-click: run EVERYTHING (offline + E2E; tests self-skip if env vars missing)
46
+ python scripts/run_tests.py full
47
+
48
+ # Individual categories
49
+ pytest tests/unit/ -m unit -v --cov=src/ --cov-branch
50
+ pytest tests/integration/ -m integration -v
51
+ pytest tests/contract/ -m contract -v
52
+ pytest tests/performance/ -m performance --benchmark-enable
53
+ pytest tests/e2e/ -m e2e -v # reads API_BASE_URL from .env
54
+
55
+ # Load tests (requires deployed API)
56
+ locust -f tests/load/locustfile.py --host=$LOAD_TEST_HOST \
57
+ --users=50 --spawn-rate=5 --run-time=5m --headless
58
+ ```
59
+
60
+ ## Safety
61
+
62
+ - Never run E2E or load tests against production without confirmation
63
+ - All test data uses prefixes: `TEST-`, `E2E-TEST-`, `LOAD-TEST-`
64
+ - Integration tests use moto (no real AWS calls)
65
+ - Load tests must have `--run-time` set
66
+ - No secrets in test files — use `.env` (gitignored) or AWS Secrets Manager / SSM Parameter Store
@@ -0,0 +1,3 @@
1
+ """aws-test-plugin — AI-powered test generation for AWS Python projects."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: aws-test-engineer
3
+ purpose: >
4
+ Generate, run, and fix comprehensive tests for AWS Python projects.
5
+ Analyzes actual handler code to produce E2E, integration, contract,
6
+ performance, and load tests. Works with Lambda, API Gateway,
7
+ Step Functions, and Batch jobs.
8
+ skills:
9
+ - aws-test-orchestrator
10
+ - aws-unit-testing
11
+ - aws-e2e-testing
12
+ - aws-integration-testing
13
+ - aws-contract-testing
14
+ - aws-perf-load-testing
15
+ definition_of_done:
16
+ - All discovered AWS components have corresponding test files
17
+ - Unit tests cover every branch, boundary value, and edge case in business logic
18
+ - Every code path (happy path, error handling, edge cases) has a test
19
+ - Tests pass locally with moto/testcontainers (no real AWS calls)
20
+ - Test coverage for handler logic is >= 80% (branch coverage)
21
+ - Performance benchmarks have defined thresholds
22
+ - Load test locustfiles match API spec endpoints
23
+ - Test data uses prefixes for easy cleanup (TEST-, E2E-TEST-, LOAD-TEST-)
24
+ - conftest.py provides shared fixtures for all test types
25
+ - pytest.ini configures markers and test paths
26
+ - No secrets, API keys, or tokens hardcoded in test files
27
+ - .env.example documents required configuration variables
28
+ - All offline tests can run in 1 click via `python scripts/run_tests.py all`
29
+ - Installation guidance lists the required test dependency groups
30
+ safety:
31
+ - Never run tests against production without explicit user confirmation
32
+ - Never make real AWS API calls — use moto or testcontainers
33
+ - Never store credentials, API keys, or tokens in test files
34
+ - Never delete production data
35
+ - Load tests must have --run-time set (no unbounded runs)
36
+ - All test data is prefixed and self-cleaning
37
+ - Use .env files (gitignored) for secrets, never commit .env
38
+ ---
39
+
40
+ # AWS Test Engineer Agent
41
+
42
+ I generate comprehensive tests for AWS Python projects by reading the actual
43
+ source code, not from generic templates.
44
+
45
+ ## Workflow
46
+
47
+ 1. **Discover** — Scan the project for Lambda handlers, Batch jobs, Step
48
+ Function definitions, API Gateway specs, and existing tests.
49
+
50
+ 2. **Analyze Code** — Read each handler to understand:
51
+ - Input event shapes and validation rules
52
+ - AWS service interactions (boto3 calls)
53
+ - Branching logic and error handling paths
54
+ - Return value structures
55
+ - Environment variable dependencies
56
+
57
+ 3. **Generate Tests** — Create test files that cover every code path:
58
+ - Unit tests for pure business logic (all branches, boundaries, edge cases)
59
+ - Integration tests with moto for AWS service interactions
60
+ - E2E tests for deployed API endpoints
61
+ - Contract tests for OpenAPI/Swagger compliance
62
+ - Performance benchmarks for latency and memory
63
+ - Load tests for throughput and SLA validation
64
+
65
+ 4. **Scaffold** — Set up test infrastructure:
66
+ - `conftest.py` with shared fixtures
67
+ - `pytest.ini` with markers and configuration
68
+ - Test directory structure matching the project layout
69
+
70
+ 5. **Run & Fix** — Execute tests and iteratively fix failures by reading
71
+ error output and adjusting test logic.
72
+
73
+ ## When to Use Me
74
+
75
+ | User Says | What I Do |
76
+ |-----------|-----------|
77
+ | "Write tests for my Lambda" | Discover handlers → read code → generate unit + integration tests |
78
+ | "Test all branches" | Read handler code → map every if/else/try/except → generate unit tests |
79
+ | "Test boundary values" | Identify numeric/string/collection fields → parametrized boundary tests |
80
+ | "Add E2E tests for my API" | Find API spec → read handlers → generate endpoint tests |
81
+ | "Test my Step Function" | Find ASL definition → read state handlers → generate flow tests |
82
+ | "Benchmark my handler" | Read handler → mock deps → generate pytest-benchmark tests |
83
+ | "Load test my API" | Read API spec → generate Locust users with weighted endpoints |
84
+ | "Set up test infrastructure" | Scaffold conftest.py, pytest.ini, .env.example, directory layout |
85
+ | "What's not tested?" | Compare handler code paths vs existing tests → report gaps |
86
+ | "Run all tests" | 1-click: `python scripts/run_tests.py all` (or `full` for E2E too) |
87
+
88
+ ## Test Categories
89
+
90
+ | Category | Tool | Runs Locally | Needs Deploy |
91
+ |----------|------|-------------|--------------|| Unit | pytest + mocks | Yes | No || Integration | moto + pytest | Yes | No |
92
+ | Contract | jsonschema + pytest | Yes | No |
93
+ | Performance | pytest-benchmark | Yes | No |
94
+ | E2E | requests + pytest | No | Yes |
95
+ | Load | Locust | No | Yes |
aws_test_plugin/cli.py ADDED
@@ -0,0 +1,199 @@
1
+ """CLI entry point for aws-test-plugin."""
2
+
3
+ import shutil
4
+ import sys
5
+ from importlib.resources import files
6
+ from pathlib import Path
7
+
8
+
9
+ def _get_pkg_dir() -> Path:
10
+ """Return the installed package directory (contains AGENTS.md, scripts/)."""
11
+ return Path(str(files("aws_test_plugin")))
12
+
13
+
14
+ def _get_data_dir() -> Path:
15
+ """Return the directory containing skills/ and agents/.
16
+
17
+ In an installed wheel, force-include maps root skills/ and agents/ into
18
+ the package directory. During editable/development installs the package
19
+ dir won't have them, so fall back to the repo root.
20
+ """
21
+ pkg = _get_pkg_dir()
22
+ if (pkg / "skills").is_dir():
23
+ return pkg
24
+ # Editable install: src/aws_test_plugin -> repo root is two levels up
25
+ repo_root = pkg.parent.parent
26
+ if (repo_root / "skills").is_dir():
27
+ return repo_root
28
+ return pkg
29
+
30
+
31
+ def _copy_tree(src: Path, dst: Path, label: str) -> int:
32
+ """Copy a directory tree, reporting what's copied. Returns file count."""
33
+ count = 0
34
+ for item in sorted(src.rglob("*")):
35
+ if item.is_file():
36
+ rel = item.relative_to(src)
37
+ target = dst / rel
38
+ target.parent.mkdir(parents=True, exist_ok=True)
39
+ if target.exists():
40
+ print(f" skip {label}/{rel} (already exists)")
41
+ else:
42
+ shutil.copy2(item, target)
43
+ print(f" + {label}/{rel}")
44
+ count += 1
45
+ return count
46
+
47
+
48
+ def cmd_init(target: str, agents: list[str] | None = None) -> None:
49
+ """Install skills and agents into a target project."""
50
+ root = Path(target).resolve()
51
+ data = _get_data_dir()
52
+
53
+ agents_to_install = agents or ["claude", "copilot", "codex"]
54
+
55
+ total = 0
56
+ skills_src = data / "skills"
57
+ agents_src = data / "agents"
58
+
59
+ # Always install skills
60
+ if skills_src.is_dir():
61
+ for agent in agents_to_install:
62
+ if agent in ("claude",):
63
+ dst = root / ".claude" / "skills"
64
+ total += _copy_tree(skills_src, dst, ".claude/skills")
65
+ if agent in ("copilot",):
66
+ dst = root / ".github" / "skills"
67
+ total += _copy_tree(skills_src, dst, ".github/skills")
68
+
69
+ # Install agent definitions
70
+ if agents_src.is_dir():
71
+ for agent in agents_to_install:
72
+ if agent in ("claude",):
73
+ dst = root / ".claude" / "agents"
74
+ total += _copy_tree(agents_src, dst, ".claude/agents")
75
+ if agent in ("copilot",):
76
+ dst = root / ".github" / "agents"
77
+ total += _copy_tree(agents_src, dst, ".github/agents")
78
+
79
+ # For Codex: generate AGENTS.md if requested
80
+ if "codex" in agents_to_install:
81
+ agents_md = root / "AGENTS.md"
82
+ if not agents_md.exists():
83
+ # AGENTS.md lives in the package dir, not with skills/agents
84
+ agents_md_src = _get_pkg_dir() / "AGENTS.md"
85
+ if agents_md_src.exists():
86
+ shutil.copy2(agents_md_src, agents_md)
87
+ print(" + AGENTS.md")
88
+ total += 1
89
+
90
+ print(f"\nInstalled {total} files into {root}")
91
+ print("Next: pip install 'aws-test-plugin[test]' (or uv sync --extra test)")
92
+
93
+
94
+ def cmd_list() -> None:
95
+ """List available skills."""
96
+ data = _get_data_dir()
97
+ skills_dir = data / "skills"
98
+ if not skills_dir.is_dir():
99
+ print("No skills found.")
100
+ return
101
+
102
+ print("Available skills:\n")
103
+ for skill in sorted(skills_dir.iterdir()):
104
+ if skill.is_dir() and (skill / "SKILL.md").exists():
105
+ # Read first line of description from frontmatter
106
+ desc = ""
107
+ in_frontmatter = False
108
+ in_desc = False
109
+ for line in (skill / "SKILL.md").read_text(encoding="utf-8").splitlines():
110
+ if line.strip() == "---" and not in_frontmatter:
111
+ in_frontmatter = True
112
+ continue
113
+ if line.strip() == "---" and in_frontmatter:
114
+ break
115
+ if line.startswith("description:"):
116
+ desc = line.split(":", 1)[1].strip().strip(">").strip()
117
+ in_desc = True
118
+ continue
119
+ if in_desc and line.startswith(" "):
120
+ desc += " " + line.strip()
121
+ continue
122
+ if in_desc:
123
+ in_desc = False
124
+
125
+ refs = list((skill / "references").glob("*.md")) if (skill / "references").is_dir() else []
126
+ print(f" {skill.name}")
127
+ if desc:
128
+ print(f" {desc[:100]}")
129
+ if refs:
130
+ print(f" references: {', '.join(r.stem for r in refs)}")
131
+ print()
132
+
133
+
134
+ def cmd_scaffold(target: str) -> None:
135
+ """Run the scaffold script to discover and set up test directories."""
136
+ # Scripts live in the package dir, not with skills/agents
137
+ pkg = _get_pkg_dir()
138
+ scaffold_script = pkg / "scripts" / "scaffold.py"
139
+ if not scaffold_script.exists():
140
+ print("Error: scaffold.py not found in package data.")
141
+ sys.exit(1)
142
+
143
+ # Import and run scaffold.main() directly
144
+ import importlib.util
145
+
146
+ spec = importlib.util.spec_from_file_location("scaffold", scaffold_script)
147
+ if spec is None or spec.loader is None:
148
+ print("Error: unable to load scaffold.py from package data.")
149
+ sys.exit(1)
150
+
151
+ mod = importlib.util.module_from_spec(spec)
152
+ spec.loader.exec_module(mod)
153
+ sys.argv = ["scaffold", "--project-root", target]
154
+ mod.main()
155
+
156
+
157
+ def main() -> None:
158
+ """CLI entry point."""
159
+ import argparse
160
+
161
+ parser = argparse.ArgumentParser(
162
+ prog="aws-test-plugin",
163
+ description="Install AWS test skills for AI coding agents (Claude Code, GitHub Copilot, Codex)",
164
+ )
165
+ sub = parser.add_subparsers(dest="command", required=True)
166
+
167
+ # init
168
+ p_init = sub.add_parser("init", help="Install skills and agents into a project")
169
+ p_init.add_argument("target", nargs="?", default=".", help="Project root (default: .)")
170
+ p_init.add_argument(
171
+ "--agent",
172
+ "-a",
173
+ action="append",
174
+ choices=["claude", "copilot", "codex", "all"],
175
+ help="Which agent(s) to install for (default: all). Repeatable.",
176
+ )
177
+
178
+ # list
179
+ sub.add_parser("list", help="List available skills")
180
+
181
+ # scaffold
182
+ p_scaffold = sub.add_parser("scaffold", help="Scaffold test directories for an AWS project")
183
+ p_scaffold.add_argument("target", nargs="?", default=".", help="Project root (default: .)")
184
+
185
+ args = parser.parse_args()
186
+
187
+ if args.command == "init":
188
+ agents = args.agent or ["all"]
189
+ if "all" in agents:
190
+ agents = ["claude", "copilot", "codex"]
191
+ cmd_init(args.target, agents)
192
+ elif args.command == "list":
193
+ cmd_list()
194
+ elif args.command == "scaffold":
195
+ cmd_scaffold(args.target)
196
+
197
+
198
+ if __name__ == "__main__":
199
+ main()
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Analyze test results from JUnit XML, Locust CSV, and pytest-benchmark JSON.
4
+
5
+ Usage:
6
+ python analyze_results.py tests/reports/
7
+ """
8
+
9
+ import csv
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from defusedxml import ElementTree as ET
15
+
16
+
17
+ def analyze_junit(xml_path: Path) -> dict:
18
+ """Parse JUnit XML and return summary."""
19
+ tree = ET.parse(xml_path)
20
+ root = tree.getroot()
21
+ if root is None:
22
+ raise ValueError(f"Invalid JUnit XML file: {xml_path}")
23
+
24
+ suites = root.findall(".//testsuite") if root.tag != "testsuite" else [root]
25
+ total = errors = failures = skipped = 0
26
+ failed_tests = []
27
+
28
+ for suite in suites:
29
+ total += int(suite.get("tests", 0))
30
+ errors += int(suite.get("errors", 0))
31
+ failures += int(suite.get("failures", 0))
32
+ skipped += int(suite.get("skipped", 0))
33
+
34
+ for tc in suite.findall("testcase"):
35
+ failure = tc.find("failure")
36
+ error = tc.find("error")
37
+ detail = failure if failure is not None else error
38
+ if detail is not None:
39
+ msg = detail.get("message", "")
40
+ failed_tests.append(
41
+ {
42
+ "name": f"{tc.get('classname')}.{tc.get('name')}",
43
+ "message": msg[:200],
44
+ }
45
+ )
46
+
47
+ return {
48
+ "file": xml_path.name,
49
+ "total": total,
50
+ "passed": total - errors - failures - skipped,
51
+ "failed": errors + failures,
52
+ "skipped": skipped,
53
+ "failed_tests": failed_tests,
54
+ }
55
+
56
+
57
+ def analyze_locust(stats_path: Path) -> dict:
58
+ """Parse Locust stats CSV and return summary."""
59
+ results = []
60
+ aggregated = None
61
+
62
+ with open(stats_path) as f:
63
+ reader = csv.DictReader(f)
64
+ for row in reader:
65
+ entry = {
66
+ "name": row.get("Name", ""),
67
+ "requests": int(row.get("Request Count", 0)),
68
+ "failures": int(row.get("Failure Count", 0)),
69
+ "avg_ms": float(row.get("Average Response Time", 0)),
70
+ "p50_ms": float(row.get("50%", 0)),
71
+ "p95_ms": float(row.get("95%", 0)),
72
+ "p99_ms": float(row.get("99%", 0)),
73
+ "rps": float(row.get("Requests/s", 0)),
74
+ }
75
+ if entry["name"] == "Aggregated":
76
+ aggregated = entry
77
+ else:
78
+ results.append(entry)
79
+
80
+ return {
81
+ "file": stats_path.name,
82
+ "endpoints": results,
83
+ "aggregated": aggregated,
84
+ }
85
+
86
+
87
+ def analyze_benchmark(json_path: Path) -> dict:
88
+ """Parse pytest-benchmark JSON and return summary."""
89
+ with open(json_path) as f:
90
+ data = json.load(f)
91
+
92
+ benchmarks = []
93
+ for bm in data.get("benchmarks", []):
94
+ benchmarks.append(
95
+ {
96
+ "name": bm.get("name", ""),
97
+ "min_ms": bm["stats"]["min"] * 1000,
98
+ "max_ms": bm["stats"]["max"] * 1000,
99
+ "mean_ms": bm["stats"]["mean"] * 1000,
100
+ "median_ms": bm["stats"]["median"] * 1000,
101
+ "stddev_ms": bm["stats"]["stddev"] * 1000,
102
+ "rounds": bm["stats"]["rounds"],
103
+ }
104
+ )
105
+
106
+ return {
107
+ "file": json_path.name,
108
+ "benchmarks": benchmarks,
109
+ }
110
+
111
+
112
+ def print_junit_report(result: dict):
113
+ """Print JUnit analysis."""
114
+ print(f"\n--- {result['file']} ---")
115
+ status = "PASS" if result["failed"] == 0 else "FAIL"
116
+ print(f" Status: {status}")
117
+ print(
118
+ f" Total: {result['total']} "
119
+ f"Passed: {result['passed']} "
120
+ f"Failed: {result['failed']} "
121
+ f"Skipped: {result['skipped']}"
122
+ )
123
+
124
+ if result["failed_tests"]:
125
+ print(" Failures:")
126
+ for ft in result["failed_tests"]:
127
+ print(f" - {ft['name']}: {ft['message']}")
128
+
129
+
130
+ def print_locust_report(result: dict):
131
+ """Print Locust analysis."""
132
+ print(f"\n--- {result['file']} ---")
133
+ agg = result.get("aggregated")
134
+ if agg:
135
+ error_pct = agg["failures"] / agg["requests"] * 100 if agg["requests"] > 0 else 0
136
+ print(f" Total Requests: {agg['requests']}")
137
+ print(f" Error Rate: {error_pct:.1f}%")
138
+ print(
139
+ f" Avg: {agg['avg_ms']:.0f}ms "
140
+ f"p50: {agg['p50_ms']:.0f}ms "
141
+ f"p95: {agg['p95_ms']:.0f}ms "
142
+ f"p99: {agg['p99_ms']:.0f}ms"
143
+ )
144
+ print(f" Throughput: {agg['rps']:.1f} req/s")
145
+
146
+ if result["endpoints"]:
147
+ print("\n Per-endpoint:")
148
+ for ep in result["endpoints"]:
149
+ print(
150
+ f" {ep['name']}: avg={ep['avg_ms']:.0f}ms "
151
+ f"p95={ep['p95_ms']:.0f}ms "
152
+ f"({ep['requests']} reqs, {ep['failures']} fails)"
153
+ )
154
+
155
+
156
+ def print_benchmark_report(result: dict):
157
+ """Print benchmark analysis."""
158
+ print(f"\n--- {result['file']} ---")
159
+ for bm in result["benchmarks"]:
160
+ print(f" {bm['name']}:")
161
+ print(
162
+ f" mean={bm['mean_ms']:.2f}ms "
163
+ f"median={bm['median_ms']:.2f}ms "
164
+ f"min={bm['min_ms']:.2f}ms "
165
+ f"max={bm['max_ms']:.2f}ms "
166
+ f"({bm['rounds']} rounds)"
167
+ )
168
+
169
+
170
+ def main():
171
+ reports_dir = Path(sys.argv[1] if len(sys.argv) > 1 else "tests/reports")
172
+
173
+ if not reports_dir.exists():
174
+ print(f"Reports directory not found: {reports_dir}")
175
+ sys.exit(1)
176
+
177
+ print(f"Analyzing results in {reports_dir}/\n")
178
+ print("=" * 60)
179
+
180
+ has_failures = False
181
+
182
+ # JUnit XML files
183
+ for xml_file in sorted(reports_dir.glob("*_results.xml")):
184
+ result = analyze_junit(xml_file)
185
+ print_junit_report(result)
186
+ if result["failed"] > 0:
187
+ has_failures = True
188
+
189
+ # Locust CSV files
190
+ for csv_file in sorted(reports_dir.glob("*_stats.csv")):
191
+ result = analyze_locust(csv_file)
192
+ print_locust_report(result)
193
+
194
+ # Benchmark JSON files
195
+ for json_file in sorted(reports_dir.glob("*benchmark*.json")):
196
+ result = analyze_benchmark(json_file)
197
+ print_benchmark_report(result)
198
+
199
+ print(f"\n{'=' * 60}")
200
+ if has_failures:
201
+ print("RESULT: Some tests failed. See details above.")
202
+ sys.exit(1)
203
+ else:
204
+ print("RESULT: All analyzed results look good.")
205
+
206
+
207
+ if __name__ == "__main__":
208
+ main()