synapse-hub 2.2.4 → 2.4.1
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.
- package/.agents/agents/failure-analyst.md +11 -0
- package/.agents/agents/live-explorer.md +28 -0
- package/.agents/agents/orchestrator.md +23 -4
- package/.agents/agents/pom-crafter.md +15 -0
- package/.agents/agents/quality-gatekeeper.md +20 -4
- package/.agents/agents/test-runner.md +12 -0
- package/.agents/rules/communication-style.md +40 -0
- package/.agents/rules/context-engineering.md +23 -0
- package/.agents/rules/git-push-protocol.md +10 -16
- package/.agents/rules/graphify.md +14 -14
- package/.agents/rules/mandatory-playwright-live.md +29 -29
- package/.agents/rules/mobile_appium_rules.md +132 -0
- package/.agents/rules/performance_testing_rules.md +74 -0
- package/.agents/rules/systematic-debugging.md +6 -6
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
- package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
- package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
- package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
- package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
- package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
- package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
- package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
- package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
- package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
- package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
- package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
- package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
- package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
- package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
- package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
- package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
- package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
- package/.agents/skills/github-actions-templates/SKILL.md +47 -304
- package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
- package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
- package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
- package/.agents/skills/lam-script-generator/SKILL.md +42 -1
- package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
- package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
- package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
- package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
- package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
- package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
- package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
- package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
- package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
- package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
- package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
- package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +54 -135
- package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
- package/.agents/skills/self-healing-locator/SKILL.md +24 -10
- package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
- package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
- package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
- package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
- package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
- package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
- package/.agents/skills/systematic-debugging/SKILL.md +100 -0
- package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
- package/.agents/skills/test-driven-development/SKILL.md +114 -0
- package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
- package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
- package/.agents/skills/verification-before-completion/SKILL.md +100 -0
- package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
- package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
- package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
- package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
- package/.agents/templates/completion_report.md +57 -57
- package/.agents/workflows/analyze_requirement_document.md +18 -0
- package/.agents/workflows/export_testcases_excel.md +32 -0
- package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
- package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
- package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
- package/.agents/workflows/generate_requirements_from_website.md +25 -0
- package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
- package/.agents/workflows/graphify.md +10 -10
- package/.agents/workflows/verify.md +2 -2
- package/.cursor/rules/language.mdc +7 -7
- package/.cursor/rules/synapse-qa.mdc +23 -0
- package/.cursorrules +7 -0
- package/.pre-commit-hooks.yaml +14 -0
- package/.windsurfrules +7 -0
- package/AGENTS.md +54 -15
- package/CLAUDE.md +35 -12
- package/LICENSE +30 -0
- package/NOTICE +180 -0
- package/README.en.md +331 -0
- package/README.md +252 -515
- package/action.yml +51 -0
- package/benchmarks/qa_eval/README.md +71 -0
- package/benchmarks/qa_eval/build_dataset.py +677 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/eval_runner.py +418 -0
- package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
- package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
- package/benchmarks/qa_eval/reports/eval_results.json +105 -0
- package/bin/agent-qa.js +16 -4
- package/docs/ARCHITECTURE.md +107 -0
- package/docs/CI_CD_INTEGRATION.md +94 -0
- package/docs/CLI_REFERENCE.md +157 -0
- package/docs/CONTRIBUTING.md +91 -0
- package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
- package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
- package/docs/adopted-ecosystem-tools.md +87 -0
- package/docs/schemas/synapse.config.schema.json +64 -0
- package/examples/cypress-ecommerce-suite/README.md +45 -0
- package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
- package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
- package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
- package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
- package/examples/cypress-ecommerce-suite/package.json +13 -0
- package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
- package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
- package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
- package/examples/katalon-suite/Profiles/default.glbl +12 -0
- package/examples/katalon-suite/README.md +18 -0
- package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
- package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
- package/examples/katalon-suite/katalon_suite.prj +21 -0
- package/examples/playwright-crm-suite/README.md +40 -0
- package/examples/playwright-crm-suite/package.json +14 -0
- package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
- package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
- package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
- package/examples/playwright-crm-suite/playwright.config.ts +27 -0
- package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
- package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
- package/examples/plugins/sample-custom-plugin/README.md +14 -0
- package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
- package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
- package/examples/practices/README.md +22 -0
- package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
- package/examples/practices/requirements/crm/requirements_customers.md +462 -0
- package/examples/practices/requirements/crm/requirements_login.md +202 -0
- package/examples/practices/requirements/crm/requirements_projects.md +396 -0
- package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
- package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +52 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +119 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +32 -0
- package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
- package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
- package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
- package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
- package/examples/pytest-api-suite/README.md +45 -0
- package/examples/pytest-api-suite/conftest.py +31 -0
- package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
- package/examples/pytest-api-suite/requirements.txt +2 -0
- package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
- package/examples/selenium-suite/README.md +18 -0
- package/examples/selenium-suite/pages/base_page.py +27 -0
- package/examples/selenium-suite/pages/login_page.py +29 -0
- package/examples/selenium-suite/pages/order_page.py +25 -0
- package/examples/selenium-suite/pytest.ini +6 -0
- package/examples/selenium-suite/requirements.txt +2 -0
- package/examples/selenium-suite/tests/test_login.py +10 -0
- package/examples/selenium-suite/tests/test_order.py +10 -0
- package/package.json +27 -12
- package/scripts/generate_skill_examples.py +244 -0
- package/scripts/install.ps1 +112 -0
- package/scripts/install_global_plugin.ps1 +94 -0
- package/scripts/install_global_plugin.sh +49 -0
- package/scripts/lint_skills.ps1 +57 -0
- package/scripts/package_dist.py +323 -0
- package/scripts/setup.sh +59 -0
- package/scripts/update_hub.ps1 +19 -0
- package/scripts/update_hub.sh +15 -0
- package/skills_manifest.json +209 -6
- package/synapse.config.json +30 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
- package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
- package/templates/ai_bridges/CLAUDE.md +33 -16
- package/templates/ai_bridges/copilot-instructions.md +14 -21
- package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
- package/templates/ai_bridges/windsurfrules +21 -21
- package/templates/ci/github-actions-synapse.yml +71 -4
- package/templates/welcome.html.tmpl +1674 -0
- package/tools/.infer_baseline.json +1102 -0
- package/tools/TOOLS_RUNBOOK.md +98 -131
- package/tools/__init__.py +26 -0
- package/tools/agent_canary.py +501 -501
- package/tools/analyze_project.py +31 -17
- package/tools/api_fuzzer.py +278 -6
- package/tools/author_integrity.py +6 -6
- package/tools/auto_heal.py +1 -1
- package/tools/browser_helpers.js +105 -7
- package/tools/bug_reporter.py +31 -1
- package/tools/bump_version.py +30 -3
- package/tools/cache_resolver.py +248 -14
- package/tools/check_env.py +55 -14
- package/tools/check_version_sync.py +171 -0
- package/tools/ci_healing_hook.py +1 -1
- package/tools/cli.py +454 -42
- package/tools/count_tokens.py +1257 -0
- package/tools/dashboard_generator.py +2638 -0
- package/tools/db_sandbox.py +198 -20
- package/tools/e2e_flow_prober.ps1 +12 -64
- package/tools/e2e_flow_prober.py +99 -0
- package/tools/e2e_flow_prober.sh +4 -0
- package/tools/flaky_detector.py +4 -3
- package/tools/generate_allure_report.ps1 +12 -29
- package/tools/generate_allure_report.py +77 -0
- package/tools/generate_allure_report.sh +4 -0
- package/tools/generate_jmx_plan.py +257 -58
- package/tools/generate_test_reports.ps1 +12 -134
- package/tools/generate_test_reports.py +133 -0
- package/tools/generate_test_reports.sh +4 -0
- package/tools/gherkin_suite.py +758 -0
- package/tools/git_pre_commit_guard.py +12 -6
- package/tools/goldfish_review.py +366 -0
- package/tools/heal_pr_automator.py +1 -1
- package/tools/heuristic_locator_matcher.py +170 -21
- package/tools/infer_linter.py +612 -70
- package/tools/lint_ai_docs.py +64 -0
- package/tools/matrix_sharder.py +1 -1
- package/tools/mcp_excel_server.ps1 +12 -250
- package/tools/mcp_excel_server.py +249 -0
- package/tools/mcp_excel_server.sh +4 -0
- package/tools/mcp_server.py +368 -144
- package/tools/md_to_xlsx.py +307 -0
- package/tools/micro_mutator.py +302 -69
- package/tools/omni_transpiler.py +2 -2
- package/tools/output_formatter.py +128 -0
- package/tools/pinpoint_failure.py +88 -88
- package/tools/project_state.py +2 -2
- package/tools/repo_map.py +1 -1
- package/tools/safety_firewall.py +196 -45
- package/tools/setup_project.py +295 -31
- package/tools/skillspector.py +31 -0
- package/tools/spec_drift_sentinel.py +439 -0
- package/tools/sync_agent_rules.py +248 -0
- package/tools/telemetry_correlator.py +45 -0
- package/tools/test_impact.py +51 -12
- package/tools/tms_sync_bridge.py +21 -5
- package/tools/universal_ir.py +3 -0
- package/tools/update_sentinel.py +87 -15
- package/tools/verify_all.ps1 +13 -0
- package/tools/verify_all.py +479 -85
- package/tools/visual_diff_canvas.py +1 -0
- package/tools/visual_som_helper.py +1 -1
- package/tools/watch_mode.py +305 -0
- package/tools/welcome_page.py +291 -0
|
@@ -18,7 +18,11 @@ Comprehensive 10/10 Pinnacle Performance Features:
|
|
|
18
18
|
- RegexExtractor for HTML hidden CSRF / Anti-Forgery tokens
|
|
19
19
|
- JSONPostProcessor with Match Number 0 (Random dynamic item choice avoiding duplicate lockups)
|
|
20
20
|
- Chaining Bearer JWT token & entity IDs.
|
|
21
|
-
6. Workload Archetypes: Linear, Step-Up (Break-point), Spike, and Soak profiles
|
|
21
|
+
6. Workload Archetypes: Linear, Step-Up (Break-point), Spike, and Soak profiles -
|
|
22
|
+
step/spike emit staged ThreadGroups (per-stage threads/ramp/hold/delay windows).
|
|
23
|
+
6b. Locust Code-as-Code Export (`--emit-locust`): generates a runnable
|
|
24
|
+
locustfile.py (User class + weighted @task endpoints) from the same
|
|
25
|
+
OpenAPI/Postman/HAR ingestion - concept borrowed from Locust (MIT).
|
|
22
26
|
7. Enterprise Security: JSR223 Groovy HMAC-SHA256 payload signer & correlation IDs.
|
|
23
27
|
8. Full-Stack Observability & Telemetry:
|
|
24
28
|
- InfluxDB Backend Listener for live Grafana APM dashboard streaming
|
|
@@ -57,6 +61,40 @@ if hasattr(sys.stdout, "reconfigure"):
|
|
|
57
61
|
pass
|
|
58
62
|
|
|
59
63
|
|
|
64
|
+
def compute_workload_stages(workload_profile: str, vus: int, ramp_up: int, duration: int) -> List[Dict[str, int]]:
|
|
65
|
+
"""Computes staged execution windows for step/spike profiles.
|
|
66
|
+
|
|
67
|
+
Each stage is a plain JMeter ThreadGroup with its own scheduler window
|
|
68
|
+
(threads, ramp, hold, delay) so staged load works on vanilla JMeter 5.x
|
|
69
|
+
without plugins. Linear/soak return a single stage (behavior unchanged).
|
|
70
|
+
"""
|
|
71
|
+
if workload_profile == "step":
|
|
72
|
+
step_count = 4
|
|
73
|
+
per_step = max(1, (vus + step_count - 1) // step_count)
|
|
74
|
+
window_ramp = max(1, min(10, ramp_up))
|
|
75
|
+
window_hold = max(30, duration // step_count)
|
|
76
|
+
stages = []
|
|
77
|
+
for i in range(step_count):
|
|
78
|
+
threads = min(per_step * (i + 1), vus)
|
|
79
|
+
stages.append({"threads": threads, "ramp": window_ramp,
|
|
80
|
+
"hold": window_hold, "delay": i * (window_ramp + window_hold)})
|
|
81
|
+
return stages
|
|
82
|
+
|
|
83
|
+
if workload_profile == "spike":
|
|
84
|
+
base = max(1, round(vus * 0.2))
|
|
85
|
+
base_hold = max(10, round(duration * 0.7))
|
|
86
|
+
spike_hold = max(10, round(duration * 0.15))
|
|
87
|
+
tail_hold = max(10, duration - base_hold - spike_hold)
|
|
88
|
+
return [
|
|
89
|
+
{"threads": base, "ramp": ramp_up, "hold": base_hold, "delay": 0},
|
|
90
|
+
{"threads": vus, "ramp": 5, "hold": spike_hold, "delay": base_hold},
|
|
91
|
+
{"threads": base, "ramp": 5, "hold": tail_hold, "delay": base_hold + spike_hold},
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# linear / soak: single continuous stage (legacy behavior, property-overridable)
|
|
95
|
+
return [{"threads": vus, "ramp": ramp_up, "hold": duration, "delay": 0}]
|
|
96
|
+
|
|
97
|
+
|
|
60
98
|
def build_jmx_xml(
|
|
61
99
|
plan_name: str,
|
|
62
100
|
base_host: str,
|
|
@@ -351,23 +389,46 @@ def build_jmx_xml(
|
|
|
351
389
|
if workload_profile == "soak" and duration < 3600:
|
|
352
390
|
actual_duration = 3600 # Default 1h for soak
|
|
353
391
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
392
|
+
stages = compute_workload_stages(workload_profile, vus, ramp_up, actual_duration)
|
|
393
|
+
is_staged = len(stages) > 1
|
|
394
|
+
|
|
395
|
+
def _emit_stage_group(stage: Dict[str, int], stage_index: int) -> None:
|
|
396
|
+
"""Emits one stage ThreadGroup header. Single-stage plans keep the
|
|
397
|
+
legacy property-overridable form (${__P(vus,...)}); staged plans use
|
|
398
|
+
concrete per-stage windows."""
|
|
399
|
+
name = tg_name if not is_staged else f"{tg_name} - Stage {stage_index + 1}"
|
|
400
|
+
if is_staged:
|
|
401
|
+
threads_prop = str(stage["threads"])
|
|
402
|
+
ramp_prop = str(stage["ramp"])
|
|
403
|
+
duration_prop = str(stage["hold"])
|
|
404
|
+
delay_prop = str(stage["delay"])
|
|
405
|
+
header = f' <!-- Workload Stage {stage_index + 1}/{len(stages)}: {stage["threads"]} VUs, ramp {stage["ramp"]}s, hold {stage["hold"]}s, delay {stage["delay"]}s -->'
|
|
406
|
+
else:
|
|
407
|
+
threads_prop = f'${{__P(vus,{stage["threads"]})}}'
|
|
408
|
+
ramp_prop = f'${{__P(ramp_up,{stage["ramp"]})}}'
|
|
409
|
+
duration_prop = f'${{__P(duration,{stage["hold"]})}}'
|
|
410
|
+
delay_prop = "0"
|
|
411
|
+
header = f' <!-- Main Thread Group: {workload_profile.upper()} Profile -->'
|
|
412
|
+
xml_lines.extend([
|
|
413
|
+
header,
|
|
414
|
+
f' <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="{name}" enabled="true">',
|
|
415
|
+
' <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>',
|
|
416
|
+
' <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">',
|
|
417
|
+
f' <boolProp name="LoopController.continue_forever">{continue_forever}</boolProp>',
|
|
418
|
+
f' <stringProp name="LoopController.loops">${{__P(loops,{loops_str})}}</stringProp>',
|
|
419
|
+
' </elementProp>',
|
|
420
|
+
f' <stringProp name="ThreadGroup.num_threads">{threads_prop}</stringProp>',
|
|
421
|
+
f' <stringProp name="ThreadGroup.ramp_time">{ramp_prop}</stringProp>',
|
|
422
|
+
' <boolProp name="ThreadGroup.scheduler">true</boolProp>',
|
|
423
|
+
f' <stringProp name="ThreadGroup.duration">{duration_prop}</stringProp>',
|
|
424
|
+
f' <stringProp name="ThreadGroup.delay">{delay_prop}</stringProp>',
|
|
425
|
+
' <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>',
|
|
426
|
+
' </ThreadGroup>',
|
|
427
|
+
' <hashTree>'
|
|
428
|
+
])
|
|
429
|
+
|
|
430
|
+
_emit_stage_group(stages[0], 0)
|
|
431
|
+
stage_body_start = len(xml_lines)
|
|
371
432
|
|
|
372
433
|
# CSV Data Set Config (Dynamic credentials / entities)
|
|
373
434
|
if csv_file:
|
|
@@ -677,47 +738,60 @@ def build_jmx_xml(
|
|
|
677
738
|
' <hashTree/>'
|
|
678
739
|
])
|
|
679
740
|
|
|
680
|
-
#
|
|
741
|
+
# Capture the first stage's hashTree body, close it, then replicate it for
|
|
742
|
+
# the remaining staged profiles (step/spike) so every stage executes the
|
|
743
|
+
# full sampler set inside its own scheduler window (vanilla JMeter, no
|
|
744
|
+
# plugins). The Result Collector moves to Test-Plan level so all stages
|
|
745
|
+
# stream into one JTL file.
|
|
746
|
+
stage_body = xml_lines[stage_body_start:]
|
|
747
|
+
xml_lines.append(' </hashTree>')
|
|
748
|
+
|
|
749
|
+
for stage_idx in range(1, len(stages)):
|
|
750
|
+
_emit_stage_group(stages[stage_idx], stage_idx)
|
|
751
|
+
xml_lines.extend(stage_body)
|
|
752
|
+
xml_lines.append(' </hashTree>')
|
|
753
|
+
|
|
754
|
+
# Result Collectors for CLI JTL export and Summary (plan-level: one JTL
|
|
755
|
+
# across all workload stages)
|
|
681
756
|
xml_lines.extend([
|
|
682
|
-
'
|
|
683
|
-
'
|
|
684
|
-
'
|
|
685
|
-
'
|
|
686
|
-
'
|
|
687
|
-
'
|
|
688
|
-
'
|
|
689
|
-
'
|
|
690
|
-
'
|
|
691
|
-
'
|
|
692
|
-
'
|
|
693
|
-
'
|
|
694
|
-
'
|
|
695
|
-
'
|
|
696
|
-
'
|
|
697
|
-
'
|
|
698
|
-
'
|
|
699
|
-
'
|
|
700
|
-
'
|
|
701
|
-
'
|
|
702
|
-
'
|
|
703
|
-
'
|
|
704
|
-
'
|
|
705
|
-
'
|
|
706
|
-
'
|
|
707
|
-
'
|
|
708
|
-
'
|
|
709
|
-
'
|
|
710
|
-
'
|
|
711
|
-
'
|
|
712
|
-
'
|
|
713
|
-
'
|
|
714
|
-
'
|
|
715
|
-
'
|
|
716
|
-
'
|
|
717
|
-
'
|
|
718
|
-
'
|
|
719
|
-
'
|
|
720
|
-
' </hashTree>',
|
|
757
|
+
' <!-- Headless JTL Result Collector & Summary Report (plan-level, all stages) -->',
|
|
758
|
+
' <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">',
|
|
759
|
+
' <boolProp name="ResultCollector.error_logging">false</boolProp>',
|
|
760
|
+
' <objProp>',
|
|
761
|
+
' <name>saveConfig</name>',
|
|
762
|
+
' <value class="SampleSaveConfiguration">',
|
|
763
|
+
' <time>true</time>',
|
|
764
|
+
' <latency>true</latency>',
|
|
765
|
+
' <timestamp>true</timestamp>',
|
|
766
|
+
' <success>true</success>',
|
|
767
|
+
' <label>true</label>',
|
|
768
|
+
' <code>true</code>',
|
|
769
|
+
' <message>true</message>',
|
|
770
|
+
' <threadName>true</threadName>',
|
|
771
|
+
' <dataType>true</dataType>',
|
|
772
|
+
' <encoding>false</encoding>',
|
|
773
|
+
' <assertions>true</assertions>',
|
|
774
|
+
' <subresults>true</subresults>',
|
|
775
|
+
' <responseData>false</responseData>',
|
|
776
|
+
' <samplerData>false</samplerData>',
|
|
777
|
+
' <xml>false</xml>',
|
|
778
|
+
' <fieldNames>true</fieldNames>',
|
|
779
|
+
' <responseHeaders>false</responseHeaders>',
|
|
780
|
+
' <requestHeaders>false</requestHeaders>',
|
|
781
|
+
' <responseDataOnError>false</responseDataOnError>',
|
|
782
|
+
' <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>',
|
|
783
|
+
' <assertionsResultsToSave>0</assertionsResultsToSave>',
|
|
784
|
+
' <bytes>true</bytes>',
|
|
785
|
+
' <sentBytes>true</sentBytes>',
|
|
786
|
+
' <url>true</url>',
|
|
787
|
+
' <threadCounts>true</threadCounts>',
|
|
788
|
+
' <idleTime>true</idleTime>',
|
|
789
|
+
' <connectTime>true</connectTime>',
|
|
790
|
+
' </value>',
|
|
791
|
+
' </objProp>',
|
|
792
|
+
' <stringProp name="filename">jmeter_results.jtl</stringProp>',
|
|
793
|
+
' </ResultCollector>',
|
|
794
|
+
' <hashTree/>',
|
|
721
795
|
' </hashTree>',
|
|
722
796
|
' </hashTree>',
|
|
723
797
|
'</jmeterTestPlan>'
|
|
@@ -726,6 +800,108 @@ def build_jmx_xml(
|
|
|
726
800
|
return "\n".join(xml_lines)
|
|
727
801
|
|
|
728
802
|
|
|
803
|
+
def generate_locustfile(
|
|
804
|
+
plan_name: str,
|
|
805
|
+
base_host: str,
|
|
806
|
+
protocol: str,
|
|
807
|
+
port: str,
|
|
808
|
+
samplers: List[Dict[str, Any]],
|
|
809
|
+
weighted_mix: Optional[str] = None,
|
|
810
|
+
think_time_min_ms: int = 0,
|
|
811
|
+
think_time_max_ms: int = 0,
|
|
812
|
+
) -> str:
|
|
813
|
+
"""Generates a runnable locustfile.py (code-as-code load model).
|
|
814
|
+
|
|
815
|
+
Concept borrowed from Locust (MIT): the plan becomes a User class with
|
|
816
|
+
weighted @task endpoints, honoring the same --weighted-mix traffic shaping
|
|
817
|
+
as the JMX export (weights chunk the samplers sequentially).
|
|
818
|
+
"""
|
|
819
|
+
host = f"{protocol}://{base_host}" if port in ("80", "443") else f"{protocol}://{base_host}:{port}"
|
|
820
|
+
|
|
821
|
+
# Distribute weights: chunk samplers sequentially per percentage.
|
|
822
|
+
task_weights: List[int] = [1] * len(samplers)
|
|
823
|
+
if weighted_mix:
|
|
824
|
+
try:
|
|
825
|
+
parts = [p.strip() for p in weighted_mix.split(",")]
|
|
826
|
+
pcts = [int(p.split(":")[0]) for p in parts if p]
|
|
827
|
+
total = sum(pcts) or 1
|
|
828
|
+
chunk = max(1, (len(samplers) + len(pcts) - 1) // len(pcts))
|
|
829
|
+
for gi, pct in enumerate(pcts):
|
|
830
|
+
for si in range(gi * chunk, min((gi + 1) * chunk, len(samplers))):
|
|
831
|
+
task_weights[si] = max(1, round(pct * 10 / total))
|
|
832
|
+
except ValueError:
|
|
833
|
+
task_weights = [1] * len(samplers)
|
|
834
|
+
|
|
835
|
+
def _slug(name: str, idx: int) -> str:
|
|
836
|
+
words = re.sub(r"[^a-zA-Z0-9]+", "_", name).strip("_").lower() or f"endpoint_{idx}"
|
|
837
|
+
return f"task_{idx + 1:02d}_{words[:40]}"
|
|
838
|
+
|
|
839
|
+
def _locust_body(raw: str) -> str:
|
|
840
|
+
"""Converts JMeter placeholders into a runtime-safe .format() body."""
|
|
841
|
+
if not raw:
|
|
842
|
+
return ""
|
|
843
|
+
s = raw.replace("${__time(HMS,)}", "\x01ts\x01")
|
|
844
|
+
s = s.replace("${RANDOM_ITEM_ID}", "\x01rnd\x01")
|
|
845
|
+
s = s.replace("${LAST_CREATED_ID}", "\x01last_id\x01")
|
|
846
|
+
s = re.sub(r"\$\{([A-Za-z0-9_]+)\}", lambda m: "sample_" + m.group(1).lower(), s)
|
|
847
|
+
s = s.replace("{", "{{").replace("}", "}}")
|
|
848
|
+
s = s.replace("\x01ts\x01", "{ts}").replace("\x01rnd\x01", "{rnd}").replace("\x01last_id\x01", "{last_id}")
|
|
849
|
+
return s
|
|
850
|
+
|
|
851
|
+
method_lines: List[str] = []
|
|
852
|
+
for idx, s in enumerate(samplers):
|
|
853
|
+
fn = _slug(s.get("name", f"{s.get('method')} {s.get('path')}"), idx)
|
|
854
|
+
weight = task_weights[idx] if idx < len(task_weights) else 1
|
|
855
|
+
method = (s.get("method") or "GET").upper()
|
|
856
|
+
path = (s.get("path") or "/").replace('"', '\\"')
|
|
857
|
+
method_lines.append(f" @task({weight})")
|
|
858
|
+
method_lines.append(f" def {fn}(self):")
|
|
859
|
+
method_lines.append(f' """{s.get("name", method + " " + path)}"""')
|
|
860
|
+
if method in ("POST", "PUT", "PATCH"):
|
|
861
|
+
method_lines.append(
|
|
862
|
+
f' self.client.{method.lower()}("{path}", '
|
|
863
|
+
f'name="{method} {path}", '
|
|
864
|
+
f'json=json.loads(BODIES["{fn}"].format(ts=datetime.now(timezone.utc).strftime("%H%M%S"), rnd=random.randint(1, 9999), last_id=LAST_CREATED_ID)))'
|
|
865
|
+
)
|
|
866
|
+
else:
|
|
867
|
+
method_lines.append(f' self.client.get("{path}", name="{method} {path}")')
|
|
868
|
+
method_lines.append("")
|
|
869
|
+
|
|
870
|
+
think_time = (
|
|
871
|
+
f"between({think_time_min_ms / 1000:.2f}, {think_time_max_ms / 1000:.2f})"
|
|
872
|
+
if think_time_max_ms > 0
|
|
873
|
+
else "constant_pacing(1.0)"
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
bodies = "\n".join(
|
|
877
|
+
f' "{_slug(s.get("name", ""), i)}": {_locust_body(s.get("body", ""))!r},'
|
|
878
|
+
for i, s in enumerate(samplers)
|
|
879
|
+
if (s.get("method") or "").upper() in ("POST", "PUT", "PATCH")
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
class_name = re.sub(r"[^A-Za-z0-9]+", "", plan_name.title()) or "EnterpriseUser"
|
|
883
|
+
|
|
884
|
+
return (
|
|
885
|
+
'"""Generated by Synapse Hub tools/generate_jmx_plan.py --emit-locust\n'
|
|
886
|
+
"Code-as-code load model (concept borrowed from Locust, MIT).\n"
|
|
887
|
+
"Run: locust -f this_file.py --host=HOST\n"
|
|
888
|
+
'"""\n'
|
|
889
|
+
"import json\n"
|
|
890
|
+
"import random\n"
|
|
891
|
+
"from datetime import datetime, timezone\n\n"
|
|
892
|
+
"from locust import HttpUser, task, between, constant_pacing\n\n"
|
|
893
|
+
"LAST_CREATED_ID = 1\n\n"
|
|
894
|
+
"BODIES = {\n"
|
|
895
|
+
f"{bodies}\n"
|
|
896
|
+
"}\n\n\n"
|
|
897
|
+
f"class {class_name}(HttpUser):\n"
|
|
898
|
+
f' """{plan_name}"""\n\n'
|
|
899
|
+
f" host = \"{host}\"\n"
|
|
900
|
+
f" wait_time = {think_time}\n\n"
|
|
901
|
+
+ "\n".join(method_lines)
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
|
|
729
905
|
def parse_openapi_for_jmeter(spec_data: Dict[str, Any]) -> tuple:
|
|
730
906
|
"""Extracts base configuration and samplers list from OpenAPI spec."""
|
|
731
907
|
info_dict = spec_data.get("info", {})
|
|
@@ -929,6 +1105,7 @@ def main():
|
|
|
929
1105
|
parser.add_argument("--loop-count", type=int, default=-1, help="Loop iterations count (-1 for infinite). Default: -1.")
|
|
930
1106
|
parser.add_argument("--sla-p95", type=int, default=500, help="P95 Latency SLA assertion threshold in ms. Default: 500.")
|
|
931
1107
|
parser.add_argument("--output", "-o", help="Target output JMeter test plan file path (.jmx).")
|
|
1108
|
+
parser.add_argument("--emit-locust", dest="emit_locust", help="Also/instead emit a runnable Locust locustfile.py (code-as-code load model).")
|
|
932
1109
|
parser.add_argument("--sample", action="store_true", help="Generate a sample load test plan using built-in enterprise schema.")
|
|
933
1110
|
|
|
934
1111
|
# Advanced JMeter 360° Extensions
|
|
@@ -1039,6 +1216,28 @@ def main():
|
|
|
1039
1216
|
else:
|
|
1040
1217
|
base_host = args.base_url
|
|
1041
1218
|
|
|
1219
|
+
# Code-as-code Locust export (concept borrowed from Locust, MIT)
|
|
1220
|
+
if args.emit_locust:
|
|
1221
|
+
locust_source = generate_locustfile(
|
|
1222
|
+
plan_name=plan_name,
|
|
1223
|
+
base_host=base_host,
|
|
1224
|
+
protocol=protocol,
|
|
1225
|
+
port=port,
|
|
1226
|
+
samplers=samplers,
|
|
1227
|
+
weighted_mix=args.weighted_mix,
|
|
1228
|
+
think_time_min_ms=args.think_time_min,
|
|
1229
|
+
think_time_max_ms=args.think_time_max,
|
|
1230
|
+
)
|
|
1231
|
+
out_dir = os.path.dirname(os.path.abspath(args.emit_locust))
|
|
1232
|
+
if out_dir and not os.path.exists(out_dir):
|
|
1233
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
1234
|
+
with open(args.emit_locust, "w", encoding="utf-8") as f:
|
|
1235
|
+
f.write(locust_source)
|
|
1236
|
+
print(f"🐍 [LOCUST EXPORT] Runnable locustfile written: {args.emit_locust}")
|
|
1237
|
+
print(f" Run: locust -f \"{args.emit_locust}\" --host={protocol}://{base_host}:{port}")
|
|
1238
|
+
if not args.output:
|
|
1239
|
+
return
|
|
1240
|
+
|
|
1042
1241
|
jmx_content = build_jmx_xml(
|
|
1043
1242
|
plan_name=plan_name,
|
|
1044
1243
|
base_host=base_host,
|
|
@@ -1,135 +1,13 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
New-Item -ItemType Directory -Path $testReportsDir -Force | Out-Null
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
$passed = 0
|
|
17
|
-
$failures = 0
|
|
18
|
-
$errors = 0
|
|
19
|
-
$incomplete = 0
|
|
20
|
-
$skipped = 0
|
|
21
|
-
$foundExistingSummary = $false
|
|
22
|
-
|
|
23
|
-
# 0. Check if Cleanup_Browser already exported summaryTemp.txt with business test case counts
|
|
24
|
-
$existingSummaryFile = Join-Path $reportsDir "summaryTemp.txt"
|
|
25
|
-
if (-not (Test-Path $existingSummaryFile)) {
|
|
26
|
-
$existingSummaryFile = Join-Path $testReportsDir "summaryTemp.txt"
|
|
27
|
-
}
|
|
28
|
-
if (Test-Path $existingSummaryFile) {
|
|
29
|
-
try {
|
|
30
|
-
$rawSummary = [System.IO.File]::ReadAllText($existingSummaryFile, [System.Text.Encoding]::UTF8)
|
|
31
|
-
if ($rawSummary -match "(\d+)\s+Passed\s+-\s+.*?(\d+)\s+Failures\s+-\s+.*?(\d+)\s+Errors") {
|
|
32
|
-
$passed = [int]$matches[1]
|
|
33
|
-
$failures = [int]$matches[2]
|
|
34
|
-
$errors = [int]$matches[3]
|
|
35
|
-
if ($passed -gt 0) {
|
|
36
|
-
$foundExistingSummary = $true
|
|
37
|
-
Write-Host "[+] Found valid summaryTemp.txt from Cleanup_Browser: $rawSummary" -ForegroundColor Green
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
} catch {}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
# 1. Find the latest report directory (check Reports, then TestReports)
|
|
44
|
-
$latestReport = Get-ChildItem -Path @($reportsDir, $testReportsDir) -Recurse -Filter "*junit*.xml" -ErrorAction SilentlyContinue |
|
|
45
|
-
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
46
|
-
|
|
47
|
-
if (-not $foundExistingSummary -and $latestReport) {
|
|
48
|
-
Write-Host "[+] Found latest JUnit report: $($latestReport.FullName)" -ForegroundColor Green
|
|
49
|
-
try {
|
|
50
|
-
[xml]$xml = Get-Content $latestReport.FullName
|
|
51
|
-
if ($xml.testsuites) {
|
|
52
|
-
$passed = [int]($xml.testsuites.passed)
|
|
53
|
-
$failures = [int]($xml.testsuites.failures)
|
|
54
|
-
$errors = [int]($xml.testsuites.errors)
|
|
55
|
-
$incomplete = [int]($xml.testsuites.incompleted)
|
|
56
|
-
$skipped = [int]($xml.testsuites.skipped)
|
|
57
|
-
} elseif ($xml.testsuite) {
|
|
58
|
-
$tests = [int]($xml.testsuite.tests)
|
|
59
|
-
$failures = [int]($xml.testsuite.failures)
|
|
60
|
-
$errors = [int]($xml.testsuite.errors)
|
|
61
|
-
$skipped = [int]($xml.testsuite.skipped)
|
|
62
|
-
$passed = [Math]::Max(0, $tests - $failures - $errors - $skipped)
|
|
63
|
-
}
|
|
64
|
-
$foundExistingSummary = $true
|
|
65
|
-
} catch {
|
|
66
|
-
Write-Warning "[!] Failed to parse XML: $_"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (-not $foundExistingSummary) {
|
|
71
|
-
# Fallback to console0.log
|
|
72
|
-
$latestLog = Get-ChildItem -Path @($reportsDir, $testReportsDir) -Recurse -Filter "console0.log" -ErrorAction SilentlyContinue |
|
|
73
|
-
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
74
|
-
|
|
75
|
-
if ($latestLog) {
|
|
76
|
-
Write-Host "[+] Fallback: Parsing console log: $($latestLog.FullName)" -ForegroundColor Yellow
|
|
77
|
-
$logContent = Get-Content $latestLog.FullName -Raw
|
|
78
|
-
if ($logContent -match "Passed:\s*(\d+)\s*\|\s*Failed:\s*(\d+)\s*\|\s*Errors:\s*(\d+)") {
|
|
79
|
-
$passed = [int]$matches[1]
|
|
80
|
-
$failures = [int]$matches[2]
|
|
81
|
-
$errors = [int]$matches[3]
|
|
82
|
-
} elseif ($logContent -match "Tổng số:\s*(\d+)\s*\|\s*Đạt:\s*(\d+)\s*\|\s*Thất bại:\s*(\d+)\s*\|\s*Lỗi:\s*(\d+)") {
|
|
83
|
-
$passed = [int]$matches[2]
|
|
84
|
-
$failures = [int]$matches[3]
|
|
85
|
-
$errors = [int]$matches[4]
|
|
86
|
-
} else {
|
|
87
|
-
$passed = ([regex]::Matches($logContent, "\[TEST_CASE\]\[PASSED\]")).Count
|
|
88
|
-
$failures = ([regex]::Matches($logContent, "\[TEST_CASE\]\[FAILED\]")).Count
|
|
89
|
-
$errors = ([regex]::Matches($logContent, "\[TEST_CASE\]\[ERROR\]")).Count
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
Write-Host "[+] Metrics: Passed=$passed, Failures=$failures, Errors=$errors, Incomplete=$incomplete, Skipped=$skipped" -ForegroundColor Cyan
|
|
95
|
-
|
|
96
|
-
# 2. Write summaryTemp.txt: \u2705 {P} Passed - \u274C {F} Failures - \u274C {E} Errors - \u2757 {I} Incomplete - \u26AA {S} Skipped
|
|
97
|
-
$summaryContent = "\u2705 $passed Passed - \u274C $failures Failures - \u274C $errors Errors - \u2757 $incomplete Incomplete - \u26AA $skipped Skipped"
|
|
98
|
-
|
|
99
|
-
$summaryFile = Join-Path $testReportsDir "summaryTemp.txt"
|
|
100
|
-
[System.IO.File]::WriteAllText($summaryFile, $summaryContent, [System.Text.Encoding]::ASCII)
|
|
101
|
-
Write-Host "[+] Created: $summaryFile" -ForegroundColor Green
|
|
102
|
-
Write-Host " $summaryContent"
|
|
103
|
-
|
|
104
|
-
# Also write to Reports/summaryTemp.txt for fallback
|
|
105
|
-
if (Test-Path $reportsDir) {
|
|
106
|
-
[System.IO.File]::WriteAllText((Join-Path $reportsDir "summaryTemp.txt"), $summaryContent, [System.Text.Encoding]::ASCII)
|
|
1
|
+
# Enterprise Automation - Test Reports Aggregator Wrapper
|
|
2
|
+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
3
|
+
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
|
|
4
|
+
if (-not $pythonCmd) { $pythonCmd = Get-Command py -ErrorAction SilentlyContinue }
|
|
5
|
+
if (-not $pythonCmd) { $pythonCmd = Get-Command python3 -ErrorAction SilentlyContinue }
|
|
6
|
+
|
|
7
|
+
if ($pythonCmd) {
|
|
8
|
+
& $pythonCmd.Source "$scriptDir\generate_test_reports.py" $args
|
|
9
|
+
exit $LASTEXITCODE
|
|
10
|
+
} else {
|
|
11
|
+
Write-Error "Python 3 is required to run generate_test_reports.py."
|
|
12
|
+
exit 1
|
|
107
13
|
}
|
|
108
|
-
|
|
109
|
-
# 3. Synchronize report files into TestReports/<Project>_<timestamp>/ if Katalon saved to Reports/
|
|
110
|
-
if ($latestReport) {
|
|
111
|
-
$reportParentDir = $latestReport.Directory.FullName
|
|
112
|
-
$nowStr = Get-Date -Format "yyyyMMddHHmmss"
|
|
113
|
-
if ($ProjectName -match "_\d{10,14}$") {
|
|
114
|
-
$targetProjDir = Join-Path $testReportsDir $ProjectName
|
|
115
|
-
} else {
|
|
116
|
-
$targetProjDir = Join-Path $testReportsDir "${ProjectName}_$nowStr"
|
|
117
|
-
}
|
|
118
|
-
$targetSubDir = Join-Path $targetProjDir (Split-Path $reportParentDir -Leaf)
|
|
119
|
-
|
|
120
|
-
if (-not (Test-Path $targetSubDir)) {
|
|
121
|
-
New-Item -ItemType Directory -Path $targetSubDir -Force | Out-Null
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
Copy-Item -Path "$reportParentDir\*" -Destination $targetSubDir -Recurse -Force
|
|
125
|
-
Write-Host "[+] Synced artifacts to: $targetSubDir" -ForegroundColor Green
|
|
126
|
-
|
|
127
|
-
# Ensure results.html exists in targetProjDir
|
|
128
|
-
$htmlFiles = Get-ChildItem -Path $targetSubDir -Filter "*.html"
|
|
129
|
-
if ($htmlFiles.Count -gt 0) {
|
|
130
|
-
Copy-Item -Path $htmlFiles[0].FullName -Destination (Join-Path $targetProjDir "results.html") -Force
|
|
131
|
-
Write-Host "[+] Created results.html in: $targetProjDir" -ForegroundColor Green
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
Write-Host "=== [Centralize CI/CD] Report Generation Complete ===" -ForegroundColor Cyan
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise Test Report Aggregator & summaryTemp Generator (Cross-Platform)
|
|
4
|
+
-------------------------------------------------------------------------
|
|
5
|
+
Parses JUnit XML and Allure test reports across target directories
|
|
6
|
+
and exports standardized summaryTemp.txt for CI/CD dashboards.
|
|
7
|
+
Replaces PowerShell-only script with a zero-dependency, cross-platform engine.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import glob
|
|
13
|
+
import argparse
|
|
14
|
+
import xml.etree.ElementTree as ET
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_latest_junit_report(search_dirs):
|
|
21
|
+
"""Finds the most recent JUnit XML file across the given search directories."""
|
|
22
|
+
candidates = []
|
|
23
|
+
for d in search_dirs:
|
|
24
|
+
if os.path.exists(d):
|
|
25
|
+
for pattern in ["**/*junit*.xml", "**/*test*.xml", "**/TEST-*.xml", "**/*.xml"]:
|
|
26
|
+
for p in glob.glob(os.path.join(d, pattern), recursive=True):
|
|
27
|
+
try:
|
|
28
|
+
mtime = os.path.getmtime(p)
|
|
29
|
+
candidates.append((mtime, p))
|
|
30
|
+
except OSError:
|
|
31
|
+
pass
|
|
32
|
+
if not candidates:
|
|
33
|
+
return None
|
|
34
|
+
candidates.sort(key=lambda x: x[0], reverse=True)
|
|
35
|
+
return candidates[0][1]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_junit_xml(xml_path):
|
|
39
|
+
"""Parses a JUnit XML file and returns (passed, failures, errors, skipped)."""
|
|
40
|
+
try:
|
|
41
|
+
tree = ET.parse(xml_path)
|
|
42
|
+
root = tree.getroot()
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"[WARN] Failed to parse XML file {xml_path}: {e}")
|
|
45
|
+
return 0, 0, 0, 0
|
|
46
|
+
|
|
47
|
+
total_tests = 0
|
|
48
|
+
failures = 0
|
|
49
|
+
errors = 0
|
|
50
|
+
skipped = 0
|
|
51
|
+
|
|
52
|
+
if root.tag == "testsuites":
|
|
53
|
+
total_tests = int(root.attrib.get("tests", 0))
|
|
54
|
+
failures = int(root.attrib.get("failures", 0))
|
|
55
|
+
errors = int(root.attrib.get("errors", 0))
|
|
56
|
+
skipped = int(root.attrib.get("skipped", 0))
|
|
57
|
+
passed_attr = root.attrib.get("passed")
|
|
58
|
+
if passed_attr is not None:
|
|
59
|
+
return int(passed_attr), failures, errors, skipped
|
|
60
|
+
elif root.tag == "testsuite":
|
|
61
|
+
total_tests = int(root.attrib.get("tests", 0))
|
|
62
|
+
failures = int(root.attrib.get("failures", 0))
|
|
63
|
+
errors = int(root.attrib.get("errors", 0))
|
|
64
|
+
skipped = int(root.attrib.get("skipped", 0))
|
|
65
|
+
else:
|
|
66
|
+
# Count testcase elements
|
|
67
|
+
for tc in root.iter("testcase"):
|
|
68
|
+
total_tests += 1
|
|
69
|
+
if tc.find("failure") is not None:
|
|
70
|
+
failures += 1
|
|
71
|
+
elif tc.find("error") is not None:
|
|
72
|
+
errors += 1
|
|
73
|
+
elif tc.find("skipped") is not None:
|
|
74
|
+
skipped += 1
|
|
75
|
+
|
|
76
|
+
passed = max(0, total_tests - (failures + errors + skipped))
|
|
77
|
+
return passed, failures, errors, skipped
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def generate_reports(target_dir=None, project_name="Enterprise-Automation"):
|
|
81
|
+
"""Aggregates reports and outputs summaryTemp.txt."""
|
|
82
|
+
root_dir = target_dir or PROJECT_ROOT
|
|
83
|
+
reports_dir = os.path.join(root_dir, "Reports")
|
|
84
|
+
test_reports_dir = os.path.join(root_dir, "TestReports")
|
|
85
|
+
|
|
86
|
+
os.makedirs(test_reports_dir, exist_ok=True)
|
|
87
|
+
summary_file = os.path.join(test_reports_dir, "summaryTemp.txt")
|
|
88
|
+
|
|
89
|
+
# Check if summary already exists
|
|
90
|
+
for check_dir in [reports_dir, test_reports_dir]:
|
|
91
|
+
existing = os.path.join(check_dir, "summaryTemp.txt")
|
|
92
|
+
if os.path.exists(existing):
|
|
93
|
+
try:
|
|
94
|
+
with open(existing, "r", encoding="utf-8") as f:
|
|
95
|
+
content = f.read().strip()
|
|
96
|
+
if "Passed" in content:
|
|
97
|
+
print(f"[+] Found existing summary file: {existing}")
|
|
98
|
+
return 0
|
|
99
|
+
except OSError:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
latest_xml = find_latest_junit_report([reports_dir, test_reports_dir, root_dir])
|
|
103
|
+
if latest_xml:
|
|
104
|
+
print(f"[+] Parsing latest JUnit XML report: {latest_xml}")
|
|
105
|
+
passed, failures, errors, skipped = parse_junit_xml(latest_xml)
|
|
106
|
+
else:
|
|
107
|
+
print("[!] No JUnit XML report found on disk. Initializing zero-baseline.")
|
|
108
|
+
passed, failures, errors, skipped = 0, 0, 0, 0
|
|
109
|
+
|
|
110
|
+
summary_text = (
|
|
111
|
+
f"Summary: {passed} Passed - {failures} Failures - {errors} Errors - {skipped} Skipped\n"
|
|
112
|
+
f"Project: {project_name}\n"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
with open(summary_file, "w", encoding="utf-8") as f:
|
|
116
|
+
f.write(summary_text)
|
|
117
|
+
|
|
118
|
+
print(f"[+] Summary report saved to: {summary_file}")
|
|
119
|
+
print(f" {summary_text.strip()}")
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main():
|
|
124
|
+
parser = argparse.ArgumentParser(description="Aggregate test reports and generate summaryTemp.txt")
|
|
125
|
+
parser.add_argument("target", nargs="?", default=None, help="Target project root directory")
|
|
126
|
+
parser.add_argument("--project-name", default="Enterprise-Automation", help="Project label for summary")
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
sys.exit(generate_reports(args.target, project_name=args.project_name))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
main()
|