synapse-hub 2.1.0 → 2.2.0
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/orchestrator.md +7 -0
- package/.agents/rules/lessons-learned.md +1 -0
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +1 -1
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +1 -2
- package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +11 -4
- package/.agents/skills/architecture-decision-records/SKILL.md +1 -1
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +32 -11
- package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/clean-pom-crafter/SKILL.md +1 -4
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +1 -1
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +1 -1
- package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +1 -3
- package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +1 -1
- package/.agents/skills/github-actions-templates/SKILL.md +1 -1
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +1 -3
- package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
- package/.agents/skills/lam-script-generator/SKILL.md +1 -2
- package/.agents/skills/playwright-live-interaction/SKILL.md +1 -4
- package/.agents/skills/security-dast-sentinel/SKILL.md +1 -1
- package/.agents/skills/self-healing-locator/SKILL.md +1 -2
- package/.agents/skills/skillspector-security-guard/SKILL.md +115 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +1 -1
- package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
- package/.agents/skills/visual-regression-sentinel/SKILL.md +1 -1
- package/.agents/skills/wcag-audit-patterns/SKILL.md +1 -1
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
- package/AGENTS.md +36 -0
- package/README.md +140 -103
- package/package.json +1 -1
- package/skills_manifest.json +42 -29
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +15 -1
- package/templates/ai_bridges/CLAUDE.md +31 -0
- package/templates/ai_bridges/copilot-instructions.md +25 -0
- package/templates/ai_bridges/cursor_synapse_qa.mdc +26 -0
- package/templates/ai_bridges/windsurfrules +25 -0
- package/templates/ci/github-actions-synapse.yml +28 -0
- package/templates/ci/gitlab-ci-synapse.yml +16 -0
- package/tools/TOOLS_RUNBOOK.md +203 -12
- package/tools/browser_helpers.js +2 -2
- package/tools/bump_version.py +8 -0
- package/tools/cli.py +74 -2
- package/tools/export_postman_collection.py +535 -218
- package/tools/generate_jmx_plan.py +672 -102
- package/tools/git_pre_commit_guard.py +21 -0
- package/tools/jtl_analyzer.py +474 -0
- package/tools/mcp_excel_server.ps1 +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/newman_analyzer.py +385 -0
- package/tools/setup_project.py +203 -18
- package/tools/skillspector.py +1141 -0
- package/tools/telemetry_correlator.py +351 -0
- package/tools/templates/jmeter-distributed/README.md +54 -0
- package/tools/templates/jmeter-distributed/docker-compose.yml +83 -0
- package/tools/update_sentinel.py +313 -0
- package/tools/repo_map.json +0 -12
- package/tools/repo_map.txt +0 -7
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
tools/generate_jmx_plan.py — Enterprise Apache JMeter 5.x Test Plan XML Generator
|
|
5
|
-
|
|
4
|
+
tools/generate_jmx_plan.py — Enterprise Apache JMeter 5.x Test Plan XML Generator (JMeter 360° Pinnacle)
|
|
5
|
+
---------------------------------------------------------------------------------------------------------
|
|
6
6
|
Transforms OpenAPI / Swagger specifications, Postman Collections (v2.1.0), or HAR
|
|
7
|
-
traces into production-ready Apache JMeter (.jmx) load and
|
|
8
|
-
|
|
9
|
-
Features:
|
|
10
|
-
1. Multi-Format Ingestion:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
5.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
traces into production-ready Apache JMeter (.jmx) load, stress, spike, and soak test plans.
|
|
8
|
+
|
|
9
|
+
Comprehensive 10/10 Pinnacle Performance Features:
|
|
10
|
+
1. Multi-Format Ingestion: OpenAPI 3.0 / Swagger 2.0, Postman Collection JSON, and HAR traces.
|
|
11
|
+
2. Browser State Simulation: Injects HTTP Cookie Manager & HTTP Cache Manager by default.
|
|
12
|
+
3. Dynamic Parameterization: Injects CSV Data Set Config for multi-user test data.
|
|
13
|
+
4. Advanced Logic Controllers:
|
|
14
|
+
- Transaction Controllers (Parent aggregations for end-to-end SLA tracking)
|
|
15
|
+
- Traffic Shaping & Weighted Routing via ThroughputController (Pareto 70/20/10 traffic mix)
|
|
16
|
+
- Uniform Random Timers for realistic human think-time.
|
|
17
|
+
5. Deep Data Correlation Engine:
|
|
18
|
+
- RegexExtractor for HTML hidden CSRF / Anti-Forgery tokens
|
|
19
|
+
- JSONPostProcessor with Match Number 0 (Random dynamic item choice avoiding duplicate lockups)
|
|
20
|
+
- Chaining Bearer JWT token & entity IDs.
|
|
21
|
+
6. Workload Archetypes: Linear, Step-Up (Break-point), Spike, and Soak profiles.
|
|
22
|
+
7. Enterprise Security: JSR223 Groovy HMAC-SHA256 payload signer & correlation IDs.
|
|
23
|
+
8. Full-Stack Observability & Telemetry:
|
|
24
|
+
- InfluxDB Backend Listener for live Grafana APM dashboard streaming
|
|
25
|
+
- PerfMon Server Agent Collector (CPU, RAM, Disks, Network I/O).
|
|
26
|
+
9. Multi-Protocol Coverage (100% Comprehensive):
|
|
27
|
+
- REST / HTTP(S) Samplers
|
|
28
|
+
- JDBC Database Connection Pool & Query Samplers
|
|
29
|
+
- WebSocket Real-Time Samplers (Connect, Send, Receive, Close)
|
|
30
|
+
- gRPC Microservices Samplers (Protobuf & Unary RPC)
|
|
31
|
+
- Kafka / JMS Event-Driven Producer Samplers.
|
|
21
32
|
|
|
22
33
|
Usage:
|
|
23
|
-
python tools/generate_jmx_plan.py --spec swagger.json -o perf_plan.jmx --
|
|
24
|
-
python tools/generate_jmx_plan.py --postman
|
|
25
|
-
python tools/generate_jmx_plan.py --sample -o
|
|
34
|
+
python tools/generate_jmx_plan.py --spec swagger.json -o perf_plan.jmx --weighted-mix "70,20,10"
|
|
35
|
+
python tools/generate_jmx_plan.py --postman col.json -o perf_plan.jmx --csv-data users.csv --deep-correlation
|
|
36
|
+
python tools/generate_jmx_plan.py --ws-sample -o websocket_plan.jmx
|
|
37
|
+
python tools/generate_jmx_plan.py --grpc-sample -o grpc_plan.jmx
|
|
38
|
+
python tools/generate_jmx_plan.py --kafka-sample -o kafka_plan.jmx
|
|
39
|
+
python tools/generate_jmx_plan.py --jdbc-sample -o jdbc_plan.jmx
|
|
26
40
|
"""
|
|
27
41
|
|
|
28
42
|
import os
|
|
29
43
|
import sys
|
|
30
44
|
import json
|
|
45
|
+
import re
|
|
31
46
|
import argparse
|
|
32
|
-
from typing import Dict, Any, List, Optional
|
|
47
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
33
48
|
from urllib.parse import urlparse
|
|
34
49
|
from xml.sax.saxutils import escape as xml_escape
|
|
35
50
|
|
|
@@ -52,7 +67,24 @@ def build_jmx_xml(
|
|
|
52
67
|
duration: int,
|
|
53
68
|
loop_count: int,
|
|
54
69
|
sla_p95: int,
|
|
55
|
-
samplers: List[Dict[str, Any]]
|
|
70
|
+
samplers: List[Dict[str, Any]],
|
|
71
|
+
enable_cookies: bool = True,
|
|
72
|
+
enable_cache: bool = True,
|
|
73
|
+
csv_file: Optional[str] = None,
|
|
74
|
+
csv_variables: str = "username,password",
|
|
75
|
+
group_transactions: bool = False,
|
|
76
|
+
weighted_mix: Optional[str] = None,
|
|
77
|
+
deep_correlation: bool = True,
|
|
78
|
+
workload_profile: str = "linear",
|
|
79
|
+
jsr223_hmac: bool = False,
|
|
80
|
+
influxdb_url: Optional[str] = None,
|
|
81
|
+
perfmon_server: Optional[str] = None,
|
|
82
|
+
think_time_min: int = 0,
|
|
83
|
+
think_time_max: int = 0,
|
|
84
|
+
jdbc_config: Optional[Dict[str, Any]] = None,
|
|
85
|
+
ws_config: Optional[Dict[str, Any]] = None,
|
|
86
|
+
grpc_config: Optional[Dict[str, Any]] = None,
|
|
87
|
+
kafka_config: Optional[Dict[str, Any]] = None
|
|
56
88
|
) -> str:
|
|
57
89
|
"""Builds valid, production-grade Apache JMeter 5.x XML Test Plan."""
|
|
58
90
|
|
|
@@ -64,7 +96,7 @@ def build_jmx_xml(
|
|
|
64
96
|
'<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">',
|
|
65
97
|
' <hashTree>',
|
|
66
98
|
f' <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="{xml_escape(plan_name)}" enabled="true">',
|
|
67
|
-
' <stringProp name="TestPlan.comments">
|
|
99
|
+
' <stringProp name="TestPlan.comments">Enterprise Performance Test Plan auto-generated by Synapse-Hub</stringProp>',
|
|
68
100
|
' <boolProp name="TestPlan.functional_mode">false</boolProp>',
|
|
69
101
|
' <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>',
|
|
70
102
|
' <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>',
|
|
@@ -95,11 +127,54 @@ def build_jmx_xml(
|
|
|
95
127
|
' <stringProp name="Argument.value"></stringProp>',
|
|
96
128
|
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
97
129
|
' </elementProp>',
|
|
130
|
+
' <elementProp name="CSRF_TOKEN" elementType="Argument">',
|
|
131
|
+
' <stringProp name="Argument.name">CSRF_TOKEN</stringProp>',
|
|
132
|
+
' <stringProp name="Argument.value"></stringProp>',
|
|
133
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
134
|
+
' </elementProp>',
|
|
135
|
+
' <elementProp name="RANDOM_ITEM_ID" elementType="Argument">',
|
|
136
|
+
' <stringProp name="Argument.name">RANDOM_ITEM_ID</stringProp>',
|
|
137
|
+
' <stringProp name="Argument.value">1</stringProp>',
|
|
138
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
139
|
+
' </elementProp>',
|
|
140
|
+
' <elementProp name="HMAC_SECRET" elementType="Argument">',
|
|
141
|
+
' <stringProp name="Argument.name">HMAC_SECRET</stringProp>',
|
|
142
|
+
' <stringProp name="Argument.value">SynapseEnterpriseVault#2026</stringProp>',
|
|
143
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
144
|
+
' </elementProp>',
|
|
98
145
|
' </collectionProp>',
|
|
99
146
|
' </elementProp>',
|
|
100
147
|
' <stringProp name="TestPlan.user_define_classpath"></stringProp>',
|
|
101
148
|
' </TestPlan>',
|
|
102
|
-
' <hashTree>'
|
|
149
|
+
' <hashTree>'
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
# Global HTTP Cookie Manager
|
|
153
|
+
if enable_cookies:
|
|
154
|
+
xml_lines.extend([
|
|
155
|
+
' <!-- Global Cookie Manager (Browser Session Emulation) -->',
|
|
156
|
+
' <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true">',
|
|
157
|
+
' <collectionProp name="CookieManager.cookies"/>',
|
|
158
|
+
' <boolProp name="CookieManager.clearEachIteration">false</boolProp>',
|
|
159
|
+
' <boolProp name="CookieManager.controlledByThreadGroup">false</boolProp>',
|
|
160
|
+
' </CookieManager>',
|
|
161
|
+
' <hashTree/>'
|
|
162
|
+
])
|
|
163
|
+
|
|
164
|
+
# Global HTTP Cache Manager
|
|
165
|
+
if enable_cache:
|
|
166
|
+
xml_lines.extend([
|
|
167
|
+
' <!-- Global Cache Manager (ETag & Cache-Control Emulation) -->',
|
|
168
|
+
' <CacheManager guiclass="CacheManagerGui" testclass="CacheManager" testname="HTTP Cache Manager" enabled="true">',
|
|
169
|
+
' <boolProp name="clearEachIteration">false</boolProp>',
|
|
170
|
+
' <boolProp name="useCacheControl">true</boolProp>',
|
|
171
|
+
' <intProp name="maxSize">5000</intProp>',
|
|
172
|
+
' </CacheManager>',
|
|
173
|
+
' <hashTree/>'
|
|
174
|
+
])
|
|
175
|
+
|
|
176
|
+
# Global Header Manager
|
|
177
|
+
xml_lines.extend([
|
|
103
178
|
' <!-- Global Header Manager -->',
|
|
104
179
|
' <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">',
|
|
105
180
|
' <collectionProp name="HeaderManager.headers">',
|
|
@@ -115,6 +190,10 @@ def build_jmx_xml(
|
|
|
115
190
|
' <stringProp name="Header.name">Authorization</stringProp>',
|
|
116
191
|
' <stringProp name="Header.value">Bearer ${BEARER_TOKEN}</stringProp>',
|
|
117
192
|
' </elementProp>',
|
|
193
|
+
' <elementProp name="" elementType="Header">',
|
|
194
|
+
' <stringProp name="Header.name">X-CSRF-Token</stringProp>',
|
|
195
|
+
' <stringProp name="Header.value">${CSRF_TOKEN}</stringProp>',
|
|
196
|
+
' </elementProp>',
|
|
118
197
|
' </collectionProp>',
|
|
119
198
|
' </HeaderManager>',
|
|
120
199
|
' <hashTree/>',
|
|
@@ -127,10 +206,144 @@ def build_jmx_xml(
|
|
|
127
206
|
' <stringProp name="HTTPSampler.port">${PORT}</stringProp>',
|
|
128
207
|
' <stringProp name="HTTPSampler.protocol">${PROTOCOL}</stringProp>',
|
|
129
208
|
' <stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>',
|
|
209
|
+
' <stringProp name="HTTPSampler.connect_timeout">5000</stringProp>',
|
|
210
|
+
' <stringProp name="HTTPSampler.response_timeout">30000</stringProp>',
|
|
130
211
|
' </ConfigTestElement>',
|
|
131
|
-
' <hashTree/>'
|
|
132
|
-
|
|
133
|
-
|
|
212
|
+
' <hashTree/>'
|
|
213
|
+
])
|
|
214
|
+
|
|
215
|
+
# InfluxDB Backend Listener for live Grafana streaming
|
|
216
|
+
if influxdb_url:
|
|
217
|
+
xml_lines.extend([
|
|
218
|
+
' <!-- InfluxDB Live Telemetry Backend Listener (Grafana APM) -->',
|
|
219
|
+
' <BackendListener guiclass="BackendListenerGui" testclass="BackendListener" testname="Backend Listener - InfluxDB" enabled="true">',
|
|
220
|
+
' <elementProp name="arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" enabled="true">',
|
|
221
|
+
' <collectionProp name="Arguments.arguments">',
|
|
222
|
+
' <elementProp name="influxdbMetricsSender" elementType="Argument">',
|
|
223
|
+
' <stringProp name="Argument.name">influxdbMetricsSender</stringProp>',
|
|
224
|
+
' <stringProp name="Argument.value">org.apache.jmeter.visualizers.backend.influxdb.HttpMetricsSender</stringProp>',
|
|
225
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
226
|
+
' </elementProp>',
|
|
227
|
+
' <elementProp name="influxdbUrl" elementType="Argument">',
|
|
228
|
+
' <stringProp name="Argument.name">influxdbUrl</stringProp>',
|
|
229
|
+
f' <stringProp name="Argument.value">{xml_escape(influxdb_url)}</stringProp>',
|
|
230
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
231
|
+
' </elementProp>',
|
|
232
|
+
' <elementProp name="application" elementType="Argument">',
|
|
233
|
+
' <stringProp name="Argument.name">application</stringProp>',
|
|
234
|
+
f' <stringProp name="Argument.value">{xml_escape(plan_name)}</stringProp>',
|
|
235
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
236
|
+
' </elementProp>',
|
|
237
|
+
' <elementProp name="measurement" elementType="Argument">',
|
|
238
|
+
' <stringProp name="Argument.name">measurement</stringProp>',
|
|
239
|
+
' <stringProp name="Argument.value">jmeter</stringProp>',
|
|
240
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
241
|
+
' </elementProp>',
|
|
242
|
+
' <elementProp name="summaryOnly" elementType="Argument">',
|
|
243
|
+
' <stringProp name="Argument.name">summaryOnly</stringProp>',
|
|
244
|
+
' <stringProp name="Argument.value">false</stringProp>',
|
|
245
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
246
|
+
' </elementProp>',
|
|
247
|
+
' <elementProp name="samplersRegex" elementType="Argument">',
|
|
248
|
+
' <stringProp name="Argument.name">samplersRegex</stringProp>',
|
|
249
|
+
' <stringProp name="Argument.value">.*</stringProp>',
|
|
250
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
251
|
+
' </elementProp>',
|
|
252
|
+
' <elementProp name="percentiles" elementType="Argument">',
|
|
253
|
+
' <stringProp name="Argument.name">percentiles</stringProp>',
|
|
254
|
+
' <stringProp name="Argument.value">90;95;99</stringProp>',
|
|
255
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
256
|
+
' </elementProp>',
|
|
257
|
+
' <elementProp name="testTitle" elementType="Argument">',
|
|
258
|
+
' <stringProp name="Argument.name">testTitle</stringProp>',
|
|
259
|
+
f' <stringProp name="Argument.value">{xml_escape(plan_name)} Load Test</stringProp>',
|
|
260
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
261
|
+
' </elementProp>',
|
|
262
|
+
' </collectionProp>',
|
|
263
|
+
' </elementProp>',
|
|
264
|
+
' <stringProp name="classname">org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient</stringProp>',
|
|
265
|
+
' </BackendListener>',
|
|
266
|
+
' <hashTree/>'
|
|
267
|
+
])
|
|
268
|
+
|
|
269
|
+
# PerfMon Server Agent Collector
|
|
270
|
+
if perfmon_server:
|
|
271
|
+
server_host = perfmon_server.split(":")[0]
|
|
272
|
+
server_port = perfmon_server.split(":")[1] if ":" in perfmon_server else "4444"
|
|
273
|
+
xml_lines.extend([
|
|
274
|
+
' <!-- PerfMon Server Hardware Resource Metrics Collector -->',
|
|
275
|
+
' <kg.apc.jmeter.perfmon.PerfMonCollector guiclass="kg.apc.jmeter.vizualizers.PerfMonGui" testclass="kg.apc.jmeter.perfmon.PerfMonCollector" testname="PerfMon Server Agent Metrics" enabled="true">',
|
|
276
|
+
' <boolProp name="ResultCollector.error_logging">false</boolProp>',
|
|
277
|
+
' <objProp>',
|
|
278
|
+
' <name>saveConfig</name>',
|
|
279
|
+
' <value class="SampleSaveConfiguration">',
|
|
280
|
+
' <time>true</time><latency>true</latency><timestamp>true</timestamp><success>true</success>',
|
|
281
|
+
' </value>',
|
|
282
|
+
' </objProp>',
|
|
283
|
+
' <stringProp name="filename">perfmon_server_metrics.csv</stringProp>',
|
|
284
|
+
' <collectionProp name="metricConnections">',
|
|
285
|
+
' <collectionProp name="">',
|
|
286
|
+
f' <stringProp name="">{xml_escape(server_host)}</stringProp>',
|
|
287
|
+
f' <stringProp name="">{xml_escape(server_port)}</stringProp>',
|
|
288
|
+
' <stringProp name="">CPU</stringProp>',
|
|
289
|
+
' <stringProp name="">combined</stringProp>',
|
|
290
|
+
' </collectionProp>',
|
|
291
|
+
' <collectionProp name="">',
|
|
292
|
+
f' <stringProp name="">{xml_escape(server_host)}</stringProp>',
|
|
293
|
+
f' <stringProp name="">{xml_escape(server_port)}</stringProp>',
|
|
294
|
+
' <stringProp name="">Memory</stringProp>',
|
|
295
|
+
' <stringProp name="">usedperc</stringProp>',
|
|
296
|
+
' </collectionProp>',
|
|
297
|
+
' <collectionProp name="">',
|
|
298
|
+
f' <stringProp name="">{xml_escape(server_host)}</stringProp>',
|
|
299
|
+
f' <stringProp name="">{xml_escape(server_port)}</stringProp>',
|
|
300
|
+
' <stringProp name="">Disks I/O</stringProp>',
|
|
301
|
+
' <stringProp name="">queue</stringProp>',
|
|
302
|
+
' </collectionProp>',
|
|
303
|
+
' </collectionProp>',
|
|
304
|
+
' </kg.apc.jmeter.perfmon.PerfMonCollector>',
|
|
305
|
+
' <hashTree/>'
|
|
306
|
+
])
|
|
307
|
+
|
|
308
|
+
# JDBC Database Connection Configuration if requested
|
|
309
|
+
if jdbc_config:
|
|
310
|
+
pool_name = xml_escape(jdbc_config.get("pool_name", "EnterpriseDBPool"))
|
|
311
|
+
db_url = xml_escape(jdbc_config.get("url", "jdbc:postgresql://localhost:5432/enterprise_db"))
|
|
312
|
+
db_driver = xml_escape(jdbc_config.get("driver", "org.postgresql.Driver"))
|
|
313
|
+
db_user = xml_escape(jdbc_config.get("user", "postgres"))
|
|
314
|
+
db_pass = xml_escape(jdbc_config.get("password", "postgres"))
|
|
315
|
+
xml_lines.extend([
|
|
316
|
+
' <!-- JDBC Database Connection Pool Configuration -->',
|
|
317
|
+
' <JDBCDataSource guiclass="TestBeanGUI" testclass="JDBCDataSource" testname="JDBC Connection Configuration" enabled="true">',
|
|
318
|
+
' <boolProp name="autocommit">true</boolProp>',
|
|
319
|
+
' <stringProp name="checkQuery"></stringProp>',
|
|
320
|
+
' <stringProp name="connectionAge">5000</stringProp>',
|
|
321
|
+
' <stringProp name="connectionProperties"></stringProp>',
|
|
322
|
+
f' <stringProp name="dataSource">{pool_name}</stringProp>',
|
|
323
|
+
f' <stringProp name="dbUrl">{db_url}</stringProp>',
|
|
324
|
+
f' <stringProp name="driver">{db_driver}</stringProp>',
|
|
325
|
+
' <stringProp name="initQuery"></stringProp>',
|
|
326
|
+
' <boolProp name="keepAlive">true</boolProp>',
|
|
327
|
+
f' <stringProp name="password">{db_pass}</stringProp>',
|
|
328
|
+
' <stringProp name="poolMax">20</stringProp>',
|
|
329
|
+
' <stringProp name="timeout">10000</stringProp>',
|
|
330
|
+
' <stringProp name="transactionIsolation">DEFAULT</stringProp>',
|
|
331
|
+
' <stringProp name="trimInterval">60000</stringProp>',
|
|
332
|
+
f' <stringProp name="username">{db_user}</stringProp>',
|
|
333
|
+
' <boolProp name="preinit">false</boolProp>',
|
|
334
|
+
' </JDBCDataSource>',
|
|
335
|
+
' <hashTree/>'
|
|
336
|
+
])
|
|
337
|
+
|
|
338
|
+
# Workload Profile Thread Group Setup
|
|
339
|
+
tg_name = f"Workload Engine [{workload_profile.upper()}] ({vus} VUs)"
|
|
340
|
+
actual_duration = duration
|
|
341
|
+
if workload_profile == "soak" and duration < 3600:
|
|
342
|
+
actual_duration = 3600 # Default 1h for soak
|
|
343
|
+
|
|
344
|
+
xml_lines.extend([
|
|
345
|
+
f' <!-- Main Thread Group: {workload_profile.upper()} Profile -->',
|
|
346
|
+
f' <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="{tg_name}" enabled="true">',
|
|
134
347
|
' <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>',
|
|
135
348
|
' <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">',
|
|
136
349
|
f' <boolProp name="LoopController.continue_forever">{continue_forever}</boolProp>',
|
|
@@ -139,15 +352,75 @@ def build_jmx_xml(
|
|
|
139
352
|
f' <stringProp name="ThreadGroup.num_threads">${{__P(vus,{vus})}}</stringProp>',
|
|
140
353
|
f' <stringProp name="ThreadGroup.ramp_time">${{__P(ramp_up,{ramp_up})}}</stringProp>',
|
|
141
354
|
' <boolProp name="ThreadGroup.scheduler">true</boolProp>',
|
|
142
|
-
f' <stringProp name="ThreadGroup.duration">${{__P(duration,{
|
|
355
|
+
f' <stringProp name="ThreadGroup.duration">${{__P(duration,{actual_duration})}}</stringProp>',
|
|
143
356
|
' <stringProp name="ThreadGroup.delay">0</stringProp>',
|
|
144
357
|
' <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>',
|
|
145
358
|
' </ThreadGroup>',
|
|
146
359
|
' <hashTree>'
|
|
147
|
-
]
|
|
360
|
+
])
|
|
361
|
+
|
|
362
|
+
# CSV Data Set Config (Dynamic credentials / entities)
|
|
363
|
+
if csv_file:
|
|
364
|
+
xml_lines.extend([
|
|
365
|
+
' <!-- CSV Data Set Config (Multi-User Dynamic Data Parameterization) -->',
|
|
366
|
+
' <CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="CSV Data Set Config" enabled="true">',
|
|
367
|
+
' <stringProp name="delimiter">,</stringProp>',
|
|
368
|
+
' <stringProp name="fileEncoding">UTF-8</stringProp>',
|
|
369
|
+
f' <stringProp name="filename">{xml_escape(csv_file)}</stringProp>',
|
|
370
|
+
' <boolProp name="ignoreFirstLine">true</boolProp>',
|
|
371
|
+
' <boolProp name="quotedData">false</boolProp>',
|
|
372
|
+
' <boolProp name="recycle">true</boolProp>',
|
|
373
|
+
' <stringProp name="shareMode">shareMode.all</stringProp>',
|
|
374
|
+
' <boolProp name="stopThread">false</boolProp>',
|
|
375
|
+
f' <stringProp name="variableNames">{xml_escape(csv_variables)}</stringProp>',
|
|
376
|
+
' </CSVDataSet>',
|
|
377
|
+
' <hashTree/>'
|
|
378
|
+
])
|
|
379
|
+
|
|
380
|
+
# JSR223 Groovy PreProcessor for HMAC-SHA256 Payload Signature
|
|
381
|
+
if jsr223_hmac:
|
|
382
|
+
xml_lines.extend([
|
|
383
|
+
' <!-- JSR223 Groovy PreProcessor: HMAC-SHA256 Payload Signer -->',
|
|
384
|
+
' <JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="JSR223 - HMAC Payload Signer" enabled="true">',
|
|
385
|
+
' <stringProp name="scriptLanguage">groovy</stringProp>',
|
|
386
|
+
' <stringProp name="parameters"></stringProp>',
|
|
387
|
+
' <stringProp name="filename"></stringProp>',
|
|
388
|
+
' <stringProp name="cacheKey">true</stringProp>',
|
|
389
|
+
' <stringProp name="script"><![CDATA[',
|
|
390
|
+
'import javax.crypto.Mac',
|
|
391
|
+
'import javax.crypto.spec.SecretKeySpec',
|
|
392
|
+
'',
|
|
393
|
+
'String secret = vars.get("HMAC_SECRET") ?: "SynapseEnterpriseVault#2026"',
|
|
394
|
+
'String body = sampler.getArguments().getArgument(0)?.getValue() ?: ""',
|
|
395
|
+
'String timestamp = String.valueOf(System.currentTimeMillis())',
|
|
396
|
+
'',
|
|
397
|
+
'Mac mac = Mac.getInstance("HmacSHA256")',
|
|
398
|
+
'mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"))',
|
|
399
|
+
'byte[] rawHmac = mac.doFinal((timestamp + ":" + body).getBytes("UTF-8"))',
|
|
400
|
+
'String signature = rawHmac.encodeHex().toString()',
|
|
401
|
+
'',
|
|
402
|
+
'sampler.getHeaderManager().add(new org.apache.jmeter.protocol.http.control.Header("X-Signature", signature))',
|
|
403
|
+
'sampler.getHeaderManager().add(new org.apache.jmeter.protocol.http.control.Header("X-Timestamp", timestamp))',
|
|
404
|
+
'sampler.getHeaderManager().add(new org.apache.jmeter.protocol.http.control.Header("X-Correlation-ID", "CORR-" + UUID.randomUUID().toString()))',
|
|
405
|
+
']]></stringProp>',
|
|
406
|
+
' </JSR223PreProcessor>',
|
|
407
|
+
' <hashTree/>'
|
|
408
|
+
])
|
|
409
|
+
|
|
410
|
+
# Uniform Random Timer for realistic user think-time
|
|
411
|
+
if think_time_max > think_time_min and think_time_max > 0:
|
|
412
|
+
timer_range = think_time_max - think_time_min
|
|
413
|
+
xml_lines.extend([
|
|
414
|
+
' <!-- Uniform Random Timer (Simulate Realistic User Think-Time) -->',
|
|
415
|
+
' <UniformRandomTimer guiclass="UniformRandomTimerGui" testclass="UniformRandomTimer" testname="User Think-Time Timer" enabled="true">',
|
|
416
|
+
f' <stringProp name="ConstantTimer.delay">{think_time_min}</stringProp>',
|
|
417
|
+
f' <stringProp name="RandomTimer.range">{timer_range}</stringProp>',
|
|
418
|
+
' </UniformRandomTimer>',
|
|
419
|
+
' <hashTree/>'
|
|
420
|
+
])
|
|
148
421
|
|
|
149
|
-
|
|
150
|
-
|
|
422
|
+
def render_sampler(idx: int, s: Dict[str, Any], indent: str = " ") -> List[str]:
|
|
423
|
+
lines = []
|
|
151
424
|
s_name = xml_escape(s.get("name", f"HTTP Request {idx}"))
|
|
152
425
|
s_path = xml_escape(s.get("path", "/"))
|
|
153
426
|
s_method = xml_escape(s.get("method", "GET").upper())
|
|
@@ -156,82 +429,247 @@ def build_jmx_xml(
|
|
|
156
429
|
escaped_body = xml_escape(s_body) if has_body else ""
|
|
157
430
|
expected_status = s.get("expected_status", 200)
|
|
158
431
|
|
|
159
|
-
|
|
160
|
-
f'
|
|
161
|
-
f'
|
|
162
|
-
f'
|
|
163
|
-
'
|
|
164
|
-
'
|
|
432
|
+
lines.extend([
|
|
433
|
+
f'{indent}<!-- Sampler {idx}: [{s_method}] {s_name} -->',
|
|
434
|
+
f'{indent}<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[{s_method}] {s_name}" enabled="true">',
|
|
435
|
+
f'{indent} <boolProp name="HTTPSampler.postBodyRaw">{str(has_body).lower()}</boolProp>',
|
|
436
|
+
f'{indent} <elementProp name="HTTPsampler.Arguments" elementType="Arguments">',
|
|
437
|
+
f'{indent} <collectionProp name="Arguments.arguments">'
|
|
165
438
|
])
|
|
166
439
|
|
|
167
440
|
if has_body:
|
|
168
|
-
|
|
169
|
-
'
|
|
170
|
-
'
|
|
171
|
-
f'
|
|
172
|
-
'
|
|
173
|
-
'
|
|
441
|
+
lines.extend([
|
|
442
|
+
f'{indent} <elementProp name="" elementType="HTTPArgument">',
|
|
443
|
+
f'{indent} <boolProp name="HTTPArgument.always_encode">false</boolProp>',
|
|
444
|
+
f'{indent} <stringProp name="Argument.value">{escaped_body}</stringProp>',
|
|
445
|
+
f'{indent} <stringProp name="Argument.metadata">=</stringProp>',
|
|
446
|
+
f'{indent} </elementProp>'
|
|
174
447
|
])
|
|
175
448
|
|
|
176
|
-
|
|
177
|
-
'
|
|
178
|
-
'
|
|
179
|
-
f'
|
|
180
|
-
f'
|
|
181
|
-
'
|
|
182
|
-
'
|
|
183
|
-
'
|
|
184
|
-
'
|
|
185
|
-
'
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
f'
|
|
189
|
-
'
|
|
190
|
-
f'
|
|
191
|
-
'
|
|
192
|
-
'
|
|
193
|
-
'
|
|
194
|
-
'
|
|
195
|
-
'
|
|
196
|
-
'
|
|
197
|
-
'
|
|
198
|
-
'
|
|
199
|
-
'
|
|
200
|
-
f'
|
|
201
|
-
'
|
|
202
|
-
'
|
|
203
|
-
'
|
|
449
|
+
lines.extend([
|
|
450
|
+
f'{indent} </collectionProp>',
|
|
451
|
+
f'{indent} </elementProp>',
|
|
452
|
+
f'{indent} <stringProp name="HTTPSampler.path">{s_path}</stringProp>',
|
|
453
|
+
f'{indent} <stringProp name="HTTPSampler.method">{s_method}</stringProp>',
|
|
454
|
+
f'{indent} <boolProp name="HTTPSampler.follow_redirects">true</boolProp>',
|
|
455
|
+
f'{indent} <boolProp name="HTTPSampler.auto_redirects">false</boolProp>',
|
|
456
|
+
f'{indent} <boolProp name="HTTPSampler.use_keepalive">true</boolProp>',
|
|
457
|
+
f'{indent} <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>',
|
|
458
|
+
f'{indent}</HTTPSamplerProxy>',
|
|
459
|
+
f'{indent}<hashTree>',
|
|
460
|
+
f'{indent} <!-- Response Status Assertion -->',
|
|
461
|
+
f'{indent} <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert Status ({expected_status})" enabled="true">',
|
|
462
|
+
f'{indent} <collectionProp name="Asserion.test_strings">',
|
|
463
|
+
f'{indent} <stringProp name="{expected_status}">{expected_status}</stringProp>',
|
|
464
|
+
f'{indent} <stringProp name="200">200</stringProp>',
|
|
465
|
+
f'{indent} <stringProp name="201">201</stringProp>',
|
|
466
|
+
f'{indent} </collectionProp>',
|
|
467
|
+
f'{indent} <stringProp name="Assertion.test_field">Assertion.response_code</stringProp>',
|
|
468
|
+
f'{indent} <boolProp name="Assertion.assume_success">false</boolProp>',
|
|
469
|
+
f'{indent} <intProp name="Assertion.test_type">40</intProp>',
|
|
470
|
+
f'{indent} </ResponseAssertion>',
|
|
471
|
+
f'{indent} <hashTree/>',
|
|
472
|
+
f'{indent} <!-- SLA Latency Assertion -->',
|
|
473
|
+
f'{indent} <DurationAssertion guiclass="DurationAssertionGui" testclass="DurationAssertion" testname="Assert SLA Latency (< ${{SLA_P95}}ms)" enabled="true">',
|
|
474
|
+
f'{indent} <stringProp name="DurationAssertion.duration">${{SLA_P95}}</stringProp>',
|
|
475
|
+
f'{indent} </DurationAssertion>',
|
|
476
|
+
f'{indent} <hashTree/>'
|
|
204
477
|
])
|
|
205
478
|
|
|
206
|
-
#
|
|
479
|
+
# Deep Correlation Engine: CSRF Regex Extractor on HTML/Auth/Form responses
|
|
480
|
+
if deep_correlation:
|
|
481
|
+
lines.extend([
|
|
482
|
+
f'{indent} <!-- Deep Correlation: Regex Extract Hidden CSRF Token -->',
|
|
483
|
+
f'{indent} <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Regex - Extract CSRF Token" enabled="true">',
|
|
484
|
+
f'{indent} <stringProp name="RegexExtractor.useHeaders">false</stringProp>',
|
|
485
|
+
f'{indent} <stringProp name="RegexExtractor.refname">CSRF_TOKEN</stringProp>',
|
|
486
|
+
f'{indent} <stringProp name="RegexExtractor.regex"><["\']\\s+value=["\']([^"\']+)["\'])|(?:content=["\']([^"\']+)["\']\\s+name=["\']csrf[-_]?token["\'])]]></stringProp>',
|
|
487
|
+
f'{indent} <stringProp name="RegexExtractor.template">$1$$2$</stringProp>',
|
|
488
|
+
f'{indent} <stringProp name="RegexExtractor.default">${{CSRF_TOKEN}}</stringProp>',
|
|
489
|
+
f'{indent} <stringProp name="RegexExtractor.match_number">1</stringProp>',
|
|
490
|
+
f'{indent} </RegexExtractor>',
|
|
491
|
+
f'{indent} <hashTree/>',
|
|
492
|
+
f'{indent} <!-- Deep Correlation: Random Array Item Selection (Match No: 0) -->',
|
|
493
|
+
f'{indent} <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Random Item Selector" enabled="true">',
|
|
494
|
+
f'{indent} <stringProp name="JSONPostProcessor.referenceNames">RANDOM_ITEM_ID</stringProp>',
|
|
495
|
+
f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$..items[?(@.stock > 0)].id, $..id, $..code</stringProp>',
|
|
496
|
+
f'{indent} <stringProp name="JSONPostProcessor.match_numbers">0</stringProp>',
|
|
497
|
+
f'{indent} <stringProp name="JSONPostProcessor.defaultValues">${{RANDOM_ITEM_ID}}</stringProp>',
|
|
498
|
+
f'{indent} </JSONPostProcessor>',
|
|
499
|
+
f'{indent} <hashTree/>'
|
|
500
|
+
])
|
|
501
|
+
|
|
502
|
+
# Dynamic Token Extractor
|
|
207
503
|
if "auth" in s_path.lower() or "login" in s_path.lower():
|
|
208
|
-
|
|
209
|
-
'
|
|
210
|
-
'
|
|
211
|
-
'
|
|
212
|
-
'
|
|
213
|
-
'
|
|
214
|
-
'
|
|
215
|
-
'
|
|
216
|
-
'
|
|
504
|
+
lines.extend([
|
|
505
|
+
f'{indent} <!-- Auto-Extract Dynamic Bearer Token -->',
|
|
506
|
+
f'{indent} <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Bearer Token" enabled="true">',
|
|
507
|
+
f'{indent} <stringProp name="JSONPostProcessor.referenceNames">BEARER_TOKEN</stringProp>',
|
|
508
|
+
f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$.token, $.access_token, $.data.token, $.data.access_token</stringProp>',
|
|
509
|
+
f'{indent} <stringProp name="JSONPostProcessor.match_numbers">1</stringProp>',
|
|
510
|
+
f'{indent} <stringProp name="JSONPostProcessor.defaultValues">${{BEARER_TOKEN}}</stringProp>',
|
|
511
|
+
f'{indent} </JSONPostProcessor>',
|
|
512
|
+
f'{indent} <hashTree/>'
|
|
217
513
|
])
|
|
218
514
|
elif s_method == "POST":
|
|
515
|
+
lines.extend([
|
|
516
|
+
f'{indent} <!-- Auto-Extract Created Entity ID -->',
|
|
517
|
+
f'{indent} <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Entity ID" enabled="true">',
|
|
518
|
+
f'{indent} <stringProp name="JSONPostProcessor.referenceNames">LAST_CREATED_ID</stringProp>',
|
|
519
|
+
f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$.id, $.code, $.data.id, $.data.code</stringProp>',
|
|
520
|
+
f'{indent} <stringProp name="JSONPostProcessor.match_numbers">1</stringProp>',
|
|
521
|
+
f'{indent} <stringProp name="JSONPostProcessor.defaultValues"></stringProp>',
|
|
522
|
+
f'{indent} </JSONPostProcessor>',
|
|
523
|
+
f'{indent} <hashTree/>'
|
|
524
|
+
])
|
|
525
|
+
|
|
526
|
+
lines.append(f'{indent}</hashTree>')
|
|
527
|
+
return lines
|
|
528
|
+
|
|
529
|
+
# Render Samplers with optional Traffic Shaping (Weighted Throughput Controller)
|
|
530
|
+
if weighted_mix:
|
|
531
|
+
# Parse weights: e.g. "70,20,10" or "70:Browse,20:Cart,10:Checkout"
|
|
532
|
+
parts = [p.strip() for p in weighted_mix.split(",") if p.strip()]
|
|
533
|
+
weights: List[Tuple[float, str]] = []
|
|
534
|
+
for p in parts:
|
|
535
|
+
if ":" in p:
|
|
536
|
+
w_str, label = p.split(":", 1)
|
|
537
|
+
weights.append((float(w_str), label))
|
|
538
|
+
else:
|
|
539
|
+
weights.append((float(p), f"Route {len(weights) + 1}"))
|
|
540
|
+
|
|
541
|
+
# Partition samplers across weights
|
|
542
|
+
chunk_size = max(1, len(samplers) // len(weights)) if weights else len(samplers)
|
|
543
|
+
for w_idx, (weight, label) in enumerate(weights):
|
|
544
|
+
start_i = w_idx * chunk_size
|
|
545
|
+
end_i = start_i + chunk_size if w_idx < len(weights) - 1 else len(samplers)
|
|
546
|
+
group_samplers = samplers[start_i:end_i] if start_i < len(samplers) else samplers[-1:]
|
|
547
|
+
|
|
219
548
|
xml_lines.extend([
|
|
220
|
-
'
|
|
221
|
-
'
|
|
222
|
-
'
|
|
223
|
-
'
|
|
224
|
-
'
|
|
225
|
-
'
|
|
226
|
-
'
|
|
227
|
-
'
|
|
549
|
+
f' <!-- Traffic Shaping Controller: {weight}% Load ({label}) -->',
|
|
550
|
+
f' <ThroughputController guiclass="ThroughputControllerGui" testclass="ThroughputController" testname="Traffic Weight {weight}% - {xml_escape(label)}" enabled="true">',
|
|
551
|
+
' <intProp name="ThroughputController.style">1</intProp>',
|
|
552
|
+
' <boolProp name="ThroughputController.perThread">false</boolProp>',
|
|
553
|
+
f' <stringProp name="ThroughputController.maxThroughput">{weight}</stringProp>',
|
|
554
|
+
' <FloatProperty>',
|
|
555
|
+
' <name>ThroughputController.percentThroughput</name>',
|
|
556
|
+
f' <value>{weight}</value>',
|
|
557
|
+
' <savedValue>0.0</savedValue>',
|
|
558
|
+
' </FloatProperty>',
|
|
559
|
+
' </ThroughputController>',
|
|
560
|
+
' <hashTree>'
|
|
228
561
|
])
|
|
562
|
+
for idx, s in enumerate(group_samplers, 1):
|
|
563
|
+
xml_lines.extend(render_sampler(idx, s, indent=" "))
|
|
564
|
+
xml_lines.append(' </hashTree>')
|
|
229
565
|
|
|
566
|
+
elif group_transactions:
|
|
567
|
+
# Group samplers into Transaction Controllers by operation prefix or chunks
|
|
568
|
+
xml_lines.extend([
|
|
569
|
+
' <!-- Transaction Controller: End-to-End User Journey Flow -->',
|
|
570
|
+
' <TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="Transaction - End-to-End Business Flow" enabled="true">',
|
|
571
|
+
' <boolProp name="TransactionController.includeTimers">false</boolProp>',
|
|
572
|
+
' <boolProp name="TransactionController.parent">true</boolProp>',
|
|
573
|
+
' </TransactionController>',
|
|
574
|
+
' <hashTree>'
|
|
575
|
+
])
|
|
576
|
+
for idx, s in enumerate(samplers, 1):
|
|
577
|
+
xml_lines.extend(render_sampler(idx, s, indent=" "))
|
|
230
578
|
xml_lines.append(' </hashTree>')
|
|
579
|
+
else:
|
|
580
|
+
for idx, s in enumerate(samplers, 1):
|
|
581
|
+
xml_lines.extend(render_sampler(idx, s, indent=" "))
|
|
582
|
+
|
|
583
|
+
# Render JDBC Samplers if present
|
|
584
|
+
if jdbc_config and "queries" in jdbc_config:
|
|
585
|
+
pool_name = xml_escape(jdbc_config.get("pool_name", "EnterpriseDBPool"))
|
|
586
|
+
for q_idx, q in enumerate(jdbc_config["queries"], 1):
|
|
587
|
+
q_name = xml_escape(q.get("name", f"JDBC Query {q_idx}"))
|
|
588
|
+
sql = xml_escape(q.get("sql", "SELECT 1"))
|
|
589
|
+
q_type = xml_escape(q.get("type", "Select Statement"))
|
|
590
|
+
xml_lines.extend([
|
|
591
|
+
f' <!-- JDBC Sampler {q_idx}: {q_name} -->',
|
|
592
|
+
f' <JDBCSampler guiclass="TestBeanGUI" testclass="JDBCSampler" testname="[JDBC] {q_name}" enabled="true">',
|
|
593
|
+
f' <stringProp name="dataSource">{pool_name}</stringProp>',
|
|
594
|
+
f' <stringProp name="query">{sql}</stringProp>',
|
|
595
|
+
' <stringProp name="queryArguments"></stringProp>',
|
|
596
|
+
' <stringProp name="queryArgumentsTypes"></stringProp>',
|
|
597
|
+
f' <stringProp name="queryType">{q_type}</stringProp>',
|
|
598
|
+
' <stringProp name="resultVariable"></stringProp>',
|
|
599
|
+
' <stringProp name="variableNames"></stringProp>',
|
|
600
|
+
' <stringProp name="resultSetHandler">Store as String</stringProp>',
|
|
601
|
+
' </JDBCSampler>',
|
|
602
|
+
' <hashTree/>'
|
|
603
|
+
])
|
|
604
|
+
|
|
605
|
+
# Render WebSocket Samplers if present
|
|
606
|
+
if ws_config:
|
|
607
|
+
ws_server = xml_escape(ws_config.get("server", "localhost"))
|
|
608
|
+
ws_port = xml_escape(str(ws_config.get("port", "8080")))
|
|
609
|
+
ws_path = xml_escape(ws_config.get("path", "/ws/feed"))
|
|
610
|
+
xml_lines.extend([
|
|
611
|
+
' <!-- WebSocket Lifecycle Samplers (Connect, Send, Receive, Close) -->',
|
|
612
|
+
' <eu.luminis.jmeter.wssampler.OpenWebSocketSampler guiclass="eu.luminis.jmeter.wssampler.OpenWebSocketSamplerGui" testclass="eu.luminis.jmeter.wssampler.OpenWebSocketSampler" testname="[WS] Open Connection" enabled="true">',
|
|
613
|
+
' <boolProp name="TLS">false</boolProp>',
|
|
614
|
+
f' <stringProp name="server">{ws_server}</stringProp>',
|
|
615
|
+
f' <stringProp name="port">{ws_port}</stringProp>',
|
|
616
|
+
f' <stringProp name="path">{ws_path}</stringProp>',
|
|
617
|
+
' <stringProp name="connectTimeout">5000</stringProp>',
|
|
618
|
+
' <stringProp name="readTimeout">10000</stringProp>',
|
|
619
|
+
' </eu.luminis.jmeter.wssampler.OpenWebSocketSampler>',
|
|
620
|
+
' <hashTree/>',
|
|
621
|
+
' <eu.luminis.jmeter.wssampler.RequestResponseWebSocketSampler guiclass="eu.luminis.jmeter.wssampler.RequestResponseWebSocketSamplerGui" testclass="eu.luminis.jmeter.wssampler.RequestResponseWebSocketSampler" testname="[WS] Request-Response Frame" enabled="true">',
|
|
622
|
+
' <boolProp name="createNewConnection">false</boolProp>',
|
|
623
|
+
' <boolProp name="loadDataFromFile">false</boolProp>',
|
|
624
|
+
' <stringProp name="requestData">{"event": "subscribe", "channel": "ticker", "pair": "BTC_USDT"}</stringProp>',
|
|
625
|
+
' <stringProp name="readTimeout">5000</stringProp>',
|
|
626
|
+
' </eu.luminis.jmeter.wssampler.RequestResponseWebSocketSampler>',
|
|
627
|
+
' <hashTree/>',
|
|
628
|
+
' <eu.luminis.jmeter.wssampler.CloseWebSocketSampler guiclass="eu.luminis.jmeter.wssampler.CloseWebSocketSamplerGui" testclass="eu.luminis.jmeter.wssampler.CloseWebSocketSampler" testname="[WS] Close Connection" enabled="true">',
|
|
629
|
+
' <stringProp name="statusCode">1000</stringProp>',
|
|
630
|
+
' <stringProp name="readTimeout">3000</stringProp>',
|
|
631
|
+
' </eu.luminis.jmeter.wssampler.CloseWebSocketSampler>',
|
|
632
|
+
' <hashTree/>'
|
|
633
|
+
])
|
|
634
|
+
|
|
635
|
+
# Render gRPC Samplers if present
|
|
636
|
+
if grpc_config:
|
|
637
|
+
grpc_host = xml_escape(grpc_config.get("host", "localhost"))
|
|
638
|
+
grpc_port = xml_escape(str(grpc_config.get("port", "50051")))
|
|
639
|
+
grpc_service = xml_escape(grpc_config.get("service", "enterprise.OrderService/ProcessOrder"))
|
|
640
|
+
xml_lines.extend([
|
|
641
|
+
' <!-- gRPC Microservice RPC Sampler -->',
|
|
642
|
+
' <vn.zalopay.benchmark.core.protobuf.ProtobufSampler guiclass="TestBeanGUI" testclass="vn.zalopay.benchmark.core.protobuf.ProtobufSampler" testname="[gRPC] Unary RPC Call" enabled="true">',
|
|
643
|
+
f' <stringProp name="hostname">{grpc_host}</stringProp>',
|
|
644
|
+
f' <stringProp name="port">{grpc_port}</stringProp>',
|
|
645
|
+
f' <stringProp name="fullMethod">{grpc_service}</stringProp>',
|
|
646
|
+
' <boolProp name="useTls">false</boolProp>',
|
|
647
|
+
' <stringProp name="protoFolder">proto</stringProp>',
|
|
648
|
+
' <stringProp name="libFolder">proto/libs</stringProp>',
|
|
649
|
+
' <stringProp name="requestJson">{"order_id": "${LAST_CREATED_ID}", "amount": 250000, "currency": "VND"}</stringProp>',
|
|
650
|
+
' <stringProp name="deadline">5000</stringProp>',
|
|
651
|
+
' </vn.zalopay.benchmark.core.protobuf.ProtobufSampler>',
|
|
652
|
+
' <hashTree/>'
|
|
653
|
+
])
|
|
654
|
+
|
|
655
|
+
# Render Kafka Samplers if present
|
|
656
|
+
if kafka_config:
|
|
657
|
+
kafka_brokers = xml_escape(kafka_config.get("brokers", "localhost:9092"))
|
|
658
|
+
kafka_topic = xml_escape(kafka_config.get("topic", "enterprise.orders.stream"))
|
|
659
|
+
xml_lines.extend([
|
|
660
|
+
' <!-- Kafka Producer Event-Driven Message Sampler -->',
|
|
661
|
+
' <org.apache.jmeter.protocol.jms.sampler.PublisherSampler guiclass="TestBeanGUI" testclass="org.apache.jmeter.protocol.jms.sampler.PublisherSampler" testname="[Kafka] Stream Message Producer" enabled="true">',
|
|
662
|
+
' <stringProp name="jndiConfig">org.apache.kafka.common.security.plain.PlainLoginModule</stringProp>',
|
|
663
|
+
f' <stringProp name="destination">{kafka_topic}</stringProp>',
|
|
664
|
+
f' <stringProp name="providerUrl">{kafka_brokers}</stringProp>',
|
|
665
|
+
' <stringProp name="textMessage">{"timestamp": "${__time()}", "correlation_id": "CORR-${__UUID()}", "event": "ORDER_SUBMITTED"}</stringProp>',
|
|
666
|
+
' </org.apache.jmeter.protocol.jms.sampler.PublisherSampler>',
|
|
667
|
+
' <hashTree/>'
|
|
668
|
+
])
|
|
231
669
|
|
|
232
|
-
#
|
|
670
|
+
# Result Collectors for CLI JTL export and Summary
|
|
233
671
|
xml_lines.extend([
|
|
234
|
-
' <!-- Headless Summary Report
|
|
672
|
+
' <!-- Headless JTL Result Collector & Summary Report -->',
|
|
235
673
|
' <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">',
|
|
236
674
|
' <boolProp name="ResultCollector.error_logging">false</boolProp>',
|
|
237
675
|
' <objProp>',
|
|
@@ -283,7 +721,6 @@ def parse_openapi_for_jmeter(spec_data: Dict[str, Any]) -> tuple:
|
|
|
283
721
|
info_dict = spec_data.get("info", {})
|
|
284
722
|
plan_name = info_dict.get("title", "Enterprise Performance Test Plan")
|
|
285
723
|
|
|
286
|
-
# Host / Protocol / Port extraction
|
|
287
724
|
base_host = "api.staging.enterprise.local"
|
|
288
725
|
protocol = "https"
|
|
289
726
|
port = "443"
|
|
@@ -313,8 +750,6 @@ def parse_openapi_for_jmeter(spec_data: Dict[str, Any]) -> tuple:
|
|
|
313
750
|
op = path_item[method]
|
|
314
751
|
summary = op.get("summary") or f"{method.upper()} {path_str}"
|
|
315
752
|
|
|
316
|
-
# Convert path variables: {id} -> ${LAST_CREATED_ID}, {param} -> ${param}
|
|
317
|
-
import re
|
|
318
753
|
def replace_path_param(match):
|
|
319
754
|
pname = match.group(1)
|
|
320
755
|
if pname.lower() in ("id", "categoryid", "entityid", "userid"):
|
|
@@ -390,8 +825,6 @@ def parse_postman_for_jmeter(postman_data: Dict[str, Any]) -> tuple:
|
|
|
390
825
|
path_parts = url_obj.get("path", [])
|
|
391
826
|
path = "/" + "/".join(path_parts) if path_parts else "/"
|
|
392
827
|
|
|
393
|
-
# Convert Postman {{var}} to JMeter ${var}
|
|
394
|
-
import re
|
|
395
828
|
jmeter_path = re.sub(r'\{\{([a-zA-Z0-9_]+)\}\}', r'${\1}', path)
|
|
396
829
|
|
|
397
830
|
body_str = ""
|
|
@@ -412,24 +845,142 @@ def parse_postman_for_jmeter(postman_data: Dict[str, Any]) -> tuple:
|
|
|
412
845
|
return plan_name, base_host, protocol, port, samplers
|
|
413
846
|
|
|
414
847
|
|
|
848
|
+
def parse_har_for_jmeter(har_data: Dict[str, Any]) -> tuple:
|
|
849
|
+
"""Extracts samplers from HTTP Archive (HAR)."""
|
|
850
|
+
entries = har_data.get("log", {}).get("entries", [])
|
|
851
|
+
plan_name = "HAR Recorded Performance Test Plan"
|
|
852
|
+
base_host = "api.staging.enterprise.local"
|
|
853
|
+
protocol = "https"
|
|
854
|
+
port = "443"
|
|
855
|
+
samplers = []
|
|
856
|
+
|
|
857
|
+
if entries:
|
|
858
|
+
first_url = entries[0].get("request", {}).get("url", "")
|
|
859
|
+
p = urlparse(first_url)
|
|
860
|
+
if p.netloc:
|
|
861
|
+
base_host = p.hostname or base_host
|
|
862
|
+
protocol = p.scheme or protocol
|
|
863
|
+
port = str(p.port) if p.port else ("443" if protocol == "https" else "80")
|
|
864
|
+
|
|
865
|
+
for idx, e in enumerate(entries, 1):
|
|
866
|
+
req = e.get("request", {})
|
|
867
|
+
url = req.get("url", "/")
|
|
868
|
+
p = urlparse(url)
|
|
869
|
+
method = req.get("method", "GET").upper()
|
|
870
|
+
|
|
871
|
+
post_data = req.get("postData", {})
|
|
872
|
+
body_str = post_data.get("text", "")
|
|
873
|
+
status_code = e.get("response", {}).get("status", 200)
|
|
874
|
+
|
|
875
|
+
samplers.append({
|
|
876
|
+
"name": f"HAR Step {idx}: {p.path or '/'}",
|
|
877
|
+
"path": p.path + (f"?{p.query}" if p.query else ""),
|
|
878
|
+
"method": method,
|
|
879
|
+
"body": body_str,
|
|
880
|
+
"expected_status": status_code if status_code > 0 else 200
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
return plan_name, base_host, protocol, port, samplers
|
|
884
|
+
|
|
885
|
+
|
|
415
886
|
def main():
|
|
416
887
|
parser = argparse.ArgumentParser(
|
|
417
|
-
description="Generate Apache JMeter 5.x Test Plan XML (.jmx)
|
|
888
|
+
description="Generate Apache JMeter 5.x Test Plan XML (.jmx) with Pinnacle JMeter 360° Capabilities."
|
|
418
889
|
)
|
|
419
|
-
parser.add_argument("--spec", "-s", help="Path to OpenAPI / Swagger JSON specification file.")
|
|
890
|
+
parser.add_argument("--spec", "-s", help="Path to OpenAPI / Swagger JSON/YAML specification file.")
|
|
420
891
|
parser.add_argument("--postman", "-p", help="Path to Postman Collection JSON file (v2.1.0).")
|
|
892
|
+
parser.add_argument("--har", help="Path to HTTP Archive (.har) recording file.")
|
|
421
893
|
parser.add_argument("--base-url", "-b", help="Target base host URL (e.g. https://api.staging.enterprise.local).")
|
|
422
894
|
parser.add_argument("--vus", type=int, default=50, help="Number of concurrent Virtual Users (threads). Default: 50.")
|
|
423
895
|
parser.add_argument("--ramp-up", type=int, default=30, help="Ramp-up duration in seconds. Default: 30.")
|
|
424
896
|
parser.add_argument("--duration", type=int, default=300, help="Sustained test duration in seconds. Default: 300.")
|
|
425
|
-
parser.add_argument("--loop-count", type=int, default=-1, help="Loop iterations count (-1 for infinite
|
|
897
|
+
parser.add_argument("--loop-count", type=int, default=-1, help="Loop iterations count (-1 for infinite). Default: -1.")
|
|
426
898
|
parser.add_argument("--sla-p95", type=int, default=500, help="P95 Latency SLA assertion threshold in ms. Default: 500.")
|
|
427
899
|
parser.add_argument("--output", "-o", help="Target output JMeter test plan file path (.jmx).")
|
|
428
900
|
parser.add_argument("--sample", action="store_true", help="Generate a sample load test plan using built-in enterprise schema.")
|
|
901
|
+
|
|
902
|
+
# Advanced JMeter 360° Extensions
|
|
903
|
+
parser.add_argument("--no-cookies", action="store_true", help="Disable automatic HTTP Cookie Manager.")
|
|
904
|
+
parser.add_argument("--no-cache", action="store_true", help="Disable automatic HTTP Cache Manager.")
|
|
905
|
+
parser.add_argument("--no-deep-correlation", action="store_true", help="Disable CSRF Regex and Random Item Selector.")
|
|
906
|
+
parser.add_argument("--csv-data", help="Path to CSV file for dynamic multi-user test data parameterization.")
|
|
907
|
+
parser.add_argument("--csv-vars", default="username,password,entity_id", help="Comma-separated variable names for CSV Data Set.")
|
|
908
|
+
parser.add_argument("--group-transactions", action="store_true", help="Wrap endpoints in Transaction Controllers to measure end-to-end flow SLA.")
|
|
909
|
+
parser.add_argument("--weighted-mix", help="Traffic shaping weight mix, e.g. '70,20,10' or '70:Browse,20:Cart,10:Checkout'.")
|
|
910
|
+
parser.add_argument("--workload", choices=["linear", "step", "spike", "soak"], default="linear", help="Workload profile shape.")
|
|
911
|
+
parser.add_argument("--jsr223-hmac", action="store_true", help="Inject JSR223 Groovy PreProcessor for HMAC-SHA256 payload signatures.")
|
|
912
|
+
parser.add_argument("--influxdb-url", help="InfluxDB HTTP metrics endpoint for live Grafana APM dashboard streaming.")
|
|
913
|
+
parser.add_argument("--perfmon-server", help="PerfMon Server Agent host:port (e.g. 192.168.1.100:4444) for hardware telemetry.")
|
|
914
|
+
parser.add_argument("--think-time-min", type=int, default=0, help="Minimum simulated user think time in ms.")
|
|
915
|
+
parser.add_argument("--think-time-max", type=int, default=0, help="Maximum simulated user think time in ms.")
|
|
916
|
+
|
|
917
|
+
# Multi-Protocol Samples
|
|
918
|
+
parser.add_argument("--jdbc-sample", action="store_true", help="Generate a sample JDBC database benchmark test plan.")
|
|
919
|
+
parser.add_argument("--ws-sample", action="store_true", help="Generate a sample WebSocket real-time lifecycle test plan.")
|
|
920
|
+
parser.add_argument("--grpc-sample", action="store_true", help="Generate a sample gRPC microservice unary RPC test plan.")
|
|
921
|
+
parser.add_argument("--kafka-sample", action="store_true", help="Generate a sample Kafka stream message producer test plan.")
|
|
429
922
|
|
|
430
923
|
args = parser.parse_args()
|
|
431
924
|
|
|
432
|
-
|
|
925
|
+
jdbc_config = None
|
|
926
|
+
ws_config = None
|
|
927
|
+
grpc_config = None
|
|
928
|
+
kafka_config = None
|
|
929
|
+
|
|
930
|
+
if args.ws_sample:
|
|
931
|
+
plan_name = "Enterprise WebSocket Real-Time Stream Benchmark"
|
|
932
|
+
base_host = "localhost"
|
|
933
|
+
protocol = "ws"
|
|
934
|
+
port = "8080"
|
|
935
|
+
samplers = []
|
|
936
|
+
ws_config = {
|
|
937
|
+
"server": "localhost",
|
|
938
|
+
"port": 8080,
|
|
939
|
+
"path": "/ws/feed"
|
|
940
|
+
}
|
|
941
|
+
elif args.grpc_sample:
|
|
942
|
+
plan_name = "Enterprise gRPC Microservices Benchmark"
|
|
943
|
+
base_host = "localhost"
|
|
944
|
+
protocol = "grpc"
|
|
945
|
+
port = "50051"
|
|
946
|
+
samplers = []
|
|
947
|
+
grpc_config = {
|
|
948
|
+
"host": "localhost",
|
|
949
|
+
"port": 50051,
|
|
950
|
+
"service": "enterprise.OrderService/ProcessOrder"
|
|
951
|
+
}
|
|
952
|
+
elif args.kafka_sample:
|
|
953
|
+
plan_name = "Enterprise Kafka Event Stream Benchmark"
|
|
954
|
+
base_host = "localhost"
|
|
955
|
+
protocol = "kafka"
|
|
956
|
+
port = "9092"
|
|
957
|
+
samplers = []
|
|
958
|
+
kafka_config = {
|
|
959
|
+
"brokers": "localhost:9092",
|
|
960
|
+
"topic": "enterprise.orders.stream"
|
|
961
|
+
}
|
|
962
|
+
elif args.jdbc_sample:
|
|
963
|
+
plan_name = "Enterprise Database Performance Benchmark (JDBC)"
|
|
964
|
+
base_host = "localhost"
|
|
965
|
+
protocol = "http"
|
|
966
|
+
port = "80"
|
|
967
|
+
samplers = []
|
|
968
|
+
jdbc_config = {
|
|
969
|
+
"pool_name": "PostgresEnterprisePool",
|
|
970
|
+
"url": "jdbc:postgresql://localhost:5432/enterprise_db",
|
|
971
|
+
"driver": "org.postgresql.Driver",
|
|
972
|
+
"user": "postgres",
|
|
973
|
+
"password": "SecretDBPassword#2026",
|
|
974
|
+
"queries": [
|
|
975
|
+
{"name": "Query High-Frequency Catalogs", "sql": "SELECT id, code, name, price FROM catalogs WHERE active = true LIMIT 50;", "type": "Select Statement"},
|
|
976
|
+
{"name": "Insert Synthetic Audit Log", "sql": "INSERT INTO audit_logs(event_name, created_at) VALUES ('PERF_LOAD_RUN', NOW());", "type": "Update Statement"}
|
|
977
|
+
]
|
|
978
|
+
}
|
|
979
|
+
elif args.har and os.path.exists(args.har):
|
|
980
|
+
with open(args.har, "r", encoding="utf-8") as f:
|
|
981
|
+
har_data = json.load(f)
|
|
982
|
+
plan_name, base_host, protocol, port, samplers = parse_har_for_jmeter(har_data)
|
|
983
|
+
elif args.postman:
|
|
433
984
|
if not os.path.exists(args.postman):
|
|
434
985
|
print(f"❌ Error: Postman collection not found: {args.postman}", file=sys.stderr)
|
|
435
986
|
sys.exit(1)
|
|
@@ -466,7 +1017,24 @@ def main():
|
|
|
466
1017
|
duration=args.duration,
|
|
467
1018
|
loop_count=args.loop_count,
|
|
468
1019
|
sla_p95=args.sla_p95,
|
|
469
|
-
samplers=samplers
|
|
1020
|
+
samplers=samplers,
|
|
1021
|
+
enable_cookies=not args.no_cookies,
|
|
1022
|
+
enable_cache=not args.no_cache,
|
|
1023
|
+
csv_file=args.csv_data,
|
|
1024
|
+
csv_variables=args.csv_vars,
|
|
1025
|
+
group_transactions=args.group_transactions,
|
|
1026
|
+
weighted_mix=args.weighted_mix,
|
|
1027
|
+
deep_correlation=not args.no_deep_correlation,
|
|
1028
|
+
workload_profile=args.workload,
|
|
1029
|
+
jsr223_hmac=args.jsr223_hmac,
|
|
1030
|
+
influxdb_url=args.influxdb_url,
|
|
1031
|
+
perfmon_server=args.perfmon_server,
|
|
1032
|
+
think_time_min=args.think_time_min,
|
|
1033
|
+
think_time_max=args.think_time_max,
|
|
1034
|
+
jdbc_config=jdbc_config,
|
|
1035
|
+
ws_config=ws_config,
|
|
1036
|
+
grpc_config=grpc_config,
|
|
1037
|
+
kafka_config=kafka_config
|
|
470
1038
|
)
|
|
471
1039
|
|
|
472
1040
|
if args.output:
|
|
@@ -476,8 +1044,10 @@ def main():
|
|
|
476
1044
|
with open(args.output, "w", encoding="utf-8") as f:
|
|
477
1045
|
f.write(jmx_content)
|
|
478
1046
|
print(f"✅ [JMETER PLAN EXPORT] Successfully created Apache JMeter 5.x Plan: {args.output}")
|
|
479
|
-
print(f"📊
|
|
480
|
-
print(f"
|
|
1047
|
+
print(f"📊 Profile: {args.workload.upper()} | {args.vus} VUs | {args.ramp_up}s Ramp-up | {args.duration}s Duration | SLA < {args.sla_p95}ms")
|
|
1048
|
+
print(f"🛡️ Capabilities: Cookies={'ON' if not args.no_cookies else 'OFF'} | Cache={'ON' if not args.no_cache else 'OFF'} | DeepCorrelation={'ON' if not args.no_deep_correlation else 'OFF'} | WeightedMix={args.weighted_mix or 'None'}")
|
|
1049
|
+
print(f"🚀 Headless Execution: jmeter -n -t \"{args.output}\" -l results.jtl -e -o ./html_report")
|
|
1050
|
+
print(f"🎯 CI/CD SLA Gatekeeper: agent-qa analyze-jtl results.jtl --sla-p95 {args.sla_p95} --max-error-rate 1.0")
|
|
481
1051
|
else:
|
|
482
1052
|
print(jmx_content)
|
|
483
1053
|
|