@testsmith/api-spector 0.7.7 → 0.7.9
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/bin/cli.js +4 -0
- package/out/main/chunks/{broker-client-Df4PpJqB.js → broker-client-CJBfpvbd.js} +4 -1
- package/out/main/chunks/{flow-engine-BVzjz4lJ.js → flow-engine-BxwjEy37.js} +66 -1
- package/out/main/chunks/{handle-Bz0C56Eq.js → handle-Crv1i9hb.js} +60 -0
- package/out/main/chunks/{import-BhGBiW7j.js → import-DTcZMRNu.js} +2 -2
- package/out/main/chunks/perf-report-ChaO3K0w.js +124 -0
- package/out/main/chunks/perf-run-Co_5unBR.js +578 -0
- package/out/main/chunks/{request-collection-KGPuvN7o.js → request-collection-gkOYlWml.js} +68 -11
- package/out/main/chunks/{request-exec-B-F0K-pN.js → request-exec-CgyqbNWk.js} +445 -13
- package/out/main/chunks/run-concurrent-468tH0AX.js +26 -0
- package/out/main/chunks/{soap-handler-DqIPcIQL.js → soap-handler-Brr7BPWO.js} +2 -2
- package/out/main/contract.js +6 -3
- package/out/main/flow.js +8 -5
- package/out/main/index.js +1352 -185
- package/out/main/lib.js +6 -3
- package/out/main/perf-worker.js +45 -0
- package/out/main/perf.js +253 -0
- package/out/main/runner.js +181 -7
- package/out/main/socketio.js +172 -0
- package/out/main/wsdl.js +3 -3
- package/out/preload/index.js +127 -2
- package/out/renderer/assets/{GraphQLEditor-BXpdmpn6.js → GraphQLEditor-CXvLKrnv.js} +59 -14
- package/out/renderer/assets/{SoapEditor-De6WlP6E.js → SoapEditor-7hxhISPZ.js} +3 -3
- package/out/renderer/assets/ajv-CJPwFjiP.js +6718 -0
- package/out/renderer/assets/{codemirror-F6m3Hm1M.js → codemirror-CdIFBPs2.js} +1214 -1207
- package/out/renderer/assets/{index-Gcd_t6pX.js → index-BUFSn_qD.js} +15344 -12285
- package/out/renderer/assets/index-DkhdKzQJ.css +627 -0
- package/out/renderer/index.html +4 -4
- package/package.json +12 -6
- package/out/renderer/assets/ajv-2MAg6kGF.js +0 -6153
- package/out/renderer/assets/index-CYArpqm5.css +0 -627
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const requestExec = require("./request-exec-CgyqbNWk.js");
|
|
3
|
+
const grpc = require("@grpc/grpc-js");
|
|
4
|
+
const protoLoader = require("@grpc/proto-loader");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const flowEngine = require("./flow-engine-BxwjEy37.js");
|
|
9
|
+
const requestCollection = require("./request-collection-gkOYlWml.js");
|
|
10
|
+
function _interopNamespaceDefault(e) {
|
|
11
|
+
const n2 = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
12
|
+
if (e) {
|
|
13
|
+
for (const k in e) {
|
|
14
|
+
if (k !== "default") {
|
|
15
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
16
|
+
Object.defineProperty(n2, k, d.get ? d : {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: () => e[k]
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
n2.default = e;
|
|
24
|
+
return Object.freeze(n2);
|
|
25
|
+
}
|
|
26
|
+
const grpc__namespace = /* @__PURE__ */ _interopNamespaceDefault(grpc);
|
|
27
|
+
const protoLoader__namespace = /* @__PURE__ */ _interopNamespaceDefault(protoLoader);
|
|
28
|
+
const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs);
|
|
29
|
+
const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
|
|
30
|
+
const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path);
|
|
31
|
+
const n = (v, min, max, dflt) => {
|
|
32
|
+
const x = Math.floor(Number(v));
|
|
33
|
+
if (!Number.isFinite(x)) return dflt;
|
|
34
|
+
return Math.max(min, Math.min(max, x));
|
|
35
|
+
};
|
|
36
|
+
function deriveLoad(plan) {
|
|
37
|
+
let executor = "vus";
|
|
38
|
+
let stages = [];
|
|
39
|
+
let maxVus = 0;
|
|
40
|
+
if (plan.pattern === "basic") {
|
|
41
|
+
const b = plan.basic;
|
|
42
|
+
const vus = n(b.vus, 1, 5e3, 10);
|
|
43
|
+
if (n(b.rampUpSec, 0, 36e3, 0) > 0) stages.push({ durationSec: n(b.rampUpSec, 0, 36e3, 0), target: vus, ramp: true });
|
|
44
|
+
stages.push({ durationSec: n(b.durationSec, 1, 36e3, 30), target: vus, ramp: false });
|
|
45
|
+
if (n(b.rampDownSec, 0, 36e3, 0) > 0) stages.push({ durationSec: n(b.rampDownSec, 0, 36e3, 0), target: 0, ramp: true });
|
|
46
|
+
} else if (plan.pattern === "stepping") {
|
|
47
|
+
const s = plan.stepping;
|
|
48
|
+
const start = n(s.startUsers, 1, 5e3, 5);
|
|
49
|
+
const step = n(s.stepUsers, 1, 5e3, 5);
|
|
50
|
+
const max = n(s.maxUsers, start, 5e3, 50);
|
|
51
|
+
const stepDur = n(s.stepDurationSec, 1, 36e3, 60);
|
|
52
|
+
const rampStep = n(s.rampStepSec, 0, 36e3, 0);
|
|
53
|
+
for (let level = start; ; level = Math.min(max, level + step)) {
|
|
54
|
+
if (rampStep > 0) stages.push({ durationSec: rampStep, target: level, ramp: true });
|
|
55
|
+
stages.push({ durationSec: stepDur, target: level, ramp: false });
|
|
56
|
+
if (level >= max) break;
|
|
57
|
+
}
|
|
58
|
+
const hold = n(s.holdFinalSec, 0, 36e3, 0);
|
|
59
|
+
if (hold > 0) stages.push({ durationSec: hold, target: max, ramp: false });
|
|
60
|
+
} else if (plan.pattern === "arrivals") {
|
|
61
|
+
executor = "arrival";
|
|
62
|
+
const a = plan.arrivals;
|
|
63
|
+
stages.push({ durationSec: n(a.durationSec, 1, 36e3, 60), target: n(a.rate, 1, 1e5, 50), ramp: false });
|
|
64
|
+
maxVus = n(a.maxVus, 1, 2e4, 200);
|
|
65
|
+
} else {
|
|
66
|
+
stages = (plan.custom.stages ?? []).map((st) => ({
|
|
67
|
+
durationSec: n(st.durationSec, 1, 36e3, 30),
|
|
68
|
+
target: n(st.target, 0, 2e4, 10),
|
|
69
|
+
ramp: Boolean(st.ramp)
|
|
70
|
+
}));
|
|
71
|
+
if (stages.length === 0) stages.push({ durationSec: 30, target: 10, ramp: false });
|
|
72
|
+
}
|
|
73
|
+
const totalSec = stages.reduce((sum, st) => sum + st.durationSec, 0);
|
|
74
|
+
return { executor, stages, maxVus, totalSec };
|
|
75
|
+
}
|
|
76
|
+
function targetAt(load, tSec) {
|
|
77
|
+
let start = 0;
|
|
78
|
+
let prevTarget = 0;
|
|
79
|
+
for (const st of load.stages) {
|
|
80
|
+
const end = start + st.durationSec;
|
|
81
|
+
if (tSec < end || st === load.stages[load.stages.length - 1]) {
|
|
82
|
+
if (st.ramp && st.durationSec > 0) {
|
|
83
|
+
const frac = Math.max(0, Math.min(1, (tSec - start) / st.durationSec));
|
|
84
|
+
return prevTarget + (st.target - prevTarget) * frac;
|
|
85
|
+
}
|
|
86
|
+
return st.target;
|
|
87
|
+
}
|
|
88
|
+
start = end;
|
|
89
|
+
prevTarget = st.target;
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
function thresholdActual(result, metric) {
|
|
94
|
+
switch (metric) {
|
|
95
|
+
case "iteration_p95":
|
|
96
|
+
return result.iterationLatency.p95;
|
|
97
|
+
case "iteration_p99":
|
|
98
|
+
return result.iterationLatency.p99;
|
|
99
|
+
case "req_p95":
|
|
100
|
+
return result.requestLatency.p95;
|
|
101
|
+
case "error_rate":
|
|
102
|
+
return result.iterations > 0 ? result.iterationsFailed / result.iterations * 100 : 0;
|
|
103
|
+
case "throughput":
|
|
104
|
+
return result.iterationsPerSec;
|
|
105
|
+
default:
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function compare(actual, op, value) {
|
|
110
|
+
switch (op) {
|
|
111
|
+
case "lt":
|
|
112
|
+
return actual < value;
|
|
113
|
+
case "lte":
|
|
114
|
+
return actual <= value;
|
|
115
|
+
case "gt":
|
|
116
|
+
return actual > value;
|
|
117
|
+
case "gte":
|
|
118
|
+
return actual >= value;
|
|
119
|
+
default:
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function evaluateThresholds(result, thresholds) {
|
|
124
|
+
return (thresholds ?? []).map((th) => {
|
|
125
|
+
const actual = thresholdActual(result, th.metric);
|
|
126
|
+
return { ...th, actual, passed: compare(actual, th.op, th.value) };
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function percentile(sorted, p) {
|
|
130
|
+
if (sorted.length === 0) return 0;
|
|
131
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1));
|
|
132
|
+
return sorted[idx];
|
|
133
|
+
}
|
|
134
|
+
function latency(values) {
|
|
135
|
+
const s = [...values].sort((a, b) => a - b);
|
|
136
|
+
return {
|
|
137
|
+
min: s[0] ?? 0,
|
|
138
|
+
max: s[s.length - 1] ?? 0,
|
|
139
|
+
mean: s.length ? s.reduce((a, b) => a + b, 0) / s.length : 0,
|
|
140
|
+
p50: percentile(s, 50),
|
|
141
|
+
p90: percentile(s, 90),
|
|
142
|
+
p95: percentile(s, 95),
|
|
143
|
+
p99: percentile(s, 99)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
147
|
+
let active = null;
|
|
148
|
+
function stopPerfRun() {
|
|
149
|
+
active?.stop();
|
|
150
|
+
}
|
|
151
|
+
async function runPerfTest(schedule, iterate, onProgress, progressMs = 500) {
|
|
152
|
+
const totalMs = Math.max(100, schedule.totalSec * 1e3);
|
|
153
|
+
const maxVus = Math.max(1, schedule.maxVus || 1e5);
|
|
154
|
+
const iterLat = [];
|
|
155
|
+
const reqLat = [];
|
|
156
|
+
const statusCounts = {};
|
|
157
|
+
const errors = {};
|
|
158
|
+
const stepOrder = [];
|
|
159
|
+
const stepMap = /* @__PURE__ */ new Map();
|
|
160
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
161
|
+
let iterations = 0;
|
|
162
|
+
let iterationsFailed = 0;
|
|
163
|
+
let bytes = 0;
|
|
164
|
+
let activeVus = 0;
|
|
165
|
+
let peakVus = 0;
|
|
166
|
+
let dropped = 0;
|
|
167
|
+
let stopped = false;
|
|
168
|
+
const start = Date.now();
|
|
169
|
+
active = { stop: () => {
|
|
170
|
+
stopped = true;
|
|
171
|
+
} };
|
|
172
|
+
const record = (s) => {
|
|
173
|
+
iterations += 1;
|
|
174
|
+
if (!s.ok) iterationsFailed += 1;
|
|
175
|
+
iterLat.push(s.durationMs);
|
|
176
|
+
const sec = Math.floor((Date.now() - start) / 1e3);
|
|
177
|
+
let b = buckets.get(sec);
|
|
178
|
+
if (!b) {
|
|
179
|
+
b = { count: 0, lat: [] };
|
|
180
|
+
buckets.set(sec, b);
|
|
181
|
+
}
|
|
182
|
+
b.count += 1;
|
|
183
|
+
b.lat.push(s.durationMs);
|
|
184
|
+
for (const step of s.steps) {
|
|
185
|
+
let e = stepMap.get(step.name);
|
|
186
|
+
if (!e) {
|
|
187
|
+
e = { count: 0, failed: 0, lat: [], status: {} };
|
|
188
|
+
stepMap.set(step.name, e);
|
|
189
|
+
stepOrder.push(step.name);
|
|
190
|
+
}
|
|
191
|
+
e.count += 1;
|
|
192
|
+
if (!step.ok) e.failed += 1;
|
|
193
|
+
e.lat.push(step.ms);
|
|
194
|
+
reqLat.push(step.ms);
|
|
195
|
+
bytes += step.bytes;
|
|
196
|
+
const key = step.statusLabel ?? (step.status > 0 ? String(step.status) : "error");
|
|
197
|
+
e.status[key] = (e.status[key] ?? 0) + 1;
|
|
198
|
+
statusCounts[key] = (statusCounts[key] ?? 0) + 1;
|
|
199
|
+
if (step.error) errors[step.error] = (errors[step.error] ?? 0) + 1;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const runOne = async () => {
|
|
203
|
+
activeVus += 1;
|
|
204
|
+
if (activeVus > peakVus) peakVus = activeVus;
|
|
205
|
+
const t0 = Date.now();
|
|
206
|
+
let controller;
|
|
207
|
+
let to;
|
|
208
|
+
if (schedule.timeoutMs) {
|
|
209
|
+
controller = new AbortController();
|
|
210
|
+
to = setTimeout(() => controller.abort(), schedule.timeoutMs);
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
record(await iterate(controller?.signal));
|
|
214
|
+
} catch (e) {
|
|
215
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
216
|
+
record({ ok: false, durationMs: Date.now() - t0, steps: [{ name: "(iteration)", ok: false, status: 0, ms: Date.now() - t0, bytes: 0, error: msg || "Iteration failed" }] });
|
|
217
|
+
} finally {
|
|
218
|
+
if (to) clearTimeout(to);
|
|
219
|
+
activeVus -= 1;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const emit = () => {
|
|
223
|
+
if (!onProgress) return;
|
|
224
|
+
const elapsedMs = Date.now() - start;
|
|
225
|
+
onProgress({
|
|
226
|
+
running: true,
|
|
227
|
+
elapsedMs,
|
|
228
|
+
iterations,
|
|
229
|
+
iterationsFailed,
|
|
230
|
+
activeVus,
|
|
231
|
+
targetNow: targetAt(schedule, elapsedMs / 1e3),
|
|
232
|
+
iterationsPerSec: elapsedMs > 0 ? iterations / elapsedMs * 1e3 : 0,
|
|
233
|
+
p95: percentile([...iterLat].sort((a, b) => a - b), 95)
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
const progressTimer = setInterval(emit, progressMs);
|
|
237
|
+
if (schedule.executor === "vus") {
|
|
238
|
+
const workers = [];
|
|
239
|
+
let retire = 0;
|
|
240
|
+
let settled = 0;
|
|
241
|
+
const thinkMin = Math.max(0, schedule.thinkMinMs ?? 0);
|
|
242
|
+
const thinkMax = Math.max(thinkMin, schedule.thinkMaxMs ?? 0);
|
|
243
|
+
const worker = async () => {
|
|
244
|
+
while (!stopped && Date.now() - start < totalMs) {
|
|
245
|
+
if (retire > 0) {
|
|
246
|
+
retire -= 1;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
await runOne();
|
|
250
|
+
if (thinkMax > 0 && !stopped) await sleep(thinkMin + Math.random() * (thinkMax - thinkMin));
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const reconcile = () => {
|
|
254
|
+
if (stopped) return;
|
|
255
|
+
const desired = Math.max(0, Math.round(targetAt(schedule, (Date.now() - start) / 1e3)));
|
|
256
|
+
const effective = workers.length - settled - retire;
|
|
257
|
+
if (effective < desired) {
|
|
258
|
+
for (let i = 0; i < desired - effective; i++) workers.push(worker().then(() => {
|
|
259
|
+
settled += 1;
|
|
260
|
+
}));
|
|
261
|
+
} else if (effective > desired) {
|
|
262
|
+
retire += effective - desired;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const tick = setInterval(reconcile, 200);
|
|
266
|
+
reconcile();
|
|
267
|
+
while (!stopped && Date.now() - start < totalMs) await sleep(100);
|
|
268
|
+
clearInterval(tick);
|
|
269
|
+
stopped = true;
|
|
270
|
+
await Promise.allSettled(workers);
|
|
271
|
+
} else {
|
|
272
|
+
let acc = 0;
|
|
273
|
+
let last = Date.now();
|
|
274
|
+
const inflight = [];
|
|
275
|
+
while (!stopped && Date.now() - start < totalMs) {
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
const dt = (now - last) / 1e3;
|
|
278
|
+
last = now;
|
|
279
|
+
acc += targetAt(schedule, (now - start) / 1e3) * dt;
|
|
280
|
+
while (acc >= 1) {
|
|
281
|
+
acc -= 1;
|
|
282
|
+
if (activeVus >= maxVus) {
|
|
283
|
+
dropped += 1;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
inflight.push(runOne());
|
|
287
|
+
}
|
|
288
|
+
await sleep(50);
|
|
289
|
+
}
|
|
290
|
+
stopped = true;
|
|
291
|
+
await Promise.allSettled(inflight);
|
|
292
|
+
}
|
|
293
|
+
clearInterval(progressTimer);
|
|
294
|
+
active = null;
|
|
295
|
+
const durationMs = Date.now() - start;
|
|
296
|
+
const steps = stepOrder.map((name) => {
|
|
297
|
+
const e = stepMap.get(name);
|
|
298
|
+
return { name, count: e.count, failed: e.failed, latency: latency(e.lat), statusCounts: e.status };
|
|
299
|
+
});
|
|
300
|
+
const totalRequests = reqLat.length;
|
|
301
|
+
const series = [...buckets.entries()].sort((a, b) => a[0] - b[0]).map(([second, b]) => ({ second, rps: b.count, p95: percentile([...b.lat].sort((x, y) => x - y), 95) }));
|
|
302
|
+
return {
|
|
303
|
+
mode: schedule.mode,
|
|
304
|
+
pattern: schedule.pattern,
|
|
305
|
+
executor: schedule.executor,
|
|
306
|
+
iterations,
|
|
307
|
+
iterationsFailed,
|
|
308
|
+
durationMs,
|
|
309
|
+
iterationsPerSec: durationMs > 0 ? iterations / durationMs * 1e3 : 0,
|
|
310
|
+
requestsPerSec: durationMs > 0 ? totalRequests / durationMs * 1e3 : 0,
|
|
311
|
+
iterationLatency: latency(iterLat),
|
|
312
|
+
requestLatency: latency(reqLat),
|
|
313
|
+
bytes,
|
|
314
|
+
bytesPerSec: durationMs > 0 ? bytes / durationMs * 1e3 : 0,
|
|
315
|
+
statusCounts,
|
|
316
|
+
errors,
|
|
317
|
+
steps,
|
|
318
|
+
series,
|
|
319
|
+
peakVus,
|
|
320
|
+
droppedArrivals: dropped,
|
|
321
|
+
stopped,
|
|
322
|
+
thresholds: [],
|
|
323
|
+
passed: true
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const LOADER_OPTS = { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true };
|
|
327
|
+
function resolveProtoFile(src) {
|
|
328
|
+
const importPaths = (src.importPaths ?? []).filter(Boolean);
|
|
329
|
+
if (src.protoPath) return { file: src.protoPath, includeDirs: [path__namespace.dirname(src.protoPath), ...importPaths] };
|
|
330
|
+
if (src.protoSource?.trim()) {
|
|
331
|
+
const dir = fs__namespace.mkdtempSync(path__namespace.join(os__namespace.tmpdir(), "apispector-proto-"));
|
|
332
|
+
const file = path__namespace.join(dir, "main.proto");
|
|
333
|
+
fs__namespace.writeFileSync(file, src.protoSource, "utf8");
|
|
334
|
+
return { file, includeDirs: [dir, ...importPaths] };
|
|
335
|
+
}
|
|
336
|
+
throw new Error("No proto source: paste a .proto or provide a file path.");
|
|
337
|
+
}
|
|
338
|
+
function resolveService(grpcObj, serviceName) {
|
|
339
|
+
let node = grpcObj;
|
|
340
|
+
for (const part of serviceName.split(".")) {
|
|
341
|
+
node = node?.[part];
|
|
342
|
+
if (!node) throw new Error(`Service "${serviceName}" not found in the proto.`);
|
|
343
|
+
}
|
|
344
|
+
if (typeof node !== "function") throw new Error(`"${serviceName}" is not a service.`);
|
|
345
|
+
return node;
|
|
346
|
+
}
|
|
347
|
+
function normalizeTarget(url) {
|
|
348
|
+
return url.replace(/^[a-z0-9+.-]+:\/\//i, "").replace(/\/+$/, "");
|
|
349
|
+
}
|
|
350
|
+
function statusName(code) {
|
|
351
|
+
return Object.keys(grpc__namespace.status).find((k) => grpc__namespace.status[k] === code) ?? String(code);
|
|
352
|
+
}
|
|
353
|
+
function createGrpcInvoker(cfg) {
|
|
354
|
+
const { file, includeDirs } = resolveProtoFile(cfg);
|
|
355
|
+
const pkgDef = protoLoader__namespace.loadSync(file, { ...LOADER_OPTS, includeDirs });
|
|
356
|
+
const grpcObj = grpc__namespace.loadPackageDefinition(pkgDef);
|
|
357
|
+
const ServiceCtor = resolveService(grpcObj, cfg.serviceName);
|
|
358
|
+
const serviceDef = ServiceCtor.service;
|
|
359
|
+
const methodKey = Object.keys(serviceDef).find((k) => k === cfg.methodName) ?? Object.keys(serviceDef).find((k) => k.toLowerCase() === cfg.methodName.toLowerCase());
|
|
360
|
+
if (!methodKey) throw new Error(`Method "${cfg.methodName}" not found on ${cfg.serviceName}.`);
|
|
361
|
+
const def = serviceDef[methodKey];
|
|
362
|
+
if (def.requestStream || def.responseStream) throw new Error("Only unary gRPC methods can be load tested.");
|
|
363
|
+
const request = cfg.message?.trim() ? JSON.parse(cfg.message) : {};
|
|
364
|
+
const creds = cfg.plaintext ? grpc__namespace.credentials.createInsecure() : grpc__namespace.credentials.createSsl();
|
|
365
|
+
const client = new ServiceCtor(normalizeTarget(cfg.target), creds);
|
|
366
|
+
const metadata = new grpc__namespace.Metadata();
|
|
367
|
+
for (const [k, v] of Object.entries(cfg.metadata ?? {})) metadata.set(k, v);
|
|
368
|
+
const method = client[methodKey].bind(client);
|
|
369
|
+
return {
|
|
370
|
+
invoke() {
|
|
371
|
+
return new Promise((resolve) => {
|
|
372
|
+
try {
|
|
373
|
+
method(request, metadata, (err, response) => {
|
|
374
|
+
if (err) resolve({ ok: false, code: err.code ?? grpc__namespace.status.UNKNOWN, codeName: statusName(err.code ?? grpc__namespace.status.UNKNOWN), bytes: 0, error: err.details || err.message });
|
|
375
|
+
else resolve({ ok: true, code: grpc__namespace.status.OK, codeName: "OK", bytes: Buffer.byteLength(JSON.stringify(response ?? {}), "utf8") });
|
|
376
|
+
});
|
|
377
|
+
} catch (e) {
|
|
378
|
+
resolve({ ok: false, code: grpc__namespace.status.UNKNOWN, codeName: "UNKNOWN", bytes: 0, error: e instanceof Error ? e.message : String(e) });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
},
|
|
382
|
+
close() {
|
|
383
|
+
try {
|
|
384
|
+
client.close();
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
async function buildFlowDeps(payload) {
|
|
391
|
+
const envVars = payload.resolvedEnvVars ?? await requestExec.buildEnvVars(payload.environment ?? null);
|
|
392
|
+
const globals = { ...payload.globals, ...requestExec.getGlobals() };
|
|
393
|
+
const dispatcher = await requestExec.buildDispatcher(payload.proxy, payload.tls);
|
|
394
|
+
const flow = flowEngine.normalizeFlow(payload.flow);
|
|
395
|
+
const flows = (payload.flows ?? []).map(flowEngine.normalizeFlow);
|
|
396
|
+
const colById = new Map((payload.collections ?? []).map((c) => [c.id, c]));
|
|
397
|
+
const deps = {
|
|
398
|
+
runRequest: async (ref, input) => {
|
|
399
|
+
const col = colById.get(ref.collectionId);
|
|
400
|
+
const original = col?.requests[ref.requestId];
|
|
401
|
+
if (!col || !original) return null;
|
|
402
|
+
const req = JSON.parse(JSON.stringify(original));
|
|
403
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(original.id, col);
|
|
404
|
+
if (!requestCollection.authIsConfigured(req.auth) && inherited.auth && inherited.auth.type !== "none") req.auth = inherited.auth;
|
|
405
|
+
const inheritedHeaders = inherited.headers.filter((h) => h.enabled && h.key);
|
|
406
|
+
if (inheritedHeaders.length) req.headers = [...inheritedHeaders, ...req.headers ?? []];
|
|
407
|
+
const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await requestExec.executeRunnerRequest({
|
|
408
|
+
req,
|
|
409
|
+
collectionVars: { ...col.collectionVariables ?? {}, ...input.collectionVars },
|
|
410
|
+
envVars: input.envVars,
|
|
411
|
+
globals: input.globals,
|
|
412
|
+
localVars: input.localVars,
|
|
413
|
+
dispatcher,
|
|
414
|
+
piiMaskPatterns: [],
|
|
415
|
+
proxy: payload.proxy,
|
|
416
|
+
tls: payload.tls
|
|
417
|
+
});
|
|
418
|
+
const rr = result.receivedResponse;
|
|
419
|
+
const response = rr ? { status: rr.status, statusText: rr.statusText, headers: rr.headers, body: rr.body, bodySize: rr.body?.length ?? 0, durationMs: result.durationMs ?? 0 } : null;
|
|
420
|
+
return {
|
|
421
|
+
envVars: updatedEnvVars,
|
|
422
|
+
collectionVars: updatedCollectionVars,
|
|
423
|
+
globals: updatedGlobals,
|
|
424
|
+
localVars: updatedLocalVars,
|
|
425
|
+
response,
|
|
426
|
+
status: result.status,
|
|
427
|
+
httpStatus: result.httpStatus,
|
|
428
|
+
durationMs: result.durationMs,
|
|
429
|
+
error: result.error,
|
|
430
|
+
testResults: result.testResults,
|
|
431
|
+
name: result.name,
|
|
432
|
+
method: result.method,
|
|
433
|
+
resolvedUrl: result.resolvedUrl
|
|
434
|
+
};
|
|
435
|
+
},
|
|
436
|
+
runScript: async (code, input) => {
|
|
437
|
+
const out = await requestExec.runScript(code, { envVars: input.envVars, collectionVars: input.collectionVars, globals: input.globals, localVars: input.localVars, response: input.response ?? void 0, piiMaskPatterns: [], data: input.data });
|
|
438
|
+
return { envVars: out.updatedEnvVars, collectionVars: out.updatedCollectionVars, globals: out.updatedGlobals, localVars: out.updatedLocalVars, testResults: out.testResults, consoleOutput: out.consoleOutput, error: out.error };
|
|
439
|
+
},
|
|
440
|
+
evalExpression: async (code, input) => {
|
|
441
|
+
const out = await requestExec.evaluateExpression(code, { envVars: input.envVars, collectionVars: input.collectionVars, globals: input.globals, localVars: input.localVars, response: input.response ?? void 0, piiMaskPatterns: [], data: input.data });
|
|
442
|
+
return { envVars: out.updatedEnvVars, collectionVars: out.updatedCollectionVars, globals: out.updatedGlobals, localVars: out.updatedLocalVars, value: out.value, consoleOutput: out.consoleOutput, error: out.error };
|
|
443
|
+
},
|
|
444
|
+
resolveFlow: (flowId) => flows.find((f) => f.id === flowId) ?? null,
|
|
445
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
446
|
+
};
|
|
447
|
+
return { deps, flow, envVars, globals };
|
|
448
|
+
}
|
|
449
|
+
async function runPerfPlan(payload, onProgress) {
|
|
450
|
+
const plan = payload.plan;
|
|
451
|
+
const load = deriveLoad(plan);
|
|
452
|
+
const schedule = {
|
|
453
|
+
...load,
|
|
454
|
+
mode: plan.target.kind,
|
|
455
|
+
pattern: plan.pattern,
|
|
456
|
+
timeoutMs: plan.timeoutSec > 0 ? plan.timeoutSec * 1e3 : void 0,
|
|
457
|
+
thinkMinMs: Math.max(0, plan.thinkTime?.minSec ?? 0) * 1e3,
|
|
458
|
+
thinkMaxMs: Math.max(0, plan.thinkTime?.maxSec ?? 0) * 1e3
|
|
459
|
+
};
|
|
460
|
+
let iterate;
|
|
461
|
+
let grpcInvoker;
|
|
462
|
+
if (plan.target.kind === "request") {
|
|
463
|
+
if (!payload.request) throw new Error("No request selected for this plan");
|
|
464
|
+
const req = payload.request;
|
|
465
|
+
requestExec.applyRequestDefaults(req);
|
|
466
|
+
const dynamicVars = await requestExec.buildDynamicVars();
|
|
467
|
+
const vars = requestExec.mergeVars(payload.vars ?? {}, {}, {}, {}, dynamicVars, {});
|
|
468
|
+
if (req.protocol === "grpc") {
|
|
469
|
+
const g = req.body?.grpc;
|
|
470
|
+
if (!g || !g.serviceName || !g.methodName) throw new Error("This gRPC request has no service/method configured");
|
|
471
|
+
const ip = (s) => requestExec.interpolate(s ?? "", vars);
|
|
472
|
+
const metadata = {};
|
|
473
|
+
for (const m of g.metadata ?? []) if (m.enabled !== false && m.key) metadata[ip(m.key)] = ip(m.value);
|
|
474
|
+
grpcInvoker = createGrpcInvoker({
|
|
475
|
+
target: ip(req.url),
|
|
476
|
+
protoSource: g.protoSource,
|
|
477
|
+
protoPath: g.protoPath,
|
|
478
|
+
importPaths: g.importPaths,
|
|
479
|
+
serviceName: g.serviceName,
|
|
480
|
+
methodName: g.methodName,
|
|
481
|
+
message: ip(g.message ?? "{}"),
|
|
482
|
+
metadata,
|
|
483
|
+
plaintext: g.plaintext
|
|
484
|
+
});
|
|
485
|
+
const label = `${g.serviceName}/${g.methodName}`;
|
|
486
|
+
iterate = async () => {
|
|
487
|
+
const t0 = Date.now();
|
|
488
|
+
const r = await grpcInvoker.invoke();
|
|
489
|
+
const ms = Date.now() - t0;
|
|
490
|
+
return { ok: r.ok, durationMs: ms, steps: [{ name: label, ok: r.ok, status: r.code, statusLabel: r.codeName, ms, bytes: r.bytes, error: r.error }] };
|
|
491
|
+
};
|
|
492
|
+
} else {
|
|
493
|
+
const resolvedUrl = requestExec.buildUrl(req.url, req.params, vars);
|
|
494
|
+
const dispatcher = await requestExec.buildDispatcher(payload.proxy, payload.tls);
|
|
495
|
+
iterate = async (signal) => {
|
|
496
|
+
const t0 = Date.now();
|
|
497
|
+
try {
|
|
498
|
+
const ex = await requestExec.performHttpExchange({ req, vars, resolvedUrl, dispatcher, proxy: payload.proxy, tls: payload.tls, signal });
|
|
499
|
+
return { ok: ex.status < 400, durationMs: ex.durationMs, steps: [{ name: req.name || "request", ok: ex.status < 400, status: ex.status, ms: ex.durationMs, bytes: Buffer.byteLength(ex.responseBody ?? "", "utf8") }] };
|
|
500
|
+
} catch (e) {
|
|
501
|
+
const msg = e instanceof Error ? e.name === "AbortError" ? "Timeout" : e.message : String(e);
|
|
502
|
+
return { ok: false, durationMs: Date.now() - t0, steps: [{ name: req.name || "request", ok: false, status: 0, ms: Date.now() - t0, bytes: 0, error: msg || "Request failed" }] };
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
} else if (plan.target.kind === "sequence") {
|
|
507
|
+
if (!payload.collection) throw new Error("No collection selected for this plan");
|
|
508
|
+
const collection = payload.collection;
|
|
509
|
+
const envVars = payload.resolvedEnvVars ?? await requestExec.buildEnvVars(payload.environment ?? null);
|
|
510
|
+
const globals = { ...payload.globals, ...requestExec.getGlobals() };
|
|
511
|
+
const dispatcher = await requestExec.buildDispatcher(payload.proxy, payload.tls);
|
|
512
|
+
const items = requestCollection.buildRunPlan(collection, plan.target.folderId ?? null, []);
|
|
513
|
+
if (items.length === 0) throw new Error("This collection or folder has no requests to run");
|
|
514
|
+
iterate = async () => {
|
|
515
|
+
let env = { ...envVars };
|
|
516
|
+
let col = { ...collection.collectionVariables ?? {} };
|
|
517
|
+
let glob = { ...globals };
|
|
518
|
+
let loc = {};
|
|
519
|
+
const steps = [];
|
|
520
|
+
const t0 = Date.now();
|
|
521
|
+
for (const item of items) {
|
|
522
|
+
const original = item.request;
|
|
523
|
+
const req = JSON.parse(JSON.stringify(original));
|
|
524
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(original.id, collection);
|
|
525
|
+
if (!requestCollection.authIsConfigured(req.auth) && inherited.auth && inherited.auth.type !== "none") req.auth = inherited.auth;
|
|
526
|
+
const inheritedHeaders = inherited.headers.filter((h) => h.enabled && h.key);
|
|
527
|
+
if (inheritedHeaders.length) req.headers = [...inheritedHeaders, ...req.headers ?? []];
|
|
528
|
+
const out = await requestExec.executeRunnerRequest({
|
|
529
|
+
req,
|
|
530
|
+
collectionVars: col,
|
|
531
|
+
envVars: env,
|
|
532
|
+
globals: glob,
|
|
533
|
+
localVars: { ...loc, ...item.dataRow ?? {} },
|
|
534
|
+
folderVars: item.folderVars,
|
|
535
|
+
dispatcher,
|
|
536
|
+
piiMaskPatterns: [],
|
|
537
|
+
proxy: payload.proxy,
|
|
538
|
+
tls: payload.tls
|
|
539
|
+
});
|
|
540
|
+
env = out.updatedEnvVars;
|
|
541
|
+
col = out.updatedCollectionVars;
|
|
542
|
+
glob = out.updatedGlobals;
|
|
543
|
+
loc = out.updatedLocalVars;
|
|
544
|
+
const ok2 = out.result.status === "passed" || out.result.status === "skipped";
|
|
545
|
+
if (!item.isHook) steps.push({ name: original.name, ok: ok2, status: out.result.httpStatus ?? 0, ms: out.result.durationMs ?? 0, bytes: out.result.receivedResponse?.body?.length ?? 0, error: ok2 ? void 0 : out.result.error });
|
|
546
|
+
}
|
|
547
|
+
const ok = steps.every((s) => s.ok);
|
|
548
|
+
return { ok, durationMs: Date.now() - t0, steps: steps.length ? steps : [{ name: "(sequence)", ok, status: 0, ms: Date.now() - t0, bytes: 0 }] };
|
|
549
|
+
};
|
|
550
|
+
} else {
|
|
551
|
+
if (!payload.flow) throw new Error("No flow selected for this plan");
|
|
552
|
+
const { deps, flow, envVars, globals } = await buildFlowDeps(payload);
|
|
553
|
+
iterate = async () => {
|
|
554
|
+
const summary = await flowEngine.runFlow(flow, deps, { envVars, globals });
|
|
555
|
+
const steps = summary.blocks.filter((b) => b.type === "request").map((b) => ({
|
|
556
|
+
name: b.label || b.resolvedUrl || b.blockId,
|
|
557
|
+
ok: b.status === "passed" || b.status === "done",
|
|
558
|
+
status: b.httpStatus ?? 0,
|
|
559
|
+
ms: b.durationMs ?? 0,
|
|
560
|
+
bytes: b.http?.bodySize ?? 0,
|
|
561
|
+
error: b.status === "error" || b.status === "failed" ? b.error || "Step failed" : void 0
|
|
562
|
+
}));
|
|
563
|
+
const ok = summary.failed === 0 && summary.errors === 0;
|
|
564
|
+
return { ok, durationMs: summary.durationMs, steps: steps.length ? steps : [{ name: "(flow)", ok, status: 0, ms: summary.durationMs, bytes: 0, error: ok ? void 0 : "Flow failed" }] };
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
let res;
|
|
568
|
+
try {
|
|
569
|
+
res = await runPerfTest(schedule, iterate, onProgress);
|
|
570
|
+
} finally {
|
|
571
|
+
grpcInvoker?.close();
|
|
572
|
+
}
|
|
573
|
+
const thresholds = evaluateThresholds(res, plan.thresholds ?? []);
|
|
574
|
+
return { ...res, thresholds, passed: thresholds.length ? thresholds.every((t) => t.passed) : true };
|
|
575
|
+
}
|
|
576
|
+
exports.deriveLoad = deriveLoad;
|
|
577
|
+
exports.runPerfPlan = runPerfPlan;
|
|
578
|
+
exports.stopPerfRun = stopPerfRun;
|