@edgetesting/jmeter 1.0.0 → 1.0.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/package.json +1 -1
- package/src/index.js +138 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgetesting/jmeter",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "CLI wrapper to upload JMeter .jtl results to EdgeTesting, evaluate SLA thresholds, and auto-create Jira bug reports on violations",
|
|
5
5
|
"bin": {
|
|
6
6
|
"edgetesting-jmeter": "./bin/edgetesting-jmeter.js"
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const { program }
|
|
5
|
-
const FormData
|
|
6
|
-
const fetch
|
|
7
|
-
const fs
|
|
8
|
-
const path
|
|
4
|
+
const { program } = require('commander');
|
|
5
|
+
const FormData = require('form-data');
|
|
6
|
+
const fetch = require('node-fetch');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
const os = require('os');
|
|
9
11
|
|
|
10
12
|
// ── Banner ────────────────────────────────────────────────────────────────────
|
|
11
13
|
const RESET = '\x1b[0m';
|
|
@@ -34,11 +36,15 @@ function printBanner() {
|
|
|
34
36
|
// ── CLI definition ────────────────────────────────────────────────────────────
|
|
35
37
|
program
|
|
36
38
|
.name('edgetesting-jmeter')
|
|
37
|
-
.description('
|
|
38
|
-
.version('1.0.
|
|
39
|
-
.
|
|
39
|
+
.description('Run a JMeter test plan and/or upload a .jtl results file to EdgeTesting.')
|
|
40
|
+
.version('1.0.1')
|
|
41
|
+
.option(
|
|
40
42
|
'-f, --file <path>',
|
|
41
|
-
'Path to
|
|
43
|
+
'Path to an existing JMeter .jtl results file (CSV or XML). Required unless --run is used.'
|
|
44
|
+
)
|
|
45
|
+
.option(
|
|
46
|
+
'-r, --run <jmx>',
|
|
47
|
+
'Path to a JMeter .jmx test plan. Runs it headlessly, then uploads the results automatically.'
|
|
42
48
|
)
|
|
43
49
|
.requiredOption(
|
|
44
50
|
'-i, --id <automation_id>',
|
|
@@ -69,6 +75,11 @@ program
|
|
|
69
75
|
.option(
|
|
70
76
|
'--dry-run',
|
|
71
77
|
'Parse the .jtl file and print metrics without uploading to EdgeTesting.'
|
|
78
|
+
)
|
|
79
|
+
.option(
|
|
80
|
+
'--jmeter-path <path>',
|
|
81
|
+
'Path to the jmeter executable. Defaults to "jmeter" (must be on PATH).',
|
|
82
|
+
'jmeter'
|
|
72
83
|
);
|
|
73
84
|
|
|
74
85
|
program.parse(process.argv);
|
|
@@ -78,37 +89,112 @@ const opts = program.opts();
|
|
|
78
89
|
(async () => {
|
|
79
90
|
printBanner();
|
|
80
91
|
|
|
81
|
-
// ──
|
|
92
|
+
// ── Validate: must provide --file or --run ────────────────────────────────
|
|
93
|
+
if (!opts.file && !opts.run) {
|
|
94
|
+
fail('You must provide either --file <path> or --run <jmx>.');
|
|
95
|
+
log('');
|
|
96
|
+
log(` Examples:`);
|
|
97
|
+
log(` ${DIM}# Upload an existing results file${RESET}`);
|
|
98
|
+
log(` ${CYAN}npx edgetesting-jmeter --file results.jtl --id TC-100 --sla 500${RESET}`);
|
|
99
|
+
log('');
|
|
100
|
+
log(` ${DIM}# Run a test plan AND upload automatically${RESET}`);
|
|
101
|
+
log(` ${CYAN}npx edgetesting-jmeter --run my-test.jmx --id TC-100 --sla 500${RESET}`);
|
|
102
|
+
log('');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Resolve config from opts → env vars ──────────────────────────────────
|
|
82
107
|
const apiKey = opts.apiKey || process.env.EDGETESTING_API_KEY || '';
|
|
83
108
|
const apiUrl = (opts.apiUrl || process.env.EDGETESTING_API_URL || '')
|
|
84
109
|
.replace(/\/api\/v1\/?$/, '')
|
|
85
110
|
.replace(/\/$/, '');
|
|
86
111
|
|
|
87
|
-
const jtlPath = path.resolve(opts.file);
|
|
88
112
|
const automationId = opts.id;
|
|
89
|
-
const slaMs = opts.sla
|
|
90
|
-
const slaErrPct = opts.slaErrorPct
|
|
91
|
-
const issueKey = opts.issue
|
|
92
|
-
const isDryRun = opts.dryRun
|
|
93
|
-
|
|
94
|
-
// ── Validate file ────────────────────────────────────────────────────────
|
|
95
|
-
if (!fs.existsSync(jtlPath)) {
|
|
96
|
-
fail(`File not found: ${jtlPath}`);
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
113
|
+
const slaMs = opts.sla ?? null;
|
|
114
|
+
const slaErrPct = opts.slaErrorPct ?? null;
|
|
115
|
+
const issueKey = opts.issue ?? null;
|
|
116
|
+
const isDryRun = opts.dryRun ?? false;
|
|
117
|
+
const jmeterBin = opts.jmeterPath || 'jmeter';
|
|
99
118
|
|
|
100
|
-
|
|
101
|
-
|
|
119
|
+
let jtlPath;
|
|
120
|
+
let tmpJtlPath = null; // track temp file so we can clean it up
|
|
102
121
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (slaMs) info(`SLA Latency: ${bold(slaMs + 'ms')}`);
|
|
107
|
-
if (slaErrPct) info(`SLA Errors: ${bold(slaErrPct + '%')}`);
|
|
108
|
-
if (issueKey) info(`Jira Issue: ${bold(issueKey)}`);
|
|
109
|
-
log('');
|
|
122
|
+
// ── --run: execute JMeter first ──────────────────────────────────────────
|
|
123
|
+
if (opts.run) {
|
|
124
|
+
const jmxPath = path.resolve(opts.run);
|
|
110
125
|
|
|
111
|
-
|
|
126
|
+
if (!fs.existsSync(jmxPath)) {
|
|
127
|
+
fail(`Test plan not found: ${jmxPath}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Generate a temp file for the output
|
|
132
|
+
tmpJtlPath = path.join(os.tmpdir(), `edgetesting-${Date.now()}.jtl`);
|
|
133
|
+
jtlPath = tmpJtlPath;
|
|
134
|
+
|
|
135
|
+
info(`Test plan: ${bold(jmxPath)}`);
|
|
136
|
+
info(`Results file: ${bold(tmpJtlPath)} (auto-generated)`);
|
|
137
|
+
info(`Automation ID: ${bold(automationId)}`);
|
|
138
|
+
if (slaMs) info(`SLA Latency: ${bold(slaMs + 'ms')}`);
|
|
139
|
+
if (slaErrPct) info(`SLA Errors: ${bold(slaErrPct + '%')}`);
|
|
140
|
+
log('');
|
|
141
|
+
|
|
142
|
+
log(`${CYAN}▶${RESET} Running JMeter...`);
|
|
143
|
+
log(` ${DIM}${jmeterBin} -n -t ${jmxPath} -l ${tmpJtlPath}${RESET}`);
|
|
144
|
+
log('');
|
|
145
|
+
|
|
146
|
+
const result = spawnSync(
|
|
147
|
+
jmeterBin,
|
|
148
|
+
['-n', '-t', jmxPath, '-l', tmpJtlPath],
|
|
149
|
+
{ stdio: 'inherit', encoding: 'utf8' }
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (result.error) {
|
|
153
|
+
fail(`Could not launch JMeter: ${result.error.message}`);
|
|
154
|
+
log('');
|
|
155
|
+
log(` Make sure JMeter is installed and on your PATH:`);
|
|
156
|
+
log(` ${DIM}https://jmeter.apache.org/download_jmeter.cgi${RESET}`);
|
|
157
|
+
log(` ${DIM}Or pass --jmeter-path /path/to/jmeter${RESET}`);
|
|
158
|
+
log('');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (result.status !== 0) {
|
|
163
|
+
fail(`JMeter exited with code ${result.status}. Check the output above for errors.`);
|
|
164
|
+
if (tmpJtlPath && fs.existsSync(tmpJtlPath)) fs.unlinkSync(tmpJtlPath);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!fs.existsSync(jtlPath)) {
|
|
169
|
+
fail(`JMeter ran but did not produce a results file at: ${jtlPath}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ok('JMeter test completed.');
|
|
174
|
+
log('');
|
|
175
|
+
|
|
176
|
+
} else {
|
|
177
|
+
// --file mode
|
|
178
|
+
jtlPath = path.resolve(opts.file);
|
|
179
|
+
|
|
180
|
+
if (!fs.existsSync(jtlPath)) {
|
|
181
|
+
fail(`File not found: ${jtlPath}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const stat = fs.statSync(jtlPath);
|
|
186
|
+
const sizeMb = (stat.size / 1024 / 1024).toFixed(2);
|
|
187
|
+
|
|
188
|
+
info(`File: ${bold(jtlPath)}`);
|
|
189
|
+
info(`Size: ${sizeMb} MB`);
|
|
190
|
+
info(`Automation ID: ${bold(automationId)}`);
|
|
191
|
+
if (slaMs) info(`SLA Latency: ${bold(slaMs + 'ms')}`);
|
|
192
|
+
if (slaErrPct) info(`SLA Errors: ${bold(slaErrPct + '%')}`);
|
|
193
|
+
if (issueKey) info(`Jira Issue: ${bold(issueKey)}`);
|
|
194
|
+
log('');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Dry run — parse locally and exit ─────────────────────────────────────
|
|
112
198
|
if (isDryRun) {
|
|
113
199
|
warn('Dry run mode — parsing locally, not uploading.');
|
|
114
200
|
try {
|
|
@@ -116,22 +202,26 @@ const opts = program.opts();
|
|
|
116
202
|
printMetrics(metrics, slaMs, slaErrPct);
|
|
117
203
|
} catch (err) {
|
|
118
204
|
fail('Parse error: ' + err.message);
|
|
205
|
+
cleanup(tmpJtlPath);
|
|
119
206
|
process.exit(1);
|
|
120
207
|
}
|
|
208
|
+
cleanup(tmpJtlPath);
|
|
121
209
|
return;
|
|
122
210
|
}
|
|
123
211
|
|
|
124
|
-
// ── Validate credentials
|
|
212
|
+
// ── Validate credentials ──────────────────────────────────────────────────
|
|
125
213
|
if (!apiKey) {
|
|
126
214
|
fail('No API key. Set EDGETESTING_API_KEY or pass --api-key.');
|
|
215
|
+
cleanup(tmpJtlPath);
|
|
127
216
|
process.exit(1);
|
|
128
217
|
}
|
|
129
218
|
if (!apiUrl) {
|
|
130
219
|
fail('No API URL. Set EDGETESTING_API_URL or pass --api-url.');
|
|
220
|
+
cleanup(tmpJtlPath);
|
|
131
221
|
process.exit(1);
|
|
132
222
|
}
|
|
133
223
|
|
|
134
|
-
// ── Upload
|
|
224
|
+
// ── Upload ────────────────────────────────────────────────────────────────
|
|
135
225
|
info(`Uploading to: ${apiUrl}/api/v1/performance-executions`);
|
|
136
226
|
log('');
|
|
137
227
|
|
|
@@ -154,6 +244,7 @@ const opts = program.opts();
|
|
|
154
244
|
});
|
|
155
245
|
} catch (err) {
|
|
156
246
|
fail('Network error: ' + err.message);
|
|
247
|
+
cleanup(tmpJtlPath);
|
|
157
248
|
process.exit(1);
|
|
158
249
|
}
|
|
159
250
|
|
|
@@ -163,15 +254,20 @@ const opts = program.opts();
|
|
|
163
254
|
} catch {
|
|
164
255
|
const text = await response.text().catch(() => '');
|
|
165
256
|
fail(`HTTP ${response.status} — unexpected response: ${text.slice(0, 200)}`);
|
|
257
|
+
cleanup(tmpJtlPath);
|
|
166
258
|
process.exit(1);
|
|
167
259
|
}
|
|
168
260
|
|
|
169
261
|
if (!response.ok) {
|
|
170
262
|
fail(`HTTP ${response.status}: ${body.error || body.message || JSON.stringify(body)}`);
|
|
263
|
+
cleanup(tmpJtlPath);
|
|
171
264
|
process.exit(1);
|
|
172
265
|
}
|
|
173
266
|
|
|
174
|
-
//
|
|
267
|
+
// Clean up temp file after successful upload
|
|
268
|
+
cleanup(tmpJtlPath);
|
|
269
|
+
|
|
270
|
+
// ── Results ───────────────────────────────────────────────────────────────
|
|
175
271
|
printMetrics(body.metrics, slaMs, slaErrPct);
|
|
176
272
|
|
|
177
273
|
log('');
|
|
@@ -200,6 +296,14 @@ const opts = program.opts();
|
|
|
200
296
|
if (body.sla_violated) process.exit(1);
|
|
201
297
|
})();
|
|
202
298
|
|
|
299
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
function cleanup(tmpPath) {
|
|
302
|
+
if (tmpPath && fs.existsSync(tmpPath)) {
|
|
303
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
203
307
|
// ── Local parser (dry-run only) ───────────────────────────────────────────────
|
|
204
308
|
async function parseLocally(filePath) {
|
|
205
309
|
const content = fs.readFileSync(filePath, 'utf8').trim();
|