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.
- aws_test_plugin/.claude-plugin/plugin.json +25 -0
- aws_test_plugin/AGENTS.md +66 -0
- aws_test_plugin/__init__.py +3 -0
- aws_test_plugin/agents/aws-test-engineer.md +95 -0
- aws_test_plugin/cli.py +199 -0
- aws_test_plugin/scripts/analyze_results.py +208 -0
- aws_test_plugin/scripts/run_tests.py +187 -0
- aws_test_plugin/scripts/scaffold.py +316 -0
- aws_test_plugin/skills/aws-contract-testing/SKILL.md +170 -0
- aws_test_plugin/skills/aws-contract-testing/references/openapi-patterns.md +249 -0
- aws_test_plugin/skills/aws-e2e-testing/SKILL.md +206 -0
- aws_test_plugin/skills/aws-e2e-testing/references/api-gateway-patterns.md +294 -0
- aws_test_plugin/skills/aws-e2e-testing/references/batch-patterns.md +184 -0
- aws_test_plugin/skills/aws-e2e-testing/references/step-function-patterns.md +210 -0
- aws_test_plugin/skills/aws-integration-testing/SKILL.md +206 -0
- aws_test_plugin/skills/aws-integration-testing/references/lambda-dynamodb-patterns.md +240 -0
- aws_test_plugin/skills/aws-integration-testing/references/lambda-eventbridge-patterns.md +337 -0
- aws_test_plugin/skills/aws-integration-testing/references/lambda-kinesis-patterns.md +431 -0
- aws_test_plugin/skills/aws-integration-testing/references/lambda-rds-patterns.md +227 -0
- aws_test_plugin/skills/aws-integration-testing/references/lambda-s3-patterns.md +174 -0
- aws_test_plugin/skills/aws-integration-testing/references/lambda-sns-patterns.md +350 -0
- aws_test_plugin/skills/aws-integration-testing/references/lambda-sqs-patterns.md +406 -0
- aws_test_plugin/skills/aws-perf-load-testing/SKILL.md +203 -0
- aws_test_plugin/skills/aws-perf-load-testing/references/benchmark-patterns.md +325 -0
- aws_test_plugin/skills/aws-perf-load-testing/references/locust-patterns.md +370 -0
- aws_test_plugin/skills/aws-test-orchestrator/SKILL.md +320 -0
- aws_test_plugin/skills/aws-test-orchestrator/references/secrets-and-config.md +644 -0
- aws_test_plugin/skills/aws-unit-testing/SKILL.md +520 -0
- aws_test_plugin/skills/aws-unit-testing/references/boundary-branch-patterns.md +308 -0
- aws_test_plugin/skills/aws-unit-testing/references/edge-case-patterns.md +543 -0
- aws_test_plugin-0.1.0.dist-info/METADATA +342 -0
- aws_test_plugin-0.1.0.dist-info/RECORD +35 -0
- aws_test_plugin-0.1.0.dist-info/WHEEL +4 -0
- aws_test_plugin-0.1.0.dist-info/entry_points.txt +2 -0
- 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,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()
|