solokit 0.1.1__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.
- solokit/__init__.py +10 -0
- solokit/__version__.py +3 -0
- solokit/cli.py +374 -0
- solokit/core/__init__.py +1 -0
- solokit/core/cache.py +102 -0
- solokit/core/command_runner.py +278 -0
- solokit/core/config.py +453 -0
- solokit/core/config_validator.py +204 -0
- solokit/core/constants.py +291 -0
- solokit/core/error_formatter.py +279 -0
- solokit/core/error_handlers.py +346 -0
- solokit/core/exceptions.py +1567 -0
- solokit/core/file_ops.py +309 -0
- solokit/core/logging_config.py +166 -0
- solokit/core/output.py +99 -0
- solokit/core/performance.py +57 -0
- solokit/core/protocols.py +141 -0
- solokit/core/types.py +312 -0
- solokit/deployment/__init__.py +1 -0
- solokit/deployment/executor.py +411 -0
- solokit/git/__init__.py +1 -0
- solokit/git/integration.py +619 -0
- solokit/init/__init__.py +41 -0
- solokit/init/claude_commands_installer.py +87 -0
- solokit/init/dependency_installer.py +313 -0
- solokit/init/docs_structure.py +90 -0
- solokit/init/env_generator.py +160 -0
- solokit/init/environment_validator.py +334 -0
- solokit/init/git_hooks_installer.py +71 -0
- solokit/init/git_setup.py +188 -0
- solokit/init/gitignore_updater.py +195 -0
- solokit/init/initial_commit.py +145 -0
- solokit/init/initial_scans.py +109 -0
- solokit/init/orchestrator.py +246 -0
- solokit/init/readme_generator.py +207 -0
- solokit/init/session_structure.py +239 -0
- solokit/init/template_installer.py +424 -0
- solokit/learning/__init__.py +1 -0
- solokit/learning/archiver.py +115 -0
- solokit/learning/categorizer.py +126 -0
- solokit/learning/curator.py +428 -0
- solokit/learning/extractor.py +352 -0
- solokit/learning/reporter.py +351 -0
- solokit/learning/repository.py +254 -0
- solokit/learning/similarity.py +342 -0
- solokit/learning/validator.py +144 -0
- solokit/project/__init__.py +1 -0
- solokit/project/init.py +1162 -0
- solokit/project/stack.py +436 -0
- solokit/project/sync_plugin.py +438 -0
- solokit/project/tree.py +375 -0
- solokit/quality/__init__.py +1 -0
- solokit/quality/api_validator.py +424 -0
- solokit/quality/checkers/__init__.py +25 -0
- solokit/quality/checkers/base.py +114 -0
- solokit/quality/checkers/context7.py +221 -0
- solokit/quality/checkers/custom.py +162 -0
- solokit/quality/checkers/deployment.py +323 -0
- solokit/quality/checkers/documentation.py +179 -0
- solokit/quality/checkers/formatting.py +161 -0
- solokit/quality/checkers/integration.py +394 -0
- solokit/quality/checkers/linting.py +159 -0
- solokit/quality/checkers/security.py +261 -0
- solokit/quality/checkers/spec_completeness.py +127 -0
- solokit/quality/checkers/tests.py +184 -0
- solokit/quality/env_validator.py +306 -0
- solokit/quality/gates.py +655 -0
- solokit/quality/reporters/__init__.py +10 -0
- solokit/quality/reporters/base.py +25 -0
- solokit/quality/reporters/console.py +98 -0
- solokit/quality/reporters/json_reporter.py +34 -0
- solokit/quality/results.py +98 -0
- solokit/session/__init__.py +1 -0
- solokit/session/briefing/__init__.py +245 -0
- solokit/session/briefing/documentation_loader.py +53 -0
- solokit/session/briefing/formatter.py +476 -0
- solokit/session/briefing/git_context.py +282 -0
- solokit/session/briefing/learning_loader.py +212 -0
- solokit/session/briefing/milestone_builder.py +78 -0
- solokit/session/briefing/orchestrator.py +137 -0
- solokit/session/briefing/stack_detector.py +51 -0
- solokit/session/briefing/tree_generator.py +52 -0
- solokit/session/briefing/work_item_loader.py +209 -0
- solokit/session/briefing.py +353 -0
- solokit/session/complete.py +1188 -0
- solokit/session/status.py +246 -0
- solokit/session/validate.py +452 -0
- solokit/templates/.claude/commands/end.md +109 -0
- solokit/templates/.claude/commands/init.md +159 -0
- solokit/templates/.claude/commands/learn-curate.md +88 -0
- solokit/templates/.claude/commands/learn-search.md +62 -0
- solokit/templates/.claude/commands/learn-show.md +69 -0
- solokit/templates/.claude/commands/learn.md +136 -0
- solokit/templates/.claude/commands/start.md +114 -0
- solokit/templates/.claude/commands/status.md +22 -0
- solokit/templates/.claude/commands/validate.md +27 -0
- solokit/templates/.claude/commands/work-delete.md +119 -0
- solokit/templates/.claude/commands/work-graph.md +139 -0
- solokit/templates/.claude/commands/work-list.md +26 -0
- solokit/templates/.claude/commands/work-new.md +114 -0
- solokit/templates/.claude/commands/work-next.md +25 -0
- solokit/templates/.claude/commands/work-show.md +24 -0
- solokit/templates/.claude/commands/work-update.md +141 -0
- solokit/templates/CHANGELOG.md +17 -0
- solokit/templates/WORK_ITEM_TYPES.md +141 -0
- solokit/templates/__init__.py +1 -0
- solokit/templates/bug_spec.md +217 -0
- solokit/templates/config.schema.json +150 -0
- solokit/templates/dashboard_refine/base/.gitignore +36 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/layout.tsx +22 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/page.tsx +68 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/users/page.tsx +77 -0
- solokit/templates/dashboard_refine/base/app/globals.css +60 -0
- solokit/templates/dashboard_refine/base/app/layout.tsx +23 -0
- solokit/templates/dashboard_refine/base/app/page.tsx +9 -0
- solokit/templates/dashboard_refine/base/components/client-refine-wrapper.tsx +21 -0
- solokit/templates/dashboard_refine/base/components/layout/header.tsx +44 -0
- solokit/templates/dashboard_refine/base/components/layout/sidebar.tsx +82 -0
- solokit/templates/dashboard_refine/base/components/ui/button.tsx +53 -0
- solokit/templates/dashboard_refine/base/components/ui/card.tsx +78 -0
- solokit/templates/dashboard_refine/base/components/ui/table.tsx +116 -0
- solokit/templates/dashboard_refine/base/components.json +16 -0
- solokit/templates/dashboard_refine/base/lib/refine.tsx +65 -0
- solokit/templates/dashboard_refine/base/lib/utils.ts +13 -0
- solokit/templates/dashboard_refine/base/next.config.ts +10 -0
- solokit/templates/dashboard_refine/base/package.json.template +40 -0
- solokit/templates/dashboard_refine/base/postcss.config.mjs +8 -0
- solokit/templates/dashboard_refine/base/providers/refine-provider.tsx +26 -0
- solokit/templates/dashboard_refine/base/tailwind.config.ts +57 -0
- solokit/templates/dashboard_refine/base/tsconfig.json +27 -0
- solokit/templates/dashboard_refine/docker/Dockerfile +57 -0
- solokit/templates/dashboard_refine/docker/docker-compose.prod.yml +31 -0
- solokit/templates/dashboard_refine/docker/docker-compose.yml +21 -0
- solokit/templates/dashboard_refine/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/dashboard_refine/tier-1-essential/package.json.tier1.template +57 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/setup.ts +26 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/unit/example.test.tsx +73 -0
- solokit/templates/dashboard_refine/tier-2-standard/package.json.tier2.template +62 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/eslint.config.mjs +22 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/package.json.tier3.template +79 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/stryker.conf.json +38 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/dashboard.spec.ts +88 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/user-management.spec.ts +102 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/integration/dashboard.test.tsx +90 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/type-coverage.json +16 -0
- solokit/templates/dashboard_refine/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/dashboard_refine/tier-4-production/k6/dashboard-load-test.js +70 -0
- solokit/templates/dashboard_refine/tier-4-production/next.config.ts +46 -0
- solokit/templates/dashboard_refine/tier-4-production/package.json.tier4.template +89 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/deployment_spec.md +500 -0
- solokit/templates/feature_spec.md +248 -0
- solokit/templates/fullstack_nextjs/base/.gitignore +36 -0
- solokit/templates/fullstack_nextjs/base/app/api/example/route.ts +65 -0
- solokit/templates/fullstack_nextjs/base/app/globals.css +27 -0
- solokit/templates/fullstack_nextjs/base/app/layout.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/app/page.tsx +32 -0
- solokit/templates/fullstack_nextjs/base/components/example-component.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/lib/prisma.ts +17 -0
- solokit/templates/fullstack_nextjs/base/lib/utils.ts +13 -0
- solokit/templates/fullstack_nextjs/base/lib/validations.ts +20 -0
- solokit/templates/fullstack_nextjs/base/next.config.ts +7 -0
- solokit/templates/fullstack_nextjs/base/package.json.template +32 -0
- solokit/templates/fullstack_nextjs/base/postcss.config.mjs +8 -0
- solokit/templates/fullstack_nextjs/base/prisma/schema.prisma +21 -0
- solokit/templates/fullstack_nextjs/base/tailwind.config.ts +19 -0
- solokit/templates/fullstack_nextjs/base/tsconfig.json +27 -0
- solokit/templates/fullstack_nextjs/docker/Dockerfile +60 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.prod.yml +57 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.yml +47 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/package.json.tier1.template +48 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/api/example.test.ts +88 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/unit/example.test.tsx +22 -0
- solokit/templates/fullstack_nextjs/tier-2-standard/package.json.tier2.template +52 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/package.json.tier3.template +68 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/stryker.conf.json +33 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/e2e/flow.spec.ts +59 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/integration/api.test.ts +165 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/fullstack_nextjs/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/fullstack_nextjs/tier-4-production/k6/load-test.js +45 -0
- solokit/templates/fullstack_nextjs/tier-4-production/next.config.ts +46 -0
- solokit/templates/fullstack_nextjs/tier-4-production/package.json.tier4.template +77 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/git-hooks/prepare-commit-msg +24 -0
- solokit/templates/integration_test_spec.md +363 -0
- solokit/templates/learnings.json +15 -0
- solokit/templates/ml_ai_fastapi/base/.gitignore +104 -0
- solokit/templates/ml_ai_fastapi/base/alembic/env.py +96 -0
- solokit/templates/ml_ai_fastapi/base/alembic.ini +114 -0
- solokit/templates/ml_ai_fastapi/base/pyproject.toml.template +91 -0
- solokit/templates/ml_ai_fastapi/base/requirements.txt.template +28 -0
- solokit/templates/ml_ai_fastapi/base/src/__init__.py +5 -0
- solokit/templates/ml_ai_fastapi/base/src/api/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/dependencies.py +20 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/example.py +134 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/health.py +66 -0
- solokit/templates/ml_ai_fastapi/base/src/core/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/core/config.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/core/database.py +50 -0
- solokit/templates/ml_ai_fastapi/base/src/main.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/models/__init__.py +7 -0
- solokit/templates/ml_ai_fastapi/base/src/models/example.py +61 -0
- solokit/templates/ml_ai_fastapi/base/src/services/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/services/example.py +115 -0
- solokit/templates/ml_ai_fastapi/docker/Dockerfile +59 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.prod.yml +112 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.yml +77 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyproject.toml.tier1.template +112 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyrightconfig.json +41 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pytest.ini +69 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/requirements-dev.txt +17 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/ruff.toml +81 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/conftest.py +72 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/test_main.py +49 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/test_example.py +113 -0
- solokit/templates/ml_ai_fastapi/tier-2-standard/pyproject.toml.tier2.template +130 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/locustfile.py +99 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/mutmut_config.py +53 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/pyproject.toml.tier3.template +150 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/conftest.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/test_api.py +131 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/pyproject.toml.tier4.template +162 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/requirements-prod.txt +25 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/api/routes/metrics.py +19 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/logging.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/monitoring.py +68 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/sentry.py +66 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/logging.py +79 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/tracing.py +60 -0
- solokit/templates/refactor_spec.md +287 -0
- solokit/templates/saas_t3/base/.gitignore +36 -0
- solokit/templates/saas_t3/base/app/api/trpc/[trpc]/route.ts +33 -0
- solokit/templates/saas_t3/base/app/globals.css +27 -0
- solokit/templates/saas_t3/base/app/layout.tsx +23 -0
- solokit/templates/saas_t3/base/app/page.tsx +31 -0
- solokit/templates/saas_t3/base/lib/api.tsx +77 -0
- solokit/templates/saas_t3/base/lib/utils.ts +13 -0
- solokit/templates/saas_t3/base/next.config.ts +7 -0
- solokit/templates/saas_t3/base/package.json.template +38 -0
- solokit/templates/saas_t3/base/postcss.config.mjs +8 -0
- solokit/templates/saas_t3/base/prisma/schema.prisma +20 -0
- solokit/templates/saas_t3/base/server/api/root.ts +19 -0
- solokit/templates/saas_t3/base/server/api/routers/example.ts +28 -0
- solokit/templates/saas_t3/base/server/api/trpc.ts +52 -0
- solokit/templates/saas_t3/base/server/db.ts +17 -0
- solokit/templates/saas_t3/base/tailwind.config.ts +19 -0
- solokit/templates/saas_t3/base/tsconfig.json +27 -0
- solokit/templates/saas_t3/docker/Dockerfile +60 -0
- solokit/templates/saas_t3/docker/docker-compose.prod.yml +59 -0
- solokit/templates/saas_t3/docker/docker-compose.yml +49 -0
- solokit/templates/saas_t3/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/saas_t3/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/saas_t3/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/saas_t3/tier-1-essential/package.json.tier1.template +54 -0
- solokit/templates/saas_t3/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/saas_t3/tier-1-essential/tests/unit/example.test.tsx +24 -0
- solokit/templates/saas_t3/tier-2-standard/package.json.tier2.template +58 -0
- solokit/templates/saas_t3/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/saas_t3/tier-3-comprehensive/package.json.tier3.template +74 -0
- solokit/templates/saas_t3/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/saas_t3/tier-3-comprehensive/stryker.conf.json +34 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/e2e/home.spec.ts +41 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/integration/api.test.ts +44 -0
- solokit/templates/saas_t3/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/saas_t3/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/saas_t3/tier-4-production/k6/load-test.js +51 -0
- solokit/templates/saas_t3/tier-4-production/next.config.ts +46 -0
- solokit/templates/saas_t3/tier-4-production/package.json.tier4.template +83 -0
- solokit/templates/saas_t3/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/saas_t3/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/vercel.json +37 -0
- solokit/templates/security_spec.md +287 -0
- solokit/templates/stack-versions.yaml +617 -0
- solokit/templates/status_update.json +6 -0
- solokit/templates/template-registry.json +257 -0
- solokit/templates/work_items.json +11 -0
- solokit/testing/__init__.py +1 -0
- solokit/testing/integration_runner.py +550 -0
- solokit/testing/performance.py +637 -0
- solokit/visualization/__init__.py +1 -0
- solokit/visualization/dependency_graph.py +788 -0
- solokit/work_items/__init__.py +1 -0
- solokit/work_items/creator.py +217 -0
- solokit/work_items/delete.py +264 -0
- solokit/work_items/get_dependencies.py +185 -0
- solokit/work_items/get_dependents.py +113 -0
- solokit/work_items/get_metadata.py +121 -0
- solokit/work_items/get_next_recommendations.py +133 -0
- solokit/work_items/manager.py +235 -0
- solokit/work_items/milestones.py +137 -0
- solokit/work_items/query.py +376 -0
- solokit/work_items/repository.py +267 -0
- solokit/work_items/scheduler.py +184 -0
- solokit/work_items/spec_parser.py +838 -0
- solokit/work_items/spec_validator.py +493 -0
- solokit/work_items/updater.py +157 -0
- solokit/work_items/validator.py +205 -0
- solokit-0.1.1.dist-info/METADATA +640 -0
- solokit-0.1.1.dist-info/RECORD +323 -0
- solokit-0.1.1.dist-info/WHEEL +5 -0
- solokit-0.1.1.dist-info/entry_points.txt +2 -0
- solokit-0.1.1.dist-info/licenses/LICENSE +21 -0
- solokit-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Performance benchmarking system for integration tests.
|
|
4
|
+
|
|
5
|
+
Tracks:
|
|
6
|
+
- Response times (p50, p95, p99)
|
|
7
|
+
- Throughput (requests/second)
|
|
8
|
+
- Resource utilization (CPU, memory)
|
|
9
|
+
- Regression detection
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from solokit.core.command_runner import CommandRunner
|
|
20
|
+
from solokit.core.constants import (
|
|
21
|
+
GIT_QUICK_TIMEOUT,
|
|
22
|
+
HTTP_REQUEST_TIMEOUT,
|
|
23
|
+
PERFORMANCE_REGRESSION_THRESHOLD,
|
|
24
|
+
PERFORMANCE_TEST_TIMEOUT,
|
|
25
|
+
)
|
|
26
|
+
from solokit.core.error_handlers import convert_subprocess_errors, log_errors
|
|
27
|
+
from solokit.core.exceptions import (
|
|
28
|
+
BenchmarkFailedError,
|
|
29
|
+
LoadTestFailedError,
|
|
30
|
+
PerformanceRegressionError,
|
|
31
|
+
PerformanceTestError,
|
|
32
|
+
ValidationError,
|
|
33
|
+
WorkItemNotFoundError,
|
|
34
|
+
)
|
|
35
|
+
from solokit.core.file_ops import load_json, save_json
|
|
36
|
+
from solokit.core.output import get_output
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
output = get_output()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PerformanceBenchmark:
|
|
43
|
+
"""Performance benchmarking for integration tests."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, work_item: dict):
|
|
46
|
+
"""
|
|
47
|
+
Initialize performance benchmark.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
work_item: Integration test work item with performance requirements
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValidationError: If work_item is invalid or missing required fields
|
|
54
|
+
"""
|
|
55
|
+
if not work_item:
|
|
56
|
+
raise ValidationError(
|
|
57
|
+
message="Work item cannot be None or empty",
|
|
58
|
+
context={"work_item": work_item},
|
|
59
|
+
remediation="Provide a valid work item dictionary",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.work_item = work_item
|
|
63
|
+
self.benchmarks = work_item.get("performance_benchmarks", {})
|
|
64
|
+
self.baselines_file = Path(".session/tracking/performance_baselines.json")
|
|
65
|
+
self.results: dict[str, Any] = {}
|
|
66
|
+
self.runner = CommandRunner(default_timeout=300) # Long timeout for perf tests
|
|
67
|
+
|
|
68
|
+
@log_errors()
|
|
69
|
+
def run_benchmarks(self, test_endpoint: str | None = None) -> tuple[bool, dict[str, Any]]:
|
|
70
|
+
"""
|
|
71
|
+
Run performance benchmarks.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
test_endpoint: Endpoint to benchmark (if None, uses work item config)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
(passed: bool, results: dict)
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
LoadTestFailedError: If load test fails
|
|
81
|
+
PerformanceTestError: If benchmark execution fails
|
|
82
|
+
BenchmarkFailedError: If benchmarks don't meet requirements
|
|
83
|
+
PerformanceRegressionError: If regression is detected
|
|
84
|
+
"""
|
|
85
|
+
logger.info("Running performance benchmarks...")
|
|
86
|
+
|
|
87
|
+
if test_endpoint is None:
|
|
88
|
+
test_endpoint = self.benchmarks.get("endpoint", "http://localhost:8000/health")
|
|
89
|
+
|
|
90
|
+
# Run load test
|
|
91
|
+
load_test_results = self._run_load_test(test_endpoint)
|
|
92
|
+
self.results["load_test"] = load_test_results
|
|
93
|
+
|
|
94
|
+
# Measure resource utilization
|
|
95
|
+
resource_usage = self._measure_resource_usage()
|
|
96
|
+
self.results["resource_usage"] = resource_usage
|
|
97
|
+
|
|
98
|
+
# Compare against baselines
|
|
99
|
+
passed = self._check_against_requirements()
|
|
100
|
+
regression_detected = self._check_for_regression()
|
|
101
|
+
|
|
102
|
+
self.results["passed"] = passed
|
|
103
|
+
self.results["regression_detected"] = regression_detected
|
|
104
|
+
|
|
105
|
+
# Store as new baseline if passed
|
|
106
|
+
if passed and not regression_detected:
|
|
107
|
+
self._store_baseline()
|
|
108
|
+
|
|
109
|
+
return passed and not regression_detected, self.results
|
|
110
|
+
|
|
111
|
+
@log_errors()
|
|
112
|
+
@convert_subprocess_errors
|
|
113
|
+
def _run_load_test(self, endpoint: str) -> dict[str, Any]:
|
|
114
|
+
"""
|
|
115
|
+
Run load test using wrk or similar tool.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
endpoint: URL to test
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Load test results dict
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
LoadTestFailedError: If load test fails
|
|
125
|
+
ValidationError: If endpoint is invalid
|
|
126
|
+
"""
|
|
127
|
+
if not endpoint:
|
|
128
|
+
raise ValidationError(
|
|
129
|
+
message="Endpoint cannot be empty",
|
|
130
|
+
context={"endpoint": endpoint},
|
|
131
|
+
remediation="Provide a valid endpoint URL",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
duration = self.benchmarks.get("load_test_duration", 60)
|
|
135
|
+
threads = self.benchmarks.get("threads", 4)
|
|
136
|
+
connections = self.benchmarks.get("connections", 100)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# Using wrk for load testing
|
|
140
|
+
result = self.runner.run(
|
|
141
|
+
[
|
|
142
|
+
"wrk",
|
|
143
|
+
"-t",
|
|
144
|
+
str(threads),
|
|
145
|
+
"-c",
|
|
146
|
+
str(connections),
|
|
147
|
+
"-d",
|
|
148
|
+
f"{duration}s",
|
|
149
|
+
"--latency",
|
|
150
|
+
endpoint,
|
|
151
|
+
],
|
|
152
|
+
timeout=duration + 30,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if result.success:
|
|
156
|
+
# Parse wrk output
|
|
157
|
+
return self._parse_wrk_output(result.stdout)
|
|
158
|
+
else:
|
|
159
|
+
# wrk not installed, try using Python requests as fallback
|
|
160
|
+
logger.info("wrk not available, using fallback load test")
|
|
161
|
+
return self._run_simple_load_test(endpoint, duration)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise LoadTestFailedError(
|
|
165
|
+
endpoint=endpoint,
|
|
166
|
+
details=str(e),
|
|
167
|
+
context={"duration": duration, "threads": threads, "connections": connections},
|
|
168
|
+
) from e
|
|
169
|
+
|
|
170
|
+
def _parse_wrk_output(self, output: str) -> dict[str, Any]:
|
|
171
|
+
"""
|
|
172
|
+
Parse wrk output to extract metrics.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
output: Raw wrk output
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Parsed metrics dictionary
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
PerformanceTestError: If parsing fails
|
|
182
|
+
"""
|
|
183
|
+
results: dict[str, Any] = {"latency": {}, "throughput": {}}
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
lines = output.split("\n")
|
|
187
|
+
|
|
188
|
+
for line in lines:
|
|
189
|
+
# Match percentile lines more precisely - look for lines starting with whitespace + percentage
|
|
190
|
+
line_stripped = line.strip()
|
|
191
|
+
if line_stripped.startswith("50.000%") or line_stripped.startswith("50%"):
|
|
192
|
+
# p50 latency
|
|
193
|
+
parts = line.split()
|
|
194
|
+
results["latency"]["p50"] = self._parse_latency(parts[-1])
|
|
195
|
+
elif line_stripped.startswith("75.000%") or line_stripped.startswith("75%"):
|
|
196
|
+
results["latency"]["p75"] = self._parse_latency(parts[-1])
|
|
197
|
+
elif line_stripped.startswith("90.000%") or line_stripped.startswith("90%"):
|
|
198
|
+
results["latency"]["p90"] = self._parse_latency(parts[-1])
|
|
199
|
+
elif line_stripped.startswith("99.000%") or line_stripped.startswith("99%"):
|
|
200
|
+
results["latency"]["p99"] = self._parse_latency(parts[-1])
|
|
201
|
+
elif "Requests/sec:" in line:
|
|
202
|
+
parts = line.split()
|
|
203
|
+
results["throughput"]["requests_per_sec"] = float(parts[1])
|
|
204
|
+
elif "Transfer/sec:" in line:
|
|
205
|
+
parts = line.split()
|
|
206
|
+
results["throughput"]["transfer_per_sec"] = parts[1]
|
|
207
|
+
|
|
208
|
+
return results
|
|
209
|
+
except (IndexError, ValueError) as e:
|
|
210
|
+
raise PerformanceTestError(
|
|
211
|
+
message="Failed to parse wrk output",
|
|
212
|
+
context={"output": output[:500], "error": str(e)},
|
|
213
|
+
remediation="Verify wrk is producing expected output format",
|
|
214
|
+
) from e
|
|
215
|
+
|
|
216
|
+
def _parse_latency(self, latency_str: str) -> float:
|
|
217
|
+
"""
|
|
218
|
+
Convert latency string (e.g., '1.23ms') to milliseconds.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
latency_str: Latency string from wrk
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Latency in milliseconds
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
PerformanceTestError: If parsing fails
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
latency_str = latency_str.strip()
|
|
231
|
+
if "ms" in latency_str: # milliseconds
|
|
232
|
+
return float(latency_str.rstrip("ms"))
|
|
233
|
+
elif "s" in latency_str: # seconds
|
|
234
|
+
return float(latency_str.rstrip("s")) * 1000
|
|
235
|
+
return 0.0
|
|
236
|
+
except (ValueError, AttributeError) as e:
|
|
237
|
+
raise PerformanceTestError(
|
|
238
|
+
message=f"Failed to parse latency value: {latency_str}",
|
|
239
|
+
context={"latency_str": latency_str},
|
|
240
|
+
remediation="Check wrk output format",
|
|
241
|
+
) from e
|
|
242
|
+
|
|
243
|
+
def _run_simple_load_test(self, endpoint: str, duration: int) -> dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Fallback load test using Python requests.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
endpoint: URL to test
|
|
249
|
+
duration: Test duration in seconds
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Load test results dictionary
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
LoadTestFailedError: If load test fails
|
|
256
|
+
"""
|
|
257
|
+
import time
|
|
258
|
+
|
|
259
|
+
import requests # type: ignore[import-untyped]
|
|
260
|
+
|
|
261
|
+
latencies = []
|
|
262
|
+
start_time = time.time()
|
|
263
|
+
request_count = 0
|
|
264
|
+
|
|
265
|
+
logger.info("Using simple load test (wrk not available)...")
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
while time.time() - start_time < duration:
|
|
269
|
+
req_start = time.time()
|
|
270
|
+
try:
|
|
271
|
+
requests.get(endpoint, timeout=HTTP_REQUEST_TIMEOUT)
|
|
272
|
+
latency = (time.time() - req_start) * 1000 # Convert to ms
|
|
273
|
+
latencies.append(latency)
|
|
274
|
+
request_count += 1
|
|
275
|
+
except Exception:
|
|
276
|
+
# Individual request failures are logged but don't stop the test
|
|
277
|
+
logger.debug(f"Request to {endpoint} failed, continuing test")
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
total_duration = time.time() - start_time
|
|
281
|
+
|
|
282
|
+
if not latencies:
|
|
283
|
+
raise LoadTestFailedError(
|
|
284
|
+
endpoint=endpoint,
|
|
285
|
+
details="No successful requests during load test",
|
|
286
|
+
context={"duration": duration, "request_count": request_count},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
latencies.sort()
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
"latency": {
|
|
293
|
+
"p50": latencies[int(len(latencies) * 0.50)],
|
|
294
|
+
"p75": latencies[int(len(latencies) * 0.75)],
|
|
295
|
+
"p90": latencies[int(len(latencies) * 0.90)],
|
|
296
|
+
"p95": latencies[int(len(latencies) * 0.95)],
|
|
297
|
+
"p99": latencies[int(len(latencies) * 0.99)],
|
|
298
|
+
},
|
|
299
|
+
"throughput": {"requests_per_sec": request_count / total_duration},
|
|
300
|
+
}
|
|
301
|
+
except LoadTestFailedError:
|
|
302
|
+
raise
|
|
303
|
+
except Exception as e:
|
|
304
|
+
raise LoadTestFailedError(
|
|
305
|
+
endpoint=endpoint,
|
|
306
|
+
details=f"Load test execution failed: {str(e)}",
|
|
307
|
+
context={"duration": duration},
|
|
308
|
+
) from e
|
|
309
|
+
|
|
310
|
+
@log_errors()
|
|
311
|
+
@convert_subprocess_errors
|
|
312
|
+
def _measure_resource_usage(self) -> dict[str, Any]:
|
|
313
|
+
"""
|
|
314
|
+
Measure CPU and memory usage of services.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Resource usage dictionary
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
PerformanceTestError: If resource measurement fails
|
|
321
|
+
"""
|
|
322
|
+
services = self.work_item.get("environment_requirements", {}).get("services_required", [])
|
|
323
|
+
|
|
324
|
+
resource_usage = {}
|
|
325
|
+
|
|
326
|
+
for service in services:
|
|
327
|
+
try:
|
|
328
|
+
# Get container ID
|
|
329
|
+
result = self.runner.run(
|
|
330
|
+
["docker-compose", "ps", "-q", service], timeout=GIT_QUICK_TIMEOUT
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
container_id = result.stdout.strip()
|
|
334
|
+
if not container_id:
|
|
335
|
+
logger.warning(f"No container found for service: {service}")
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
# Get resource stats
|
|
339
|
+
stats_result = self.runner.run(
|
|
340
|
+
[
|
|
341
|
+
"docker",
|
|
342
|
+
"stats",
|
|
343
|
+
container_id,
|
|
344
|
+
"--no-stream",
|
|
345
|
+
"--format",
|
|
346
|
+
"{{.CPUPerc}},{{.MemUsage}}",
|
|
347
|
+
],
|
|
348
|
+
timeout=PERFORMANCE_TEST_TIMEOUT,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if stats_result.success:
|
|
352
|
+
parts = stats_result.stdout.strip().split(",")
|
|
353
|
+
resource_usage[service] = {
|
|
354
|
+
"cpu_percent": parts[0].rstrip("%"),
|
|
355
|
+
"memory_usage": parts[1],
|
|
356
|
+
}
|
|
357
|
+
else:
|
|
358
|
+
logger.warning(
|
|
359
|
+
f"Failed to get stats for service {service}: {stats_result.stderr}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.warning(f"Error measuring resource usage for {service}: {e}")
|
|
364
|
+
resource_usage[service] = {"error": str(e)}
|
|
365
|
+
|
|
366
|
+
return resource_usage
|
|
367
|
+
|
|
368
|
+
def _check_against_requirements(self) -> bool:
|
|
369
|
+
"""
|
|
370
|
+
Check if benchmarks meet requirements.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
True if all requirements met, False otherwise
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
BenchmarkFailedError: If benchmarks fail to meet requirements
|
|
377
|
+
"""
|
|
378
|
+
requirements = self.benchmarks.get("response_time", {})
|
|
379
|
+
load_test = self.results.get("load_test", {})
|
|
380
|
+
latency = load_test.get("latency", {})
|
|
381
|
+
|
|
382
|
+
failed_benchmarks = []
|
|
383
|
+
|
|
384
|
+
# Check response time requirements
|
|
385
|
+
if "p50" in requirements:
|
|
386
|
+
actual = latency.get("p50", float("inf"))
|
|
387
|
+
expected = requirements["p50"]
|
|
388
|
+
if actual > expected:
|
|
389
|
+
logger.warning(f"p50 latency {actual}ms exceeds requirement {expected}ms")
|
|
390
|
+
failed_benchmarks.append(
|
|
391
|
+
BenchmarkFailedError(metric="p50_latency", actual=actual, expected=expected)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if "p95" in requirements:
|
|
395
|
+
actual = latency.get("p95", float("inf"))
|
|
396
|
+
expected = requirements["p95"]
|
|
397
|
+
if actual > expected:
|
|
398
|
+
logger.warning(f"p95 latency {actual}ms exceeds requirement {expected}ms")
|
|
399
|
+
failed_benchmarks.append(
|
|
400
|
+
BenchmarkFailedError(metric="p95_latency", actual=actual, expected=expected)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if "p99" in requirements:
|
|
404
|
+
actual = latency.get("p99", float("inf"))
|
|
405
|
+
expected = requirements["p99"]
|
|
406
|
+
if actual > expected:
|
|
407
|
+
logger.warning(f"p99 latency {actual}ms exceeds requirement {expected}ms")
|
|
408
|
+
failed_benchmarks.append(
|
|
409
|
+
BenchmarkFailedError(metric="p99_latency", actual=actual, expected=expected)
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Check throughput requirements
|
|
413
|
+
throughput_req = self.benchmarks.get("throughput", {})
|
|
414
|
+
throughput = load_test.get("throughput", {})
|
|
415
|
+
|
|
416
|
+
if "minimum" in throughput_req:
|
|
417
|
+
actual_rps = throughput.get("requests_per_sec", 0)
|
|
418
|
+
expected_rps = throughput_req["minimum"]
|
|
419
|
+
if actual_rps < expected_rps:
|
|
420
|
+
logger.warning(f"Throughput {actual_rps} req/s below minimum {expected_rps} req/s")
|
|
421
|
+
failed_benchmarks.append(
|
|
422
|
+
BenchmarkFailedError(
|
|
423
|
+
metric="throughput", actual=actual_rps, expected=expected_rps, unit="req/s"
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# If any benchmarks failed, raise the first one
|
|
428
|
+
if failed_benchmarks:
|
|
429
|
+
raise failed_benchmarks[0]
|
|
430
|
+
|
|
431
|
+
return True
|
|
432
|
+
|
|
433
|
+
def _check_for_regression(self) -> bool:
|
|
434
|
+
"""
|
|
435
|
+
Check for performance regression against baseline.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
True if regression detected, False otherwise
|
|
439
|
+
|
|
440
|
+
Raises:
|
|
441
|
+
PerformanceRegressionError: If regression is detected
|
|
442
|
+
"""
|
|
443
|
+
if not self.baselines_file.exists():
|
|
444
|
+
logger.info("No baseline found, skipping regression check")
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
baselines = load_json(self.baselines_file)
|
|
449
|
+
except Exception as e:
|
|
450
|
+
logger.warning(f"Failed to load baselines: {e}")
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
work_item_id = self.work_item.get("id")
|
|
454
|
+
if not work_item_id:
|
|
455
|
+
logger.info("Work item has no id, skipping regression check")
|
|
456
|
+
return False
|
|
457
|
+
|
|
458
|
+
baseline = baselines.get(work_item_id)
|
|
459
|
+
|
|
460
|
+
if not baseline:
|
|
461
|
+
logger.info(f"No baseline for work item {work_item_id}")
|
|
462
|
+
return False
|
|
463
|
+
|
|
464
|
+
load_test = self.results.get("load_test", {})
|
|
465
|
+
latency = load_test.get("latency", {})
|
|
466
|
+
baseline_latency = baseline.get("latency", {})
|
|
467
|
+
|
|
468
|
+
# Check for latency regression
|
|
469
|
+
for percentile in ["p50", "p95", "p99"]:
|
|
470
|
+
current = latency.get(percentile, 0)
|
|
471
|
+
baseline_val = baseline_latency.get(percentile, 0)
|
|
472
|
+
|
|
473
|
+
if baseline_val > 0 and current > baseline_val * PERFORMANCE_REGRESSION_THRESHOLD:
|
|
474
|
+
regression_percent = (current / baseline_val - 1) * 100
|
|
475
|
+
logger.warning(
|
|
476
|
+
f"Performance regression detected: {percentile} increased from "
|
|
477
|
+
f"{baseline_val}ms to {current}ms ({regression_percent:.1f}% slower)"
|
|
478
|
+
)
|
|
479
|
+
raise PerformanceRegressionError(
|
|
480
|
+
metric=percentile,
|
|
481
|
+
current=current,
|
|
482
|
+
baseline=baseline_val,
|
|
483
|
+
threshold_percent=(PERFORMANCE_REGRESSION_THRESHOLD - 1) * 100,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
@log_errors()
|
|
489
|
+
def _store_baseline(self) -> None:
|
|
490
|
+
"""
|
|
491
|
+
Store current results as baseline.
|
|
492
|
+
|
|
493
|
+
Raises:
|
|
494
|
+
PerformanceTestError: If baseline storage fails
|
|
495
|
+
"""
|
|
496
|
+
try:
|
|
497
|
+
if not self.baselines_file.exists():
|
|
498
|
+
baselines = {}
|
|
499
|
+
else:
|
|
500
|
+
baselines = load_json(self.baselines_file)
|
|
501
|
+
|
|
502
|
+
work_item_id = self.work_item.get("id")
|
|
503
|
+
if not work_item_id:
|
|
504
|
+
raise PerformanceTestError(
|
|
505
|
+
message="Work item has no id, cannot store baseline",
|
|
506
|
+
context={"work_item": self.work_item},
|
|
507
|
+
remediation="Ensure work item has an 'id' field",
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
baselines[work_item_id] = {
|
|
511
|
+
"latency": self.results.get("load_test", {}).get("latency", {}),
|
|
512
|
+
"throughput": self.results.get("load_test", {}).get("throughput", {}),
|
|
513
|
+
"resource_usage": self.results.get("resource_usage", {}),
|
|
514
|
+
"timestamp": datetime.now().isoformat(),
|
|
515
|
+
"session": self._get_current_session(),
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
save_json(self.baselines_file, baselines)
|
|
519
|
+
logger.info(f"Baseline stored for work item {work_item_id}")
|
|
520
|
+
except Exception as e:
|
|
521
|
+
raise PerformanceTestError(
|
|
522
|
+
message=f"Failed to store baseline for work item {work_item_id}",
|
|
523
|
+
context={"work_item_id": work_item_id, "error": str(e)},
|
|
524
|
+
remediation="Check file permissions and disk space",
|
|
525
|
+
) from e
|
|
526
|
+
|
|
527
|
+
def _get_current_session(self) -> int:
|
|
528
|
+
"""
|
|
529
|
+
Get current session number.
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
Current session number or 0 if not found
|
|
533
|
+
"""
|
|
534
|
+
status_file = Path(".session/tracking/status_update.json")
|
|
535
|
+
if status_file.exists():
|
|
536
|
+
try:
|
|
537
|
+
status = load_json(status_file)
|
|
538
|
+
session_num = status.get("session_number", 0)
|
|
539
|
+
return int(session_num) if session_num is not None else 0
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.warning(f"Failed to load session status: {e}")
|
|
542
|
+
return 0
|
|
543
|
+
return 0
|
|
544
|
+
|
|
545
|
+
def generate_report(self) -> str:
|
|
546
|
+
"""
|
|
547
|
+
Generate performance benchmark report.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Formatted report string
|
|
551
|
+
"""
|
|
552
|
+
load_test = self.results.get("load_test", {})
|
|
553
|
+
latency = load_test.get("latency", {})
|
|
554
|
+
throughput = load_test.get("throughput", {})
|
|
555
|
+
|
|
556
|
+
report = f"""
|
|
557
|
+
Performance Benchmark Report
|
|
558
|
+
{"=" * 80}
|
|
559
|
+
|
|
560
|
+
Latency:
|
|
561
|
+
p50: {latency.get("p50", "N/A")} ms
|
|
562
|
+
p75: {latency.get("p75", "N/A")} ms
|
|
563
|
+
p90: {latency.get("p90", "N/A")} ms
|
|
564
|
+
p95: {latency.get("p95", "N/A")} ms
|
|
565
|
+
p99: {latency.get("p99", "N/A")} ms
|
|
566
|
+
|
|
567
|
+
Throughput:
|
|
568
|
+
Requests/sec: {throughput.get("requests_per_sec", "N/A")}
|
|
569
|
+
|
|
570
|
+
Resource Usage:
|
|
571
|
+
"""
|
|
572
|
+
|
|
573
|
+
for service, usage in self.results.get("resource_usage", {}).items():
|
|
574
|
+
report += f" {service}:\n"
|
|
575
|
+
report += f" CPU: {usage.get('cpu_percent', 'N/A')}\n"
|
|
576
|
+
report += f" Memory: {usage.get('memory_usage', 'N/A')}\n"
|
|
577
|
+
|
|
578
|
+
report += f"\nStatus: {'PASSED' if self.results.get('passed') else 'FAILED'}\n"
|
|
579
|
+
|
|
580
|
+
if self.results.get("regression_detected"):
|
|
581
|
+
report += "WARNING: Performance regression detected!\n"
|
|
582
|
+
|
|
583
|
+
return report
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
@log_errors()
|
|
587
|
+
def main() -> None:
|
|
588
|
+
"""
|
|
589
|
+
CLI entry point.
|
|
590
|
+
|
|
591
|
+
Raises:
|
|
592
|
+
ValidationError: If command line arguments are invalid
|
|
593
|
+
WorkItemNotFoundError: If work item doesn't exist
|
|
594
|
+
PerformanceTestError: If benchmarks fail
|
|
595
|
+
"""
|
|
596
|
+
import sys
|
|
597
|
+
|
|
598
|
+
if len(sys.argv) < 2:
|
|
599
|
+
raise ValidationError(
|
|
600
|
+
message="Missing required argument: work_item_id",
|
|
601
|
+
context={"usage": "python performance_benchmark.py <work_item_id>"},
|
|
602
|
+
remediation="Provide a work item ID as the first argument",
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
work_item_id = sys.argv[1]
|
|
606
|
+
|
|
607
|
+
# Load work item
|
|
608
|
+
work_items_file = Path(".session/tracking/work_items.json")
|
|
609
|
+
try:
|
|
610
|
+
data = load_json(work_items_file)
|
|
611
|
+
work_item = data["work_items"].get(work_item_id)
|
|
612
|
+
|
|
613
|
+
if not work_item:
|
|
614
|
+
raise WorkItemNotFoundError(work_item_id)
|
|
615
|
+
|
|
616
|
+
# Run benchmarks
|
|
617
|
+
benchmark = PerformanceBenchmark(work_item)
|
|
618
|
+
passed, results = benchmark.run_benchmarks()
|
|
619
|
+
|
|
620
|
+
output.info(benchmark.generate_report())
|
|
621
|
+
|
|
622
|
+
sys.exit(0 if passed else 1)
|
|
623
|
+
|
|
624
|
+
except (BenchmarkFailedError, PerformanceRegressionError, LoadTestFailedError) as e:
|
|
625
|
+
logger.error(f"Performance test failed: {e.message}")
|
|
626
|
+
output.info(f"\nERROR: {e.message}")
|
|
627
|
+
if e.remediation:
|
|
628
|
+
output.info(f"REMEDIATION: {e.remediation}")
|
|
629
|
+
sys.exit(e.exit_code)
|
|
630
|
+
except Exception as e:
|
|
631
|
+
logger.exception("Unexpected error during performance benchmarking")
|
|
632
|
+
output.info(f"\nERROR: {e}")
|
|
633
|
+
sys.exit(1)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
if __name__ == "__main__":
|
|
637
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Visualization tools including dependency graphs."""
|