patchi 0.7.5__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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- patchi-0.7.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stress Orchestrator — Load testing engine for web applications.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- Load testing (steady state)
|
|
6
|
+
- Spike testing (sudden bursts)
|
|
7
|
+
- Soak testing (long duration)
|
|
8
|
+
- Breakpoint testing (find limits)
|
|
9
|
+
- Custom scenarios
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
import random
|
|
17
|
+
import statistics
|
|
18
|
+
import time
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
_log = logging.getLogger("patchi.testing.stress_orchestrator")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class StressConfig:
|
|
29
|
+
"""Configuration for a stress test."""
|
|
30
|
+
|
|
31
|
+
base_url: str
|
|
32
|
+
scenario: str = "load" # load, spike, soak, breakpoint
|
|
33
|
+
users: int = 10
|
|
34
|
+
duration_seconds: int = 60
|
|
35
|
+
ramp_up_seconds: int = 10
|
|
36
|
+
# Spike config
|
|
37
|
+
spike_multiplier: float = 3.0
|
|
38
|
+
spike_duration_seconds: int = 10
|
|
39
|
+
# Soak config
|
|
40
|
+
soak_check_interval: int = 300 # seconds
|
|
41
|
+
# Breakpoint config
|
|
42
|
+
max_users: int = 1000
|
|
43
|
+
step_users: int = 50
|
|
44
|
+
step_duration: int = 30
|
|
45
|
+
# Request config
|
|
46
|
+
requests_per_second: float | None = None # None = as fast as possible
|
|
47
|
+
think_time_ms: int = 100
|
|
48
|
+
# Endpoints to test
|
|
49
|
+
endpoints: list[dict] = field(default_factory=list)
|
|
50
|
+
# Custom scenario function
|
|
51
|
+
custom_scenario: Callable | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class RequestResult:
|
|
56
|
+
"""Result of a single HTTP request."""
|
|
57
|
+
|
|
58
|
+
timestamp: float
|
|
59
|
+
method: str
|
|
60
|
+
url: str
|
|
61
|
+
status_code: int
|
|
62
|
+
response_time_ms: float
|
|
63
|
+
success: bool
|
|
64
|
+
error: str = ""
|
|
65
|
+
response_size: int = 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class UserSession:
|
|
70
|
+
"""A single virtual user's session."""
|
|
71
|
+
|
|
72
|
+
user_id: int
|
|
73
|
+
start_time: float
|
|
74
|
+
results: list[RequestResult] = field(default_factory=list)
|
|
75
|
+
active: bool = True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class StressTestReport:
|
|
80
|
+
"""Complete stress test report."""
|
|
81
|
+
|
|
82
|
+
config: StressConfig
|
|
83
|
+
started_at: str
|
|
84
|
+
completed_at: str
|
|
85
|
+
duration_seconds: float
|
|
86
|
+
total_requests: int
|
|
87
|
+
successful_requests: int
|
|
88
|
+
failed_requests: int
|
|
89
|
+
requests_per_second: float
|
|
90
|
+
latency: dict # min, max, mean, median, p50, p90, p95, p99
|
|
91
|
+
status_codes: dict[str, int]
|
|
92
|
+
errors: dict[str, int]
|
|
93
|
+
throughput_over_time: list[dict] # time series
|
|
94
|
+
latency_over_time: list[dict]
|
|
95
|
+
user_sessions: int
|
|
96
|
+
peak_users: int
|
|
97
|
+
# Scenario-specific
|
|
98
|
+
breakpoint_found: bool = False
|
|
99
|
+
breakpoint_users: int = 0
|
|
100
|
+
soak_stability: bool = True
|
|
101
|
+
|
|
102
|
+
def to_dict(self) -> dict:
|
|
103
|
+
import dataclasses
|
|
104
|
+
return dataclasses.asdict(self)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class StressOrchestrator:
|
|
108
|
+
"""
|
|
109
|
+
Orchestrates load/stress tests against a web application.
|
|
110
|
+
|
|
111
|
+
Usage:
|
|
112
|
+
config = StressConfig(
|
|
113
|
+
base_url="https://api.example.com",
|
|
114
|
+
scenario="spike",
|
|
115
|
+
users=100,
|
|
116
|
+
duration_seconds=60,
|
|
117
|
+
)
|
|
118
|
+
orchestrator = StressOrchestrator(config)
|
|
119
|
+
report = await orchestrator.run()
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
config: StressConfig,
|
|
125
|
+
on_progress: Callable[[str], None] = None,
|
|
126
|
+
):
|
|
127
|
+
self.config = config
|
|
128
|
+
self.on_progress = on_progress or (lambda _: None)
|
|
129
|
+
self._session: Any = None
|
|
130
|
+
self._users: list[UserSession] = []
|
|
131
|
+
self._running = False
|
|
132
|
+
self._start_time = 0
|
|
133
|
+
self._results: list[RequestResult] = []
|
|
134
|
+
self._time_series: list[dict] = []
|
|
135
|
+
|
|
136
|
+
async def run(self) -> StressTestReport:
|
|
137
|
+
"""Run the stress test based on scenario."""
|
|
138
|
+
self._start_time = time.time()
|
|
139
|
+
started_at = datetime.now(UTC).isoformat()
|
|
140
|
+
|
|
141
|
+
# Initialize HTTP session
|
|
142
|
+
await self._init_session()
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
if self.config.scenario == "load":
|
|
146
|
+
await self._run_load_test()
|
|
147
|
+
elif self.config.scenario == "spike":
|
|
148
|
+
await self._run_spike_test()
|
|
149
|
+
elif self.config.scenario == "soak":
|
|
150
|
+
await self._run_soak_test()
|
|
151
|
+
elif self.config.scenario == "breakpoint":
|
|
152
|
+
await self._run_breakpoint_test()
|
|
153
|
+
elif self.config.scenario == "custom" and self.config.custom_scenario:
|
|
154
|
+
await self.config.custom_scenario(self)
|
|
155
|
+
else:
|
|
156
|
+
raise ValueError(f"Unknown scenario: {self.config.scenario}")
|
|
157
|
+
finally:
|
|
158
|
+
await self._close_session()
|
|
159
|
+
|
|
160
|
+
completed_at = datetime.now(UTC).isoformat()
|
|
161
|
+
duration = time.time() - self._start_time
|
|
162
|
+
|
|
163
|
+
return self._generate_report(started_at, completed_at, duration)
|
|
164
|
+
|
|
165
|
+
async def _init_session(self):
|
|
166
|
+
"""Initialize aiohttp session."""
|
|
167
|
+
try:
|
|
168
|
+
import aiohttp
|
|
169
|
+
|
|
170
|
+
self._session = aiohttp.ClientSession(
|
|
171
|
+
timeout=aiohttp.ClientTimeout(total=30),
|
|
172
|
+
connector=aiohttp.TCPConnector(limit=1000),
|
|
173
|
+
)
|
|
174
|
+
except ImportError:
|
|
175
|
+
_log.error("aiohttp not installed. Install with: pip install aiohttp")
|
|
176
|
+
raise
|
|
177
|
+
|
|
178
|
+
async def _close_session(self):
|
|
179
|
+
"""Close aiohttp session."""
|
|
180
|
+
if self._session:
|
|
181
|
+
await self._session.close()
|
|
182
|
+
|
|
183
|
+
async def _run_load_test(self):
|
|
184
|
+
"""Run steady-state load test."""
|
|
185
|
+
self.on_progress(
|
|
186
|
+
f"🚀 Starting load test: {self.config.users} users for {self.config.duration_seconds}s"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Ramp up users
|
|
190
|
+
await self._ramp_up_users(self.config.users, self.config.ramp_up_seconds)
|
|
191
|
+
|
|
192
|
+
# Steady state
|
|
193
|
+
steady_duration = self.config.duration_seconds - self.config.ramp_up_seconds
|
|
194
|
+
if steady_duration > 0:
|
|
195
|
+
await self._run_steady_state(steady_duration)
|
|
196
|
+
|
|
197
|
+
# Ramp down
|
|
198
|
+
await self._ramp_down_users()
|
|
199
|
+
|
|
200
|
+
async def _run_spike_test(self):
|
|
201
|
+
"""Run spike test: baseline -> spike -> baseline."""
|
|
202
|
+
self.on_progress(
|
|
203
|
+
f"⚡ Starting spike test: {self.config.users} baseline -> {int(self.config.users * self.config.spike_multiplier)} spike"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Baseline
|
|
207
|
+
await self._ramp_up_users(self.config.users, self.config.ramp_up_seconds)
|
|
208
|
+
baseline_duration = (self.config.duration_seconds - self.config.spike_duration_seconds) // 2
|
|
209
|
+
await self._run_steady_state(baseline_duration)
|
|
210
|
+
|
|
211
|
+
# Spike
|
|
212
|
+
spike_users = int(self.config.users * self.config.spike_multiplier)
|
|
213
|
+
self.on_progress(f"📈 Spiking to {spike_users} users")
|
|
214
|
+
await self._ramp_up_users(spike_users, 5) # Fast ramp
|
|
215
|
+
await self._run_steady_state(self.config.spike_duration_seconds)
|
|
216
|
+
|
|
217
|
+
# Return to baseline
|
|
218
|
+
self.on_progress("📉 Returning to baseline")
|
|
219
|
+
await self._ramp_down_users(spike_users - self.config.users)
|
|
220
|
+
await self._run_steady_state(baseline_duration)
|
|
221
|
+
|
|
222
|
+
# Final ramp down
|
|
223
|
+
await self._ramp_down_users()
|
|
224
|
+
|
|
225
|
+
async def _run_soak_test(self):
|
|
226
|
+
"""Run long-duration soak test."""
|
|
227
|
+
self.on_progress(
|
|
228
|
+
f"🏃 Starting soak test: {self.config.users} users for {self.config.duration_seconds}s"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
await self._ramp_up_users(self.config.users, self.config.ramp_up_seconds)
|
|
232
|
+
|
|
233
|
+
# Run with periodic checks
|
|
234
|
+
elapsed = 0
|
|
235
|
+
check_interval = self.config.soak_check_interval
|
|
236
|
+
|
|
237
|
+
while elapsed < self.config.duration_seconds - self.config.ramp_up_seconds:
|
|
238
|
+
remaining = min(
|
|
239
|
+
check_interval, self.config.duration_seconds - self.config.ramp_up_seconds - elapsed
|
|
240
|
+
)
|
|
241
|
+
await self._run_steady_state(remaining)
|
|
242
|
+
elapsed += remaining
|
|
243
|
+
|
|
244
|
+
# Check for degradation
|
|
245
|
+
await self._check_soak_health()
|
|
246
|
+
|
|
247
|
+
await self._ramp_down_users()
|
|
248
|
+
|
|
249
|
+
async def _run_breakpoint_test(self):
|
|
250
|
+
"""Run breakpoint test: gradually increase load until failure."""
|
|
251
|
+
self.on_progress(f"🔍 Starting breakpoint test: up to {self.config.max_users} users")
|
|
252
|
+
|
|
253
|
+
current_users = self.config.step_users
|
|
254
|
+
breakpoint_found = False
|
|
255
|
+
|
|
256
|
+
while current_users <= self.config.max_users and not breakpoint_found:
|
|
257
|
+
self.on_progress(f" Testing {current_users} users...")
|
|
258
|
+
|
|
259
|
+
await self._ramp_up_users(current_users, self.config.ramp_up_seconds)
|
|
260
|
+
await self._run_steady_state(self.config.step_duration)
|
|
261
|
+
|
|
262
|
+
# Check if system is degrading
|
|
263
|
+
if await self._check_breakpoint():
|
|
264
|
+
breakpoint_found = True
|
|
265
|
+
self.on_progress(f"💥 Breakpoint found at {current_users} users")
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
# Ramp down before next step
|
|
269
|
+
await self._ramp_down_users()
|
|
270
|
+
current_users += self.config.step_users
|
|
271
|
+
|
|
272
|
+
if not breakpoint_found:
|
|
273
|
+
self.on_progress(f"✅ No breakpoint found up to {self.config.max_users} users")
|
|
274
|
+
|
|
275
|
+
async def _ramp_up_users(self, target_users: int, duration: float):
|
|
276
|
+
"""Gradually ramp up virtual users."""
|
|
277
|
+
if target_users <= len(self._users):
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
users_to_add = target_users - len(self._users)
|
|
281
|
+
interval = duration / users_to_add if users_to_add > 0 else 0
|
|
282
|
+
|
|
283
|
+
for _i in range(users_to_add):
|
|
284
|
+
user_id = len(self._users) + 1
|
|
285
|
+
self._users.append(UserSession(user_id=user_id, start_time=time.time()))
|
|
286
|
+
|
|
287
|
+
# Start user task
|
|
288
|
+
asyncio.create_task(self._run_user(user_id))
|
|
289
|
+
|
|
290
|
+
if interval > 0:
|
|
291
|
+
await asyncio.sleep(interval)
|
|
292
|
+
|
|
293
|
+
self.on_progress(f" Ramped up to {target_users} users")
|
|
294
|
+
|
|
295
|
+
async def _ramp_down_users(self, count: int = None):
|
|
296
|
+
"""Ramp down virtual users."""
|
|
297
|
+
if count is None:
|
|
298
|
+
count = len(self._users)
|
|
299
|
+
|
|
300
|
+
for _ in range(min(count, len(self._users))):
|
|
301
|
+
if self._users:
|
|
302
|
+
user = self._users.pop()
|
|
303
|
+
user.active = False
|
|
304
|
+
|
|
305
|
+
self.on_progress(f" Ramped down to {len(self._users)} users")
|
|
306
|
+
|
|
307
|
+
async def _run_steady_state(self, duration: float):
|
|
308
|
+
"""Run steady state for specified duration."""
|
|
309
|
+
end_time = time.time() + duration
|
|
310
|
+
|
|
311
|
+
while time.time() < end_time and self._running:
|
|
312
|
+
# Collect time series data
|
|
313
|
+
await self._collect_time_series()
|
|
314
|
+
await asyncio.sleep(1)
|
|
315
|
+
|
|
316
|
+
async def _run_user(self, user_id: int):
|
|
317
|
+
"""Run a single virtual user's workload."""
|
|
318
|
+
user = next((u for u in self._users if u.user_id == user_id), None)
|
|
319
|
+
if not user:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
endpoints = self.config.endpoints or [{"method": "GET", "path": "/"}]
|
|
323
|
+
|
|
324
|
+
while user.active and self._running:
|
|
325
|
+
# Select endpoint
|
|
326
|
+
endpoint = random.choice(endpoints)
|
|
327
|
+
method = endpoint.get("method", "GET")
|
|
328
|
+
path = endpoint.get("path", "/")
|
|
329
|
+
url = f"{self.config.base_url.rstrip('/')}{path}"
|
|
330
|
+
|
|
331
|
+
# Execute request
|
|
332
|
+
result = await self._make_request(method, url)
|
|
333
|
+
user.results.append(result)
|
|
334
|
+
self._results.append(result)
|
|
335
|
+
|
|
336
|
+
# Think time
|
|
337
|
+
if self.config.think_time_ms > 0:
|
|
338
|
+
await asyncio.sleep(self.config.think_time_ms / 1000)
|
|
339
|
+
|
|
340
|
+
# Rate limiting
|
|
341
|
+
if self.config.requests_per_second:
|
|
342
|
+
await asyncio.sleep(1.0 / self.config.requests_per_second)
|
|
343
|
+
|
|
344
|
+
async def _make_request(self, method: str, url: str) -> RequestResult:
|
|
345
|
+
"""Make an HTTP request and record result."""
|
|
346
|
+
start = time.time()
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
async with self._session.request(method, url) as response:
|
|
350
|
+
await response.read()
|
|
351
|
+
response_time = (time.time() - start) * 1000
|
|
352
|
+
|
|
353
|
+
return RequestResult(
|
|
354
|
+
timestamp=time.time(),
|
|
355
|
+
method=method,
|
|
356
|
+
url=url,
|
|
357
|
+
status_code=response.status,
|
|
358
|
+
response_time_ms=response_time,
|
|
359
|
+
success=200 <= response.status < 400,
|
|
360
|
+
response_size=len(response.body) if hasattr(response, "body") else 0,
|
|
361
|
+
)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
response_time = (time.time() - start) * 1000
|
|
364
|
+
return RequestResult(
|
|
365
|
+
timestamp=time.time(),
|
|
366
|
+
method=method,
|
|
367
|
+
url=url,
|
|
368
|
+
status_code=0,
|
|
369
|
+
response_time_ms=response_time,
|
|
370
|
+
success=False,
|
|
371
|
+
error=str(e),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
async def _collect_time_series(self):
|
|
375
|
+
"""Collect time series metrics."""
|
|
376
|
+
now = time.time()
|
|
377
|
+
recent = [r for r in self._results if now - r.timestamp < 5]
|
|
378
|
+
|
|
379
|
+
if recent:
|
|
380
|
+
latencies = [r.response_time_ms for r in recent]
|
|
381
|
+
self._time_series.append(
|
|
382
|
+
{
|
|
383
|
+
"timestamp": now,
|
|
384
|
+
"requests_per_sec": len(recent) / 5,
|
|
385
|
+
"latency_p50": statistics.median(latencies),
|
|
386
|
+
"latency_p95": self._percentile(latencies, 95),
|
|
387
|
+
"error_rate": sum(1 for r in recent if not r.success) / len(recent),
|
|
388
|
+
"active_users": len([u for u in self._users if u.active]),
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
async def _check_soak_health(self) -> bool:
|
|
393
|
+
"""Check system health during soak test."""
|
|
394
|
+
# Check for memory leaks, error rate increase, latency degradation
|
|
395
|
+
recent = [r for r in self._results if time.time() - r.timestamp < 60]
|
|
396
|
+
if not recent:
|
|
397
|
+
return True
|
|
398
|
+
|
|
399
|
+
error_rate = sum(1 for r in recent if not r.success) / len(recent)
|
|
400
|
+
if error_rate > 0.05: # 5% error rate
|
|
401
|
+
self.on_progress(f"⚠️ High error rate: {error_rate:.1%}")
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
latencies = [r.response_time_ms for r in recent]
|
|
405
|
+
p99 = self._percentile(latencies, 99)
|
|
406
|
+
if p99 > 10000: # 10 seconds
|
|
407
|
+
self.on_progress(f"⚠️ High latency P99: {p99:.0f}ms")
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
return True
|
|
411
|
+
|
|
412
|
+
async def _check_breakpoint(self) -> bool:
|
|
413
|
+
"""Check if breakpoint has been reached."""
|
|
414
|
+
recent = [r for r in self._results if time.time() - r.timestamp < 10]
|
|
415
|
+
if not recent:
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
error_rate = sum(1 for r in recent if not r.success) / len(recent)
|
|
419
|
+
if error_rate > 0.10: # 10% error rate
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
latencies = [r.response_time_ms for r in recent]
|
|
423
|
+
p99 = self._percentile(latencies, 99)
|
|
424
|
+
if p99 > 30000: # 30 seconds
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
def _percentile(self, data: list[float], percentile: int) -> float:
|
|
430
|
+
"""Calculate percentile."""
|
|
431
|
+
if not data:
|
|
432
|
+
return 0
|
|
433
|
+
sorted_data = sorted(data)
|
|
434
|
+
index = int(len(sorted_data) * percentile / 100)
|
|
435
|
+
return sorted_data[min(index, len(sorted_data) - 1)]
|
|
436
|
+
|
|
437
|
+
def _generate_report(
|
|
438
|
+
self,
|
|
439
|
+
started_at: str,
|
|
440
|
+
completed_at: str,
|
|
441
|
+
duration: float,
|
|
442
|
+
) -> StressTestReport:
|
|
443
|
+
"""Generate final stress test report."""
|
|
444
|
+
if not self._results:
|
|
445
|
+
return StressTestReport(
|
|
446
|
+
config=self.config,
|
|
447
|
+
started_at=started_at,
|
|
448
|
+
completed_at=completed_at,
|
|
449
|
+
duration_seconds=duration,
|
|
450
|
+
total_requests=0,
|
|
451
|
+
successful_requests=0,
|
|
452
|
+
failed_requests=0,
|
|
453
|
+
requests_per_second=0,
|
|
454
|
+
latency={},
|
|
455
|
+
status_codes={},
|
|
456
|
+
errors={},
|
|
457
|
+
throughput_over_time=[],
|
|
458
|
+
latency_over_time=[],
|
|
459
|
+
user_sessions=len(self._users),
|
|
460
|
+
peak_users=max(len(self._users), 1),
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
latencies = [r.response_time_ms for r in self._results]
|
|
464
|
+
status_codes = {}
|
|
465
|
+
errors = {}
|
|
466
|
+
|
|
467
|
+
for r in self._results:
|
|
468
|
+
status_codes[str(r.status_code)] = status_codes.get(str(r.status_code), 0) + 1
|
|
469
|
+
if not r.success:
|
|
470
|
+
errors[r.error or "unknown"] = errors.get(r.error or "unknown", 0) + 1
|
|
471
|
+
|
|
472
|
+
return StressTestReport(
|
|
473
|
+
config=self.config,
|
|
474
|
+
started_at=started_at,
|
|
475
|
+
completed_at=completed_at,
|
|
476
|
+
duration_seconds=duration,
|
|
477
|
+
total_requests=len(self._results),
|
|
478
|
+
successful_requests=sum(1 for r in self._results if r.success),
|
|
479
|
+
failed_requests=sum(1 for r in self._results if not r.success),
|
|
480
|
+
requests_per_second=len(self._results) / duration if duration > 0 else 0,
|
|
481
|
+
latency={
|
|
482
|
+
"min": min(latencies),
|
|
483
|
+
"max": max(latencies),
|
|
484
|
+
"mean": statistics.mean(latencies),
|
|
485
|
+
"median": statistics.median(latencies),
|
|
486
|
+
"p50": self._percentile(latencies, 50),
|
|
487
|
+
"p90": self._percentile(latencies, 90),
|
|
488
|
+
"p95": self._percentile(latencies, 95),
|
|
489
|
+
"p99": self._percentile(latencies, 99),
|
|
490
|
+
},
|
|
491
|
+
status_codes=status_codes,
|
|
492
|
+
errors=errors,
|
|
493
|
+
throughput_over_time=[
|
|
494
|
+
{"timestamp": ts["timestamp"], "rps": ts["requests_per_sec"]}
|
|
495
|
+
for ts in self._time_series
|
|
496
|
+
],
|
|
497
|
+
latency_over_time=[
|
|
498
|
+
{"timestamp": ts["timestamp"], "p50": ts["latency_p50"], "p95": ts["latency_p95"]}
|
|
499
|
+
for ts in self._time_series
|
|
500
|
+
],
|
|
501
|
+
user_sessions=len(self._users),
|
|
502
|
+
peak_users=max(len(self._users), 1),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# Convenience function
|
|
507
|
+
async def run_stress_test(
|
|
508
|
+
base_url: str,
|
|
509
|
+
scenario: str = "load",
|
|
510
|
+
users: int = 10,
|
|
511
|
+
duration_seconds: int = 60,
|
|
512
|
+
**kwargs,
|
|
513
|
+
) -> StressTestReport:
|
|
514
|
+
"""Run a stress test with simple parameters."""
|
|
515
|
+
config = StressConfig(
|
|
516
|
+
base_url=base_url,
|
|
517
|
+
scenario=scenario,
|
|
518
|
+
users=users,
|
|
519
|
+
duration_seconds=duration_seconds,
|
|
520
|
+
**kwargs,
|
|
521
|
+
)
|
|
522
|
+
orchestrator = StressOrchestrator(config)
|
|
523
|
+
return await orchestrator.run()
|