@testsmith/api-spector 0.7.6 → 0.7.8
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-5IyCTx40.js → broker-client-BYpWeOWT.js} +4 -1
- package/out/main/chunks/{flow-engine-BVzjz4lJ.js → flow-engine-BxwjEy37.js} +66 -1
- package/out/main/chunks/{handle-DRmKOL-s.js → handle-Crv1i9hb.js} +61 -0
- package/out/main/chunks/{import-B5T5wDu7.js → import-DTcZMRNu.js} +2 -2
- package/out/main/chunks/perf-report-ChaO3K0w.js +124 -0
- package/out/main/chunks/perf-run-uZBqnG3S.js +578 -0
- package/out/main/chunks/{request-collection-KGPuvN7o.js → request-collection-gkOYlWml.js} +68 -11
- package/out/main/chunks/{request-exec-CwJ8_OKC.js → request-exec-Cv2jkrO1.js} +458 -12
- package/out/main/chunks/run-concurrent-468tH0AX.js +26 -0
- package/out/main/chunks/{soap-handler-DTfFnzNY.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 +1338 -179
- 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 -0
- package/out/renderer/assets/{GraphQLEditor-CDLI1OHR.js → GraphQLEditor-Dv2K4wdd.js} +59 -14
- package/out/renderer/assets/{SoapEditor-CSUFJk7c.js → SoapEditor-Cnz3M4Dz.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-8bbf24uI.js → index-CtCdFiRx.js} +16957 -13834
- 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
package/out/main/lib.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const mockServer = require("./chunks/mock-server-C4kTFkod.js");
|
|
4
|
-
const requestExec = require("./chunks/request-exec-
|
|
5
|
-
const flowEngine = require("./chunks/flow-engine-
|
|
4
|
+
const requestExec = require("./chunks/request-exec-Cv2jkrO1.js");
|
|
5
|
+
const flowEngine = require("./chunks/flow-engine-BxwjEy37.js");
|
|
6
6
|
require("node:http");
|
|
7
7
|
require("node:crypto");
|
|
8
8
|
require("node:vm");
|
|
@@ -11,13 +11,16 @@ require("@xmldom/xmldom");
|
|
|
11
11
|
require("undici");
|
|
12
12
|
require("fs/promises");
|
|
13
13
|
require("tls");
|
|
14
|
-
require("./chunks/handle-
|
|
14
|
+
require("./chunks/handle-Crv1i9hb.js");
|
|
15
15
|
require("node:fs");
|
|
16
16
|
require("node:os");
|
|
17
17
|
require("node:path");
|
|
18
18
|
require("node:fs/promises");
|
|
19
19
|
require("tv4");
|
|
20
20
|
require("jsonpath-plus");
|
|
21
|
+
require("mqtt");
|
|
22
|
+
require("ws");
|
|
23
|
+
require("socket.io-client");
|
|
21
24
|
require("ajv");
|
|
22
25
|
const refKey = (ref) => `${ref.collectionId}:${ref.requestId}`;
|
|
23
26
|
async function runFlowHeadless(input) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const perfRun = require("./chunks/perf-run-uZBqnG3S.js");
|
|
3
|
+
const requestExec = require("./chunks/request-exec-Cv2jkrO1.js");
|
|
4
|
+
require("@grpc/grpc-js");
|
|
5
|
+
require("@grpc/proto-loader");
|
|
6
|
+
require("node:fs");
|
|
7
|
+
require("node:os");
|
|
8
|
+
require("node:path");
|
|
9
|
+
require("./chunks/flow-engine-BxwjEy37.js");
|
|
10
|
+
require("./chunks/request-collection-gkOYlWml.js");
|
|
11
|
+
require("uuid");
|
|
12
|
+
require("undici");
|
|
13
|
+
require("fs/promises");
|
|
14
|
+
require("tls");
|
|
15
|
+
require("./chunks/handle-Crv1i9hb.js");
|
|
16
|
+
require("node:crypto");
|
|
17
|
+
require("node:fs/promises");
|
|
18
|
+
require("dayjs");
|
|
19
|
+
require("node:vm");
|
|
20
|
+
require("tv4");
|
|
21
|
+
require("jsonpath-plus");
|
|
22
|
+
require("@xmldom/xmldom");
|
|
23
|
+
require("mqtt");
|
|
24
|
+
require("ws");
|
|
25
|
+
require("socket.io-client");
|
|
26
|
+
require("node:http");
|
|
27
|
+
require("ajv");
|
|
28
|
+
const port = process.parentPort;
|
|
29
|
+
port.on("message", (e) => {
|
|
30
|
+
const msg = e.data;
|
|
31
|
+
if (msg.type === "stop") {
|
|
32
|
+
perfRun.stopPerfRun();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (msg.type !== "run" || !msg.payload) return;
|
|
36
|
+
void (async () => {
|
|
37
|
+
try {
|
|
38
|
+
if (msg.payload.secretsConfig) requestExec.setSecretsConfig(msg.payload.secretsConfig);
|
|
39
|
+
const result = await perfRun.runPerfPlan(msg.payload, (p) => port.postMessage({ type: "progress", p }));
|
|
40
|
+
port.postMessage({ type: "result", result });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
port.postMessage({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
});
|
package/out/main/perf.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const promises = require("node:fs/promises");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const requestExec = require("./chunks/request-exec-Cv2jkrO1.js");
|
|
6
|
+
const environments = require("./chunks/environments-iM3SUM-4.js");
|
|
7
|
+
const flowEngine = require("./chunks/flow-engine-BxwjEy37.js");
|
|
8
|
+
const perfRun = require("./chunks/perf-run-uZBqnG3S.js");
|
|
9
|
+
const perfReport = require("./chunks/perf-report-ChaO3K0w.js");
|
|
10
|
+
const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
|
|
11
|
+
require("undici");
|
|
12
|
+
require("fs/promises");
|
|
13
|
+
require("tls");
|
|
14
|
+
require("./chunks/handle-Crv1i9hb.js");
|
|
15
|
+
require("node:fs");
|
|
16
|
+
require("node:os");
|
|
17
|
+
require("node:crypto");
|
|
18
|
+
require("dayjs");
|
|
19
|
+
require("node:vm");
|
|
20
|
+
require("tv4");
|
|
21
|
+
require("jsonpath-plus");
|
|
22
|
+
require("@xmldom/xmldom");
|
|
23
|
+
require("mqtt");
|
|
24
|
+
require("ws");
|
|
25
|
+
require("socket.io-client");
|
|
26
|
+
require("node:http");
|
|
27
|
+
require("ajv");
|
|
28
|
+
require("@grpc/grpc-js");
|
|
29
|
+
require("@grpc/proto-loader");
|
|
30
|
+
require("./chunks/request-collection-gkOYlWml.js");
|
|
31
|
+
require("uuid");
|
|
32
|
+
require("./chunks/data-files-BYqhsoPe.js");
|
|
33
|
+
function parseDurationSec(s) {
|
|
34
|
+
if (s == null) return void 0;
|
|
35
|
+
const str = String(s).trim();
|
|
36
|
+
if (/^\d+(\.\d+)?$/.test(str)) return Number(str);
|
|
37
|
+
let total = 0;
|
|
38
|
+
let matched = false;
|
|
39
|
+
for (const m of str.matchAll(/(\d+)\s*(h|m|s)/gi)) {
|
|
40
|
+
matched = true;
|
|
41
|
+
total += Number(m[1]) * (m[2].toLowerCase() === "h" ? 3600 : m[2].toLowerCase() === "m" ? 60 : 1);
|
|
42
|
+
}
|
|
43
|
+
return matched ? total : void 0;
|
|
44
|
+
}
|
|
45
|
+
function parseThreshold(spec) {
|
|
46
|
+
const m = spec.trim().match(/^(\S+)\s*(lt|lte|gt|gte|<=|>=|<|>)\s*([\d.]+)$/i);
|
|
47
|
+
if (!m) return null;
|
|
48
|
+
const opMap = { "<": "lt", "<=": "lte", ">": "gt", ">=": "gte", lt: "lt", lte: "lte", gt: "gt", gte: "gte" };
|
|
49
|
+
const metric = m[1];
|
|
50
|
+
const op = opMap[m[2].toLowerCase()];
|
|
51
|
+
const valid = ["iteration_p95", "iteration_p99", "req_p95", "error_rate", "throughput"];
|
|
52
|
+
if (!valid.includes(metric) || !op) return null;
|
|
53
|
+
return { metric, op, value: Number(m[3]) };
|
|
54
|
+
}
|
|
55
|
+
function fmtMs(v) {
|
|
56
|
+
return v < 1 ? "<1 ms" : v < 1e3 ? `${Math.round(v)} ms` : `${(v / 1e3).toFixed(2)} s`;
|
|
57
|
+
}
|
|
58
|
+
function applyOverrides(plan, args) {
|
|
59
|
+
const p = JSON.parse(JSON.stringify(plan));
|
|
60
|
+
const num = (k) => {
|
|
61
|
+
const v = args[k];
|
|
62
|
+
return typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)) ? Number(v) : void 0;
|
|
63
|
+
};
|
|
64
|
+
const dur = (k) => typeof args[k] === "string" ? parseDurationSec(args[k]) : void 0;
|
|
65
|
+
if (typeof args.pattern === "string" && ["basic", "stepping", "arrivals", "custom"].includes(args.pattern)) p.pattern = args.pattern;
|
|
66
|
+
if (num("vus") !== void 0) p.basic.vus = num("vus");
|
|
67
|
+
if (dur("duration") !== void 0) {
|
|
68
|
+
p.basic.durationSec = dur("duration");
|
|
69
|
+
p.arrivals.durationSec = dur("duration");
|
|
70
|
+
}
|
|
71
|
+
if (dur("ramp-up") !== void 0) p.basic.rampUpSec = dur("ramp-up");
|
|
72
|
+
if (dur("ramp-down") !== void 0) p.basic.rampDownSec = dur("ramp-down");
|
|
73
|
+
if (num("rate") !== void 0) p.arrivals.rate = num("rate");
|
|
74
|
+
if (num("max-vus") !== void 0) p.arrivals.maxVus = num("max-vus");
|
|
75
|
+
if (num("start-users") !== void 0) p.stepping.startUsers = num("start-users");
|
|
76
|
+
if (num("step-users") !== void 0) p.stepping.stepUsers = num("step-users");
|
|
77
|
+
if (dur("step-duration") !== void 0) p.stepping.stepDurationSec = dur("step-duration");
|
|
78
|
+
if (num("max-users") !== void 0) p.stepping.maxUsers = num("max-users");
|
|
79
|
+
if (dur("timeout") !== void 0) p.timeoutSec = dur("timeout");
|
|
80
|
+
if (typeof args.think === "string") {
|
|
81
|
+
const t = args.think.trim();
|
|
82
|
+
const range = t.match(/^([\d.]+)\s*-\s*([\d.]+)$/);
|
|
83
|
+
if (range) p.thinkTime = { minSec: Number(range[1]), maxSec: Number(range[2]) };
|
|
84
|
+
else if (Number.isFinite(Number(t))) p.thinkTime = { minSec: Number(t), maxSec: Number(t) };
|
|
85
|
+
}
|
|
86
|
+
const thr = args.threshold;
|
|
87
|
+
if (thr !== void 0) {
|
|
88
|
+
const specs = Array.isArray(thr) ? thr : [thr];
|
|
89
|
+
const parsed = specs.map(parseThreshold).filter((x) => !!x);
|
|
90
|
+
p.thresholds = parsed;
|
|
91
|
+
}
|
|
92
|
+
return p;
|
|
93
|
+
}
|
|
94
|
+
async function main() {
|
|
95
|
+
const argv = process.argv.slice(2);
|
|
96
|
+
const positionals = [];
|
|
97
|
+
for (const a of argv) {
|
|
98
|
+
if (a.startsWith("--")) break;
|
|
99
|
+
positionals.push(a);
|
|
100
|
+
}
|
|
101
|
+
const sub = positionals[0];
|
|
102
|
+
const name = positionals[1];
|
|
103
|
+
const args = cliCommon.parseArgs(argv, ["threshold"]);
|
|
104
|
+
if (args.help || !sub || sub !== "run" && sub !== "list") {
|
|
105
|
+
console.log('\nUsage:\n api-spector perf list --workspace <path>\n api-spector perf run <plan> --workspace <path> [--vus N] [--duration 5m] [--rate N] [--threshold "iteration_p95 lt 500"] [--out report.html]\n');
|
|
106
|
+
process.exit(args.help ? 0 : 1);
|
|
107
|
+
}
|
|
108
|
+
const wsPath = args.workspace;
|
|
109
|
+
if (!wsPath) {
|
|
110
|
+
console.error(cliCommon.color("Error: --workspace is required", cliCommon.C.red));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
let workspace, wsDir;
|
|
114
|
+
try {
|
|
115
|
+
({ workspace, dir: wsDir } = await cliCommon.loadWorkspace(wsPath));
|
|
116
|
+
} catch {
|
|
117
|
+
console.error(cliCommon.color(`Error: could not read workspace: ${wsPath}`, cliCommon.C.red));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
await requestExec.loadGlobals(wsDir);
|
|
121
|
+
requestExec.setSecretsConfig(workspace.settings?.secrets);
|
|
122
|
+
const plans = workspace.perfPlans ?? [];
|
|
123
|
+
if (sub === "list") {
|
|
124
|
+
if (plans.length === 0) {
|
|
125
|
+
console.log(cliCommon.color(" No performance plans in this workspace.", cliCommon.C.gray));
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
console.log("");
|
|
129
|
+
for (const pl of plans) {
|
|
130
|
+
const load2 = perfRun.deriveLoad(pl);
|
|
131
|
+
const tgt = pl.target.kind === "request" ? "request" : pl.target.kind === "sequence" ? "sequence" : "flow";
|
|
132
|
+
console.log(` ${cliCommon.color(pl.name, cliCommon.C.bold, cliCommon.C.white)} ${cliCommon.color(`(${tgt}, ${pl.pattern}, ~${load2.totalSec}s)`, cliCommon.C.gray)}`);
|
|
133
|
+
}
|
|
134
|
+
console.log("");
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
if (!name) {
|
|
138
|
+
console.error(cliCommon.color("Error: a plan name is required: api-spector perf run <plan>", cliCommon.C.red));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
const basePlan = plans.find((pl) => pl.name.toLowerCase() === name.toLowerCase());
|
|
142
|
+
if (!basePlan) {
|
|
143
|
+
console.error(cliCommon.color(`Error: plan "${name}" not found. Run \`api-spector perf list\` to see plans.`, cliCommon.C.red));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const plan = applyOverrides(basePlan, args);
|
|
147
|
+
const collections = await cliCommon.loadCollections(workspace, wsDir, { onError: (relPath) => console.error(cliCommon.color(` [warn] Could not load collection: ${relPath}`, cliCommon.C.yellow)) });
|
|
148
|
+
const environments$1 = await cliCommon.loadEnvironments(workspace, wsDir);
|
|
149
|
+
const env = environments.selectEnvironment(workspace, environments$1, args.environment ?? args.env);
|
|
150
|
+
const flowsAll = (await cliCommon.loadFlows(workspace, wsDir, () => {
|
|
151
|
+
})).map(flowEngine.normalizeFlow);
|
|
152
|
+
const globals = requestExec.getGlobals();
|
|
153
|
+
const payload = { plan, environment: env ?? null, globals, proxy: workspace.settings?.proxy, tls: workspace.settings?.tls };
|
|
154
|
+
let targetLabel = "";
|
|
155
|
+
if (plan.target.kind === "request") {
|
|
156
|
+
let found;
|
|
157
|
+
let foundCol;
|
|
158
|
+
for (const c of collections) {
|
|
159
|
+
const r = c.requests[plan.target.requestId];
|
|
160
|
+
if (r) {
|
|
161
|
+
found = r;
|
|
162
|
+
foundCol = c;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!found) {
|
|
167
|
+
console.error(cliCommon.color("Error: the plan's request was not found in this workspace.", cliCommon.C.red));
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
const envVars = await requestExec.buildEnvVars(env);
|
|
171
|
+
payload.request = found;
|
|
172
|
+
payload.vars = { ...globals, ...foundCol?.collectionVariables ?? {}, ...envVars };
|
|
173
|
+
targetLabel = `${found.method} ${found.name}`;
|
|
174
|
+
} else if (plan.target.kind === "sequence") {
|
|
175
|
+
const seqCol = collections.find((c) => c.id === plan.target.collectionId);
|
|
176
|
+
if (!seqCol) {
|
|
177
|
+
console.error(cliCommon.color("Error: the plan's collection was not found in this workspace.", cliCommon.C.red));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
payload.collection = seqCol;
|
|
181
|
+
targetLabel = `sequence: ${seqCol.name}${plan.target.folderId ? " / folder" : ""}`;
|
|
182
|
+
} else {
|
|
183
|
+
const flow = flowsAll.find((f) => f.id === plan.target.flowId);
|
|
184
|
+
if (!flow) {
|
|
185
|
+
console.error(cliCommon.color("Error: the plan's flow was not found in this workspace.", cliCommon.C.red));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
payload.flow = flow;
|
|
189
|
+
payload.flows = flowsAll;
|
|
190
|
+
payload.collections = collections;
|
|
191
|
+
targetLabel = `flow: ${flow.name}`;
|
|
192
|
+
}
|
|
193
|
+
const load = perfRun.deriveLoad(plan);
|
|
194
|
+
const version = `v${"0.7.8"}`;
|
|
195
|
+
console.log("");
|
|
196
|
+
console.log(cliCommon.color(" API Performance" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
|
|
197
|
+
console.log(cliCommon.color(` Plan: ${plan.name}`, cliCommon.C.gray));
|
|
198
|
+
console.log(cliCommon.color(` Target: ${targetLabel}`, cliCommon.C.gray));
|
|
199
|
+
console.log(cliCommon.color(` Pattern: ${plan.pattern} (${load.executor}, ~${load.totalSec}s)`, cliCommon.C.gray));
|
|
200
|
+
console.log(cliCommon.color(` Environment: ${env?.name ?? "(none)"}`, cliCommon.C.gray));
|
|
201
|
+
if (plan.thresholds.length) console.log(cliCommon.color(` Thresholds: ${plan.thresholds.map((t) => `${t.metric} ${t.op} ${t.value}`).join(", ")}`, cliCommon.C.gray));
|
|
202
|
+
console.log("");
|
|
203
|
+
const quiet = Boolean(args.quiet);
|
|
204
|
+
let lastPrint = 0;
|
|
205
|
+
const result = await perfRun.runPerfPlan(payload, (p) => {
|
|
206
|
+
if (quiet) return;
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
if (now - lastPrint < 1e3) return;
|
|
209
|
+
lastPrint = now;
|
|
210
|
+
process.stdout.write(`\r ${cliCommon.color("running", cliCommon.C.cyan)} ${Math.round(p.elapsedMs / 1e3)}s iters ${p.iterations} active ${p.activeVus} ${p.iterationsPerSec.toFixed(1)}/s p95 ${fmtMs(p.p95)} failed ${p.iterationsFailed} `);
|
|
211
|
+
}).catch((err) => {
|
|
212
|
+
console.error("\n" + cliCommon.color(`Error: ${err instanceof Error ? err.message : String(err)}`, cliCommon.C.red));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
});
|
|
215
|
+
if (!quiet) process.stdout.write("\n\n");
|
|
216
|
+
const errorRate = result.iterations > 0 ? result.iterationsFailed / result.iterations * 100 : 0;
|
|
217
|
+
console.log(cliCommon.color(" Results", cliCommon.C.bold, cliCommon.C.white));
|
|
218
|
+
console.log(` Throughput ${cliCommon.color(result.iterationsPerSec.toFixed(1) + " iter/s", cliCommon.C.cyan)} (${result.requestsPerSec.toFixed(1)} req/s)`);
|
|
219
|
+
console.log(` Iteration p95 ${fmtMs(result.iterationLatency.p95)} p99 ${fmtMs(result.iterationLatency.p99)}`);
|
|
220
|
+
console.log(` Error rate ${errorRate.toFixed(1)}% (${result.iterationsFailed}/${result.iterations})`);
|
|
221
|
+
console.log(` Peak users ${result.peakVus}${result.droppedArrivals ? ` dropped ${result.droppedArrivals}` : ""}`);
|
|
222
|
+
console.log(` Duration ${fmtMs(result.durationMs)}`);
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log(cliCommon.color(" Steps", cliCommon.C.bold, cliCommon.C.white));
|
|
225
|
+
for (const [i, s] of result.steps.entries()) {
|
|
226
|
+
const label = result.mode === "request" ? s.name : `${i + 1}. ${s.name}`;
|
|
227
|
+
console.log(` ${label.padEnd(36).slice(0, 36)} p95 ${fmtMs(s.latency.p95).padStart(8)} ${cliCommon.color(s.failed ? `${s.failed} failed` : "ok", s.failed ? cliCommon.C.red : cliCommon.C.green)}`);
|
|
228
|
+
}
|
|
229
|
+
console.log("");
|
|
230
|
+
if (result.thresholds.length) {
|
|
231
|
+
console.log(cliCommon.color(" Thresholds", cliCommon.C.bold, cliCommon.C.white));
|
|
232
|
+
for (const t of result.thresholds) {
|
|
233
|
+
const actual = t.metric === "error_rate" ? `${t.actual.toFixed(1)}%` : t.metric === "throughput" ? t.actual.toFixed(1) : fmtMs(t.actual);
|
|
234
|
+
console.log(` ${t.passed ? cliCommon.color("✓", cliCommon.C.green) : cliCommon.color("✗", cliCommon.C.red)} ${t.metric} ${t.op} ${t.value} (actual ${actual})`);
|
|
235
|
+
}
|
|
236
|
+
console.log("");
|
|
237
|
+
console.log(result.passed ? cliCommon.color(" PASSED", cliCommon.C.green, cliCommon.C.bold) : cliCommon.color(" FAILED", cliCommon.C.red, cliCommon.C.bold));
|
|
238
|
+
console.log("");
|
|
239
|
+
}
|
|
240
|
+
const out = args.out;
|
|
241
|
+
if (out) {
|
|
242
|
+
const content = path.extname(out).toLowerCase() === ".json" ? JSON.stringify(result, null, 2) : perfReport.perfReportToHtml(result, { planName: plan.name, target: targetLabel, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
243
|
+
await promises.writeFile(out, content, "utf8");
|
|
244
|
+
console.log(cliCommon.color(` Report written to ${out}`, cliCommon.C.gray));
|
|
245
|
+
console.log("");
|
|
246
|
+
}
|
|
247
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
248
|
+
process.exit(result.passed ? 0 : 1);
|
|
249
|
+
}
|
|
250
|
+
main().catch((err) => {
|
|
251
|
+
console.error(err);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
});
|
package/out/main/runner.js
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
const promises = require("node:fs/promises");
|
|
4
4
|
const path = require("node:path");
|
|
5
|
-
const requestExec = require("./chunks/request-exec-
|
|
5
|
+
const requestExec = require("./chunks/request-exec-Cv2jkrO1.js");
|
|
6
6
|
const escape = require("./chunks/escape-DDVAlOvZ.js");
|
|
7
|
-
const requestCollection = require("./chunks/request-collection-
|
|
7
|
+
const requestCollection = require("./chunks/request-collection-gkOYlWml.js");
|
|
8
|
+
const runConcurrent = require("./chunks/run-concurrent-468tH0AX.js");
|
|
8
9
|
const environments = require("./chunks/environments-iM3SUM-4.js");
|
|
9
10
|
const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
|
|
10
11
|
require("undici");
|
|
11
12
|
require("fs/promises");
|
|
12
13
|
require("tls");
|
|
13
|
-
require("./chunks/handle-
|
|
14
|
+
require("./chunks/handle-Crv1i9hb.js");
|
|
14
15
|
require("node:fs");
|
|
15
16
|
require("node:os");
|
|
16
17
|
require("node:crypto");
|
|
@@ -19,6 +20,9 @@ require("node:vm");
|
|
|
19
20
|
require("tv4");
|
|
20
21
|
require("jsonpath-plus");
|
|
21
22
|
require("@xmldom/xmldom");
|
|
23
|
+
require("mqtt");
|
|
24
|
+
require("ws");
|
|
25
|
+
require("socket.io-client");
|
|
22
26
|
require("node:http");
|
|
23
27
|
require("ajv");
|
|
24
28
|
require("uuid");
|
|
@@ -437,6 +441,58 @@ function buildSarifReport(results, _summary, meta = {}) {
|
|
|
437
441
|
}]
|
|
438
442
|
}, null, 2);
|
|
439
443
|
}
|
|
444
|
+
const ASK_RE = /\{\{\s*@([^}]+?)\s*\}\}/g;
|
|
445
|
+
function parseInner(inner) {
|
|
446
|
+
const m = /^(.*?)\[([^\]]*)\]\s*$/.exec(inner);
|
|
447
|
+
if (m) {
|
|
448
|
+
const choices = m[2].split(",").map((s) => s.trim()).filter(Boolean);
|
|
449
|
+
return { message: m[1].trim(), choices: choices.length ? choices : void 0 };
|
|
450
|
+
}
|
|
451
|
+
return { message: inner.trim() };
|
|
452
|
+
}
|
|
453
|
+
function extractAskVars(texts) {
|
|
454
|
+
const seen = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const text of texts) {
|
|
456
|
+
if (!text) continue;
|
|
457
|
+
for (const m of text.matchAll(ASK_RE)) {
|
|
458
|
+
const inner = m[1].trim();
|
|
459
|
+
const key = `@${inner}`;
|
|
460
|
+
if (seen.has(key)) continue;
|
|
461
|
+
const { message, choices } = parseInner(inner);
|
|
462
|
+
if (!message) continue;
|
|
463
|
+
seen.set(key, { key, message, choices });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return [...seen.values()];
|
|
467
|
+
}
|
|
468
|
+
function askVarsInRequest(req) {
|
|
469
|
+
const texts = [req.url];
|
|
470
|
+
for (const p of req.params ?? []) texts.push(p.key, p.value);
|
|
471
|
+
for (const h of req.headers ?? []) texts.push(h.key, h.value);
|
|
472
|
+
const b = req.body;
|
|
473
|
+
if (b) {
|
|
474
|
+
texts.push(b.json, b.raw);
|
|
475
|
+
if (b.graphql) texts.push(b.graphql.query, b.graphql.variables);
|
|
476
|
+
for (const f of b.form ?? []) texts.push(f.key, f.value);
|
|
477
|
+
if (b.soap) texts.push(b.soap.envelope);
|
|
478
|
+
if (b.grpc) texts.push(b.grpc.message);
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
texts.push(JSON.stringify(req.auth ?? {}));
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
return extractAskVars(texts);
|
|
485
|
+
}
|
|
486
|
+
function resolveAskValues(asks, answers) {
|
|
487
|
+
const out = {};
|
|
488
|
+
for (const a of asks) out[a.key] = answers[a.message] ?? "";
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
function missingAskMessages(asks, answers) {
|
|
492
|
+
const missing = [];
|
|
493
|
+
for (const a of asks) if (!(a.message in answers) && !missing.includes(a.message)) missing.push(a.message);
|
|
494
|
+
return missing;
|
|
495
|
+
}
|
|
440
496
|
function printResult(r, verbose) {
|
|
441
497
|
const icon = r.status === "passed" ? cliCommon.color("✓", cliCommon.C.green, cliCommon.C.bold) : r.status === "failed" ? cliCommon.color("✗", cliCommon.C.red, cliCommon.C.bold) : r.status === "skipped" ? cliCommon.color("○", cliCommon.C.gray, cliCommon.C.bold) : cliCommon.color("⚠", cliCommon.C.yellow, cliCommon.C.bold);
|
|
442
498
|
const http = r.httpStatus ? cliCommon.color(` ${r.httpStatus}`, r.httpStatus < 400 ? cliCommon.C.green : cliCommon.C.red) : "";
|
|
@@ -457,7 +513,7 @@ async function main() {
|
|
|
457
513
|
const args = cliCommon.parseArgs(process.argv.slice(2));
|
|
458
514
|
if (args.help) {
|
|
459
515
|
console.log(
|
|
460
|
-
|
|
516
|
+
'\nUsage:\n api-spector run --workspace <path> [--environment <name>] [--tags <a,b>]\n [--collection <name>] [--output <path>] [--format json|junit|html|sarif]\n [--ask "Message=value"] [--concurrency <n>] [--verbose] [--bail]\n\n --ask "Message=value" Supply an ask variable ({{@Message}}) for this run\n (repeatable). Requests whose ask variables are not\n supplied are skipped.\n --concurrency <n> Run n requests at a time (default 1 = sequential).\n Hooks run as barriers; best for independent requests.\n'
|
|
461
517
|
);
|
|
462
518
|
process.exit(0);
|
|
463
519
|
}
|
|
@@ -495,13 +551,21 @@ async function main() {
|
|
|
495
551
|
} else if (!envName && workspace.settings?.defaultEnvironment && !env) {
|
|
496
552
|
console.warn(cliCommon.color(`Warning: default environment "${workspace.settings.defaultEnvironment}" not found. Running without environment.`, cliCommon.C.yellow));
|
|
497
553
|
}
|
|
498
|
-
const version = `v${"0.7.
|
|
554
|
+
const version = `v${"0.7.8"}`;
|
|
499
555
|
console.log("");
|
|
500
556
|
console.log(cliCommon.color(" API Test Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
|
|
501
557
|
console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));
|
|
502
558
|
console.log(cliCommon.color(` Environment: ${env?.name ?? "(none)"}`, cliCommon.C.gray));
|
|
503
559
|
if (filterTags.length) console.log(cliCommon.color(` Tags: ${filterTags.join(", ")}`, cliCommon.C.gray));
|
|
504
560
|
console.log("");
|
|
561
|
+
const askAnswers = {};
|
|
562
|
+
const askArgRaw = args["ask"];
|
|
563
|
+
for (const entry of Array.isArray(askArgRaw) ? askArgRaw : askArgRaw ? [askArgRaw] : []) {
|
|
564
|
+
if (typeof entry !== "string") continue;
|
|
565
|
+
const eq = entry.indexOf("=");
|
|
566
|
+
if (eq > 0) askAnswers[entry.slice(0, eq).trim()] = entry.slice(eq + 1);
|
|
567
|
+
}
|
|
568
|
+
const concurrency = Math.max(1, Math.min(20, Math.floor(Number(args["concurrency"] ?? 1) || 1)));
|
|
505
569
|
const envVarsSnapshot = await requestExec.buildEnvVars(env);
|
|
506
570
|
const secretValuesToMask = (env?.variables ?? []).filter((v) => (v.secret || v.secretRef) && v.enabled).map((v) => envVarsSnapshot[v.key]).filter((v) => typeof v === "string" && v.length > 0);
|
|
507
571
|
function redact(s) {
|
|
@@ -556,6 +620,7 @@ async function main() {
|
|
|
556
620
|
let runGlobals = requestExec.getGlobals();
|
|
557
621
|
let runCollectionVars = { ...col.collectionVariables };
|
|
558
622
|
let runLocalVars = {};
|
|
623
|
+
const runFolderVars = {};
|
|
559
624
|
console.log(cliCommon.color(` ┌ ${col.name}`, cliCommon.C.bold, cliCommon.C.white));
|
|
560
625
|
const workspaceTls = workspace.settings?.tls;
|
|
561
626
|
const effectiveTls = col.tls ? { ...workspaceTls, ...col.tls } : workspaceTls;
|
|
@@ -580,9 +645,11 @@ async function main() {
|
|
|
580
645
|
if (verbose && lines.length) lines.forEach((l) => console.log(cliCommon.color(` [${phase}] ${l}`, cliCommon.C.gray)));
|
|
581
646
|
if (phase === "pre" && error) console.error(cliCommon.color(` [pre-script error] ${error}`, cliCommon.C.red));
|
|
582
647
|
};
|
|
583
|
-
for (const item of items) {
|
|
648
|
+
if (concurrency <= 1) for (const item of items) {
|
|
584
649
|
const { isHook, hookType, scopeId } = item;
|
|
585
650
|
const skipReason = skipTracker.shouldSkip(item);
|
|
651
|
+
const itemAsks = askVarsInRequest(item.request);
|
|
652
|
+
const missingAsks = itemAsks.length ? missingAskMessages(itemAsks, askAnswers) : [];
|
|
586
653
|
let result;
|
|
587
654
|
if (skipReason) {
|
|
588
655
|
result = {
|
|
@@ -599,13 +666,32 @@ async function main() {
|
|
|
599
666
|
scopePath: item.scopePath,
|
|
600
667
|
iterationLabel: item.iterationLabel
|
|
601
668
|
};
|
|
669
|
+
} else if (missingAsks.length) {
|
|
670
|
+
result = {
|
|
671
|
+
requestId: item.request.id,
|
|
672
|
+
name: item.request.name,
|
|
673
|
+
method: item.request.method,
|
|
674
|
+
resolvedUrl: item.request.url,
|
|
675
|
+
status: "skipped",
|
|
676
|
+
error: `Ask variable(s) not provided: ${missingAsks.join(", ")} (use --ask "${missingAsks[0]}=value")`,
|
|
677
|
+
isHook,
|
|
678
|
+
hookType,
|
|
679
|
+
scopeId,
|
|
680
|
+
collection: item.collection,
|
|
681
|
+
scopePath: item.scopePath,
|
|
682
|
+
iterationLabel: item.iterationLabel
|
|
683
|
+
};
|
|
602
684
|
} else {
|
|
603
685
|
const out = await requestExec.executeRunnerRequest({
|
|
604
686
|
req: item.request,
|
|
605
|
-
collectionVars: { ...item.collectionVars, ...runCollectionVars },
|
|
687
|
+
collectionVars: { ...item.collectionVars, ...runCollectionVars, ...resolveAskValues(itemAsks, askAnswers) },
|
|
606
688
|
envVars: runEnvVars,
|
|
607
689
|
globals: runGlobals,
|
|
608
690
|
localVars: { ...runLocalVars, ...item.dataRow },
|
|
691
|
+
folderVars: {
|
|
692
|
+
...item.folderVars ?? {},
|
|
693
|
+
...item.folderId ? runFolderVars[item.folderId] ?? {} : {}
|
|
694
|
+
},
|
|
609
695
|
dispatcher,
|
|
610
696
|
piiMaskPatterns: piiPatterns,
|
|
611
697
|
tls: effectiveTls,
|
|
@@ -618,6 +704,9 @@ async function main() {
|
|
|
618
704
|
runCollectionVars = out.updatedCollectionVars;
|
|
619
705
|
runGlobals = out.updatedGlobals;
|
|
620
706
|
runLocalVars = out.updatedLocalVars;
|
|
707
|
+
if (item.folderId) {
|
|
708
|
+
runFolderVars[item.folderId] = { ...runFolderVars[item.folderId] ?? {}, ...out.updatedFolderVars };
|
|
709
|
+
}
|
|
621
710
|
result.isHook = isHook;
|
|
622
711
|
result.hookType = hookType;
|
|
623
712
|
result.scopeId = scopeId;
|
|
@@ -644,6 +733,91 @@ async function main() {
|
|
|
644
733
|
break;
|
|
645
734
|
}
|
|
646
735
|
}
|
|
736
|
+
else {
|
|
737
|
+
const resultsByIndex = new Array(items.length);
|
|
738
|
+
const preResult = (item, status, error) => ({
|
|
739
|
+
requestId: item.request.id,
|
|
740
|
+
name: item.request.name,
|
|
741
|
+
method: item.request.method,
|
|
742
|
+
resolvedUrl: item.request.url,
|
|
743
|
+
status,
|
|
744
|
+
error,
|
|
745
|
+
isHook: item.isHook,
|
|
746
|
+
hookType: item.hookType,
|
|
747
|
+
scopeId: item.scopeId,
|
|
748
|
+
collection: item.collection,
|
|
749
|
+
scopePath: item.scopePath,
|
|
750
|
+
iterationLabel: item.iterationLabel
|
|
751
|
+
});
|
|
752
|
+
const runItem = async (item, i) => {
|
|
753
|
+
const skipReason = skipTracker.shouldSkip(item);
|
|
754
|
+
const itemAsks = askVarsInRequest(item.request);
|
|
755
|
+
const missingAsks = itemAsks.length ? missingAskMessages(itemAsks, askAnswers) : [];
|
|
756
|
+
if (skipReason) {
|
|
757
|
+
resultsByIndex[i] = preResult(item, "failed", skipReason);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (missingAsks.length) {
|
|
761
|
+
resultsByIndex[i] = preResult(item, "skipped", `Ask variable(s) not provided: ${missingAsks.join(", ")}`);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
const out = await requestExec.executeRunnerRequest({
|
|
766
|
+
req: item.request,
|
|
767
|
+
collectionVars: { ...item.collectionVars, ...runCollectionVars, ...resolveAskValues(itemAsks, askAnswers) },
|
|
768
|
+
envVars: runEnvVars,
|
|
769
|
+
globals: runGlobals,
|
|
770
|
+
localVars: { ...runLocalVars, ...item.dataRow },
|
|
771
|
+
folderVars: { ...item.folderVars ?? {}, ...item.folderId ? runFolderVars[item.folderId] ?? {} : {} },
|
|
772
|
+
dispatcher,
|
|
773
|
+
piiMaskPatterns: piiPatterns,
|
|
774
|
+
tls: effectiveTls,
|
|
775
|
+
expectStatus: item.expectStatus,
|
|
776
|
+
owaspTag: item.owaspTag
|
|
777
|
+
});
|
|
778
|
+
runEnvVars = { ...runEnvVars, ...out.updatedEnvVars };
|
|
779
|
+
runCollectionVars = { ...runCollectionVars, ...out.updatedCollectionVars };
|
|
780
|
+
runGlobals = { ...runGlobals, ...out.updatedGlobals };
|
|
781
|
+
runLocalVars = { ...runLocalVars, ...out.updatedLocalVars };
|
|
782
|
+
if (item.folderId) runFolderVars[item.folderId] = { ...runFolderVars[item.folderId] ?? {}, ...out.updatedFolderVars };
|
|
783
|
+
const result = out.result;
|
|
784
|
+
result.isHook = item.isHook;
|
|
785
|
+
result.hookType = item.hookType;
|
|
786
|
+
result.scopeId = item.scopeId;
|
|
787
|
+
result.collection = item.collection;
|
|
788
|
+
result.scopePath = item.scopePath;
|
|
789
|
+
result.iterationLabel = item.iterationLabel;
|
|
790
|
+
skipTracker.recordResult(item, result.status);
|
|
791
|
+
resultsByIndex[i] = result;
|
|
792
|
+
} catch (e) {
|
|
793
|
+
resultsByIndex[i] = preResult(item, "error", e instanceof Error ? e.message : String(e));
|
|
794
|
+
skipTracker.recordResult(item, "error");
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
await runConcurrent.runPlanConcurrent(concurrency, items, (it) => Boolean(it.isHook), runItem);
|
|
798
|
+
for (let i = 0; i < items.length; i++) {
|
|
799
|
+
const result = resultsByIndex[i];
|
|
800
|
+
if (!result) continue;
|
|
801
|
+
const item = items[i];
|
|
802
|
+
const scopeKey = (item.scopePath ?? []).join(" / ");
|
|
803
|
+
if (scopeKey !== lastPrintedScope) {
|
|
804
|
+
if (scopeKey) console.log(cliCommon.color(` ${scopeKey}`, cliCommon.C.gray, cliCommon.C.bold));
|
|
805
|
+
lastPrintedScope = scopeKey;
|
|
806
|
+
}
|
|
807
|
+
printResult(result, verbose);
|
|
808
|
+
allResults.push(result);
|
|
809
|
+
summary.total++;
|
|
810
|
+
if (result.status === "passed") summary.passed++;
|
|
811
|
+
else if (result.status === "failed") summary.failed++;
|
|
812
|
+
else if (result.status === "skipped") summary.skipped++;
|
|
813
|
+
else summary.errors++;
|
|
814
|
+
if (bail && (result.status === "failed" || result.status === "error")) {
|
|
815
|
+
console.log(cliCommon.color("\n Bailing after first failure.", cliCommon.C.yellow));
|
|
816
|
+
bailed = true;
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
647
821
|
console.log("");
|
|
648
822
|
if (bailed) break;
|
|
649
823
|
}
|