@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.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 } = require('commander');
5
- const FormData = require('form-data');
6
- const fetch = require('node-fetch');
7
- const fs = require('fs');
8
- const path = require('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('Upload a JMeter .jtl results file to EdgeTesting and evaluate SLA thresholds.')
38
- .version('1.0.0')
39
- .requiredOption(
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 the JMeter .jtl results file (CSV or XML)'
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
- // ── Resolve config from opts env vars ─────────────────────────────────
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 ?? null;
90
- const slaErrPct = opts.slaErrorPct ?? null;
91
- const issueKey = opts.issue ?? null;
92
- const isDryRun = opts.dryRun ?? false;
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
- const stat = fs.statSync(jtlPath);
101
- const sizeMb = (stat.size / 1024 / 1024).toFixed(2);
119
+ let jtlPath;
120
+ let tmpJtlPath = null; // track temp file so we can clean it up
102
121
 
103
- info(`File: ${bold(jtlPath)}`);
104
- info(`Size: ${sizeMb} MB`);
105
- info(`Automation ID: ${bold(automationId)}`);
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
- // ── Dry run — parse locally and exit ────────────────────────────────────
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
- // ── Results ──────────────────────────────────────────────────────────────
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();