redfireforge-cli 0.8.1 → 0.8.3
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/README.md +100 -4
- package/dist/redfireforge.mjs +158 -60
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -52,6 +52,9 @@ redfireforge run tests/api-test.yaml -c 10 -i 100
|
|
|
52
52
|
# Run a workflow performance test
|
|
53
53
|
redfireforge workflow tests/checkout-flow.yaml -i 50 -c 5
|
|
54
54
|
|
|
55
|
+
# Print machine-readable JSON to stdout (for CI)
|
|
56
|
+
redfireforge run tests/api-test.yaml --output json
|
|
57
|
+
|
|
55
58
|
# Every command above also works with the short "rff" alias:
|
|
56
59
|
rff run tests/api-test.yaml -c 10 -i 100
|
|
57
60
|
```
|
|
@@ -92,7 +95,8 @@ npx tsx cli/index.ts mock start ./api-mock-workspace.json --standalone --wait-re
|
|
|
92
95
|
| Option | Commands | Description |
|
|
93
96
|
|--------|----------|-------------|
|
|
94
97
|
| `--server <id>` | simulate, verify | Target server (default: active / first) |
|
|
95
|
-
| `-o, --output <path>` | simulate | Write JSON results |
|
|
98
|
+
| `-o, --output <path>` | simulate | Write JSON results to a file |
|
|
99
|
+
| `-o, --output json\|junit` | simulate | Print results to stdout in that format |
|
|
96
100
|
| `--junit <path>` | simulate | Write JUnit XML |
|
|
97
101
|
| `--min-calls <n>` | verify | Require at least N matching journal calls (samples when `--simulate`) |
|
|
98
102
|
| `--expect-outcome <outcome>` | verify | Require matching outcome |
|
|
@@ -131,11 +135,80 @@ npx tsx cli/index.ts mock start ./api-mock-workspace.json --standalone --wait-re
|
|
|
131
135
|
|
|
132
136
|
| Option | Description |
|
|
133
137
|
|--------|-------------|
|
|
134
|
-
| `-o, --output <path>` | Write JSON report |
|
|
138
|
+
| `-o, --output <path>` | Write JSON report to a file |
|
|
139
|
+
| `-o, --output json` | Print a CI-friendly JSON report to **stdout** |
|
|
140
|
+
| `-o, --output junit` | Print JUnit XML to **stdout** |
|
|
135
141
|
| `--junit <path>` | Write JUnit XML report |
|
|
136
142
|
| `--markdown <path>` | Write Markdown report |
|
|
137
143
|
| `-q, --quiet` | Suppress progress output |
|
|
138
144
|
|
|
145
|
+
> `json` and `junit` are format keywords, not filenames — supported by `run`,
|
|
146
|
+
> `workflow`, and `mock simulate`. To write to a file literally named `json`,
|
|
147
|
+
> qualify it: `--output ./json`.
|
|
148
|
+
|
|
149
|
+
### Machine-Readable Output (CI)
|
|
150
|
+
|
|
151
|
+
`--output json` prints a flat, stable report to stdout and suppresses **all**
|
|
152
|
+
other stdout output, so the stream is safe to pipe straight into `jq`:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
rff run tests/api-test.yaml --output json | jq '.failed'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"passed": 12,
|
|
161
|
+
"failed": 2,
|
|
162
|
+
"total": 14,
|
|
163
|
+
"durationMs": 3421,
|
|
164
|
+
"results": [
|
|
165
|
+
{
|
|
166
|
+
"name": "Get Users",
|
|
167
|
+
"status": "pass",
|
|
168
|
+
"durationMs": 123,
|
|
169
|
+
"error": null
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "Create Order",
|
|
173
|
+
"status": "fail",
|
|
174
|
+
"durationMs": 456,
|
|
175
|
+
"error": "Expected status 201 but got 500"
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Notes:
|
|
182
|
+
|
|
183
|
+
- `status` is `"pass"` or `"fail"`; `error` is `null` for passing tests.
|
|
184
|
+
- Parameterized rows are qualified as `Scenario [Row label]` so names stay unique.
|
|
185
|
+
- Errors and diagnostics still go to **stderr**, keeping stdout pure.
|
|
186
|
+
- Exit codes are unchanged — `--fail-on-error` (1), `--fail-on-regression` (2/3)
|
|
187
|
+
and `--fail-on-sla` (4) all still fire, and the SLA / baseline reports that
|
|
188
|
+
normally print on failure are suppressed so they cannot corrupt the report.
|
|
189
|
+
|
|
190
|
+
#### Workflow runs
|
|
191
|
+
|
|
192
|
+
For `workflow`, one result is emitted **per iteration** — matching `--output junit`,
|
|
193
|
+
so both formats agree on `total`. Each iteration fails if any of its steps failed,
|
|
194
|
+
and the individual steps are preserved under `steps` (same shape, one level deep):
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"name": "Iteration 1",
|
|
199
|
+
"status": "fail",
|
|
200
|
+
"durationMs": 56,
|
|
201
|
+
"error": "Create Order: (http): expected 2xx, got HTTP 500",
|
|
202
|
+
"steps": [
|
|
203
|
+
{ "name": "Login", "status": "pass", "durationMs": 54, "error": null },
|
|
204
|
+
{ "name": "Create Order", "status": "fail", "durationMs": 2, "error": "(http): expected 2xx, got HTTP 500" }
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`steps` is additive — pipelines that only read the documented fields are unaffected.
|
|
210
|
+
The iteration `error` concatenates every failing step as `Step: error`, joined by `; `.
|
|
211
|
+
|
|
139
212
|
### CI/CD Options
|
|
140
213
|
|
|
141
214
|
| Option | Description |
|
|
@@ -209,8 +282,16 @@ edges:
|
|
|
209
282
|
| Code | Meaning |
|
|
210
283
|
|------|---------|
|
|
211
284
|
| 0 | Success — all tests passed |
|
|
212
|
-
| 1 | Test failure
|
|
213
|
-
| 2 |
|
|
285
|
+
| 1 | Test failure, or an execution error (invalid/missing file) |
|
|
286
|
+
| 2 | Performance regression vs. baseline (`--fail-on-regression`) |
|
|
287
|
+
| 3 | Regression **and** test failures (`--fail-on-regression`) |
|
|
288
|
+
| 4 | SLA violation (`--fail-on-sla`) |
|
|
289
|
+
|
|
290
|
+
A **test failure** exits `1` only with `--fail-on-error` or `--fail-threshold <pct>`;
|
|
291
|
+
otherwise failures are reported and the run still exits `0`. An **execution error**
|
|
292
|
+
(invalid or missing file) always exits `1`, no flag required. Codes `2`/`3` need
|
|
293
|
+
`--fail-on-regression`, `4` needs `--fail-on-sla`. The `workflow` command uses `1`
|
|
294
|
+
for failures and `2` for execution errors. Exit codes are unaffected by `--output json`.
|
|
214
295
|
|
|
215
296
|
## CI/CD Example
|
|
216
297
|
|
|
@@ -226,6 +307,21 @@ edges:
|
|
|
226
307
|
-q
|
|
227
308
|
```
|
|
228
309
|
|
|
310
|
+
To parse results in the pipeline instead of writing a file, stream JSON to stdout:
|
|
311
|
+
|
|
312
|
+
```yaml
|
|
313
|
+
- name: Run API Tests and gate on failures
|
|
314
|
+
run: |
|
|
315
|
+
npx redfireforge-cli run tests/api-test.yaml \
|
|
316
|
+
--output json \
|
|
317
|
+
--fail-on-error > results.json
|
|
318
|
+
# Non-zero exit already fails the step; results.json is still valid JSON.
|
|
319
|
+
|
|
320
|
+
- name: Summarize
|
|
321
|
+
if: always()
|
|
322
|
+
run: jq -r '"\(.passed)/\(.total) passed in \(.durationMs)ms"' results.json
|
|
323
|
+
```
|
|
324
|
+
|
|
229
325
|
## Links
|
|
230
326
|
|
|
231
327
|
- [Full Documentation](https://github.com/your-org/redfireforge/blob/main/docs/guides/cli-reference.md)
|
package/dist/redfireforge.mjs
CHANGED
|
@@ -30920,7 +30920,11 @@ async function nodeFetch(url, method, headers, body, signal) {
|
|
|
30920
30920
|
if (signal) opts.signal = signal;
|
|
30921
30921
|
const doFetch = async (fetchOpts) => {
|
|
30922
30922
|
const t0 = performance.now();
|
|
30923
|
-
const
|
|
30923
|
+
const fetchFn = fetchOpts.dispatcher ? (await import(
|
|
30924
|
+
/* @vite-ignore */
|
|
30925
|
+
"undici"
|
|
30926
|
+
)).fetch : fetch;
|
|
30927
|
+
const response = await fetchFn(targetUrl, fetchOpts);
|
|
30924
30928
|
const tFirstByte = performance.now();
|
|
30925
30929
|
const responseBody = await response.text();
|
|
30926
30930
|
const tDone = performance.now();
|
|
@@ -63454,6 +63458,59 @@ function buildJsonReport(results, summary, config, meta) {
|
|
|
63454
63458
|
projectName: meta.name
|
|
63455
63459
|
};
|
|
63456
63460
|
}
|
|
63461
|
+
function ciResultName(r) {
|
|
63462
|
+
return r.dataRowLabel ? `${r.scenarioName} [${r.dataRowLabel}]` : r.scenarioName;
|
|
63463
|
+
}
|
|
63464
|
+
function ciResultError(r) {
|
|
63465
|
+
if (r.passed) return null;
|
|
63466
|
+
const detail = r.errorMessage || (r.failureDetails.length > 0 ? formatFailureDetails(r.failureDetails) : null);
|
|
63467
|
+
if ((r.transportType ?? "http") === "http" && r.httpStatus >= 400) {
|
|
63468
|
+
return detail ? `HTTP ${r.httpStatus}: ${detail}` : `HTTP ${r.httpStatus}`;
|
|
63469
|
+
}
|
|
63470
|
+
return detail ?? formatTransportErrorFallback(r);
|
|
63471
|
+
}
|
|
63472
|
+
function ciStepResult(r) {
|
|
63473
|
+
return {
|
|
63474
|
+
name: ciResultName(r),
|
|
63475
|
+
status: r.passed ? "pass" : "fail",
|
|
63476
|
+
durationMs: Math.round(r.responseTimeMs),
|
|
63477
|
+
error: ciResultError(r)
|
|
63478
|
+
};
|
|
63479
|
+
}
|
|
63480
|
+
function buildCiJsonReport(results, durationMs) {
|
|
63481
|
+
const exportSafeResults = redactGrpcHarnessRunnerArtifactsForExport(results);
|
|
63482
|
+
const passed = exportSafeResults.filter((r) => r.passed).length;
|
|
63483
|
+
return {
|
|
63484
|
+
passed,
|
|
63485
|
+
failed: exportSafeResults.length - passed,
|
|
63486
|
+
total: exportSafeResults.length,
|
|
63487
|
+
durationMs: Math.round(durationMs),
|
|
63488
|
+
results: exportSafeResults.map(ciStepResult)
|
|
63489
|
+
};
|
|
63490
|
+
}
|
|
63491
|
+
function buildWorkflowCiJsonReport(results, iterations, durationMs) {
|
|
63492
|
+
const exportSafeResults = redactGrpcHarnessRunnerArtifactsForExport(results);
|
|
63493
|
+
const grouped = groupResultsByIteration(exportSafeResults, iterations);
|
|
63494
|
+
const iterationResults = grouped.map((iterResults, i) => {
|
|
63495
|
+
const steps = iterResults.map(ciStepResult);
|
|
63496
|
+
const failedSteps = steps.filter((s2) => s2.status === "fail");
|
|
63497
|
+
return {
|
|
63498
|
+
name: `Iteration ${i + 1}`,
|
|
63499
|
+
status: failedSteps.length === 0 ? "pass" : "fail",
|
|
63500
|
+
durationMs: steps.reduce((sum, s2) => sum + s2.durationMs, 0),
|
|
63501
|
+
error: failedSteps.length === 0 ? null : failedSteps.map((s2) => `${s2.name}: ${s2.error}`).join("; "),
|
|
63502
|
+
steps
|
|
63503
|
+
};
|
|
63504
|
+
});
|
|
63505
|
+
const passed = iterationResults.filter((r) => r.status === "pass").length;
|
|
63506
|
+
return {
|
|
63507
|
+
passed,
|
|
63508
|
+
failed: iterationResults.length - passed,
|
|
63509
|
+
total: iterationResults.length,
|
|
63510
|
+
durationMs: Math.round(durationMs),
|
|
63511
|
+
results: iterationResults
|
|
63512
|
+
};
|
|
63513
|
+
}
|
|
63457
63514
|
function escapeXml(s2) {
|
|
63458
63515
|
return s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
63459
63516
|
}
|
|
@@ -63651,26 +63708,26 @@ function computePerStepStats(results) {
|
|
|
63651
63708
|
}
|
|
63652
63709
|
return stats;
|
|
63653
63710
|
}
|
|
63654
|
-
function
|
|
63711
|
+
function groupResultsByIteration(results, iterations) {
|
|
63655
63712
|
const byIteration = /* @__PURE__ */ new Map();
|
|
63656
63713
|
for (const r of results) {
|
|
63657
63714
|
const idx = r.iterationIndex ?? 0;
|
|
63658
63715
|
if (!byIteration.has(idx)) byIteration.set(idx, []);
|
|
63659
63716
|
byIteration.get(idx).push(r);
|
|
63660
63717
|
}
|
|
63661
|
-
const
|
|
63718
|
+
const grouped = [];
|
|
63662
63719
|
for (let i = 0; i < iterations; i++) {
|
|
63663
|
-
|
|
63664
|
-
const allPassed = iterResults.every((r) => r.passed);
|
|
63665
|
-
const totalDuration = iterResults.reduce((sum, r) => sum + r.responseTimeMs, 0);
|
|
63666
|
-
stats.push({
|
|
63667
|
-
index: i,
|
|
63668
|
-
passed: allPassed,
|
|
63669
|
-
durationMs: totalDuration,
|
|
63670
|
-
stepCount: iterResults.length
|
|
63671
|
-
});
|
|
63720
|
+
grouped.push(byIteration.get(i) || []);
|
|
63672
63721
|
}
|
|
63673
|
-
return
|
|
63722
|
+
return grouped;
|
|
63723
|
+
}
|
|
63724
|
+
function computePerIterationStats(results, iterations) {
|
|
63725
|
+
return groupResultsByIteration(results, iterations).map((iterResults, i) => ({
|
|
63726
|
+
index: i,
|
|
63727
|
+
passed: iterResults.every((r) => r.passed),
|
|
63728
|
+
durationMs: iterResults.reduce((sum, r) => sum + r.responseTimeMs, 0),
|
|
63729
|
+
stepCount: iterResults.length
|
|
63730
|
+
}));
|
|
63674
63731
|
}
|
|
63675
63732
|
function formatRowLabel(r) {
|
|
63676
63733
|
return r.dataRowLabel || r.dataRowId || "?";
|
|
@@ -63890,6 +63947,22 @@ function buildWorkflowMarkdownReport(summary, workflow, iterations, concurrency,
|
|
|
63890
63947
|
return lines.join("\n");
|
|
63891
63948
|
}
|
|
63892
63949
|
|
|
63950
|
+
// cli/outputTarget.ts
|
|
63951
|
+
var STDOUT_FORMATS = ["json", "junit"];
|
|
63952
|
+
function resolveOutputTarget(value) {
|
|
63953
|
+
if (value == null) return null;
|
|
63954
|
+
const trimmed = value.trim();
|
|
63955
|
+
if (trimmed === "") return null;
|
|
63956
|
+
const keyword = trimmed.toLowerCase();
|
|
63957
|
+
if (STDOUT_FORMATS.includes(keyword)) {
|
|
63958
|
+
return { kind: "stdout", format: keyword };
|
|
63959
|
+
}
|
|
63960
|
+
return { kind: "file", path: trimmed };
|
|
63961
|
+
}
|
|
63962
|
+
function stdoutFormatOf(target) {
|
|
63963
|
+
return target?.kind === "stdout" ? target.format : null;
|
|
63964
|
+
}
|
|
63965
|
+
|
|
63893
63966
|
// cli/slaEval.ts
|
|
63894
63967
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
63895
63968
|
function getMetricFromSummary(metric, summary) {
|
|
@@ -67059,7 +67132,7 @@ var ApiMockNetworkListener = class {
|
|
|
67059
67132
|
handler
|
|
67060
67133
|
);
|
|
67061
67134
|
} catch (err) {
|
|
67062
|
-
throw new Error(`TLS material rejected: ${err instanceof Error ? err.message : String(err)}
|
|
67135
|
+
throw new Error(`TLS material rejected: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
67063
67136
|
}
|
|
67064
67137
|
} else {
|
|
67065
67138
|
server = http.createServer(handler);
|
|
@@ -67810,6 +67883,8 @@ function sleep4(ms) {
|
|
|
67810
67883
|
|
|
67811
67884
|
// cli/mockCommands.ts
|
|
67812
67885
|
async function runMockSimulate(opts) {
|
|
67886
|
+
const outputTarget = resolveOutputTarget(opts.output);
|
|
67887
|
+
const stdoutFormat = stdoutFormatOf(outputTarget);
|
|
67813
67888
|
const raw = asWorkspace(loadDefinitionFile(opts.file));
|
|
67814
67889
|
const loaded = cliLoadAndValidate(raw);
|
|
67815
67890
|
if (reportValidation(loaded.validationErrors)) return 1;
|
|
@@ -67828,10 +67903,7 @@ async function runMockSimulate(opts) {
|
|
|
67828
67903
|
failed: failed.length,
|
|
67829
67904
|
results
|
|
67830
67905
|
};
|
|
67831
|
-
const
|
|
67832
|
-
if (opts.output) writeFileSync2(resolve4(opts.output), json, "utf8");
|
|
67833
|
-
else console.log(json);
|
|
67834
|
-
if (opts.junit) {
|
|
67906
|
+
const buildJunit = () => {
|
|
67835
67907
|
const cases = results.map((r) => {
|
|
67836
67908
|
const name2 = r.sampleId;
|
|
67837
67909
|
if (isFailedSimulation(r)) {
|
|
@@ -67839,12 +67911,18 @@ async function runMockSimulate(opts) {
|
|
|
67839
67911
|
}
|
|
67840
67912
|
return `<testcase classname="api-mock" name="${escapeXml2(name2)}"/>`;
|
|
67841
67913
|
}).join("\n");
|
|
67842
|
-
|
|
67914
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
67843
67915
|
<testsuite name="api-mock-simulate" tests="${results.length}" failures="${failed.length}">
|
|
67844
67916
|
${cases}
|
|
67845
67917
|
</testsuite>
|
|
67846
67918
|
`;
|
|
67847
|
-
|
|
67919
|
+
};
|
|
67920
|
+
const json = JSON.stringify(summary, null, 2);
|
|
67921
|
+
if (stdoutFormat === "junit") process.stdout.write(buildJunit());
|
|
67922
|
+
else if (outputTarget?.kind === "file") writeFileSync2(resolve4(outputTarget.path), json, "utf8");
|
|
67923
|
+
else console.log(json);
|
|
67924
|
+
if (opts.junit) {
|
|
67925
|
+
writeFileSync2(resolve4(opts.junit), buildJunit(), "utf8");
|
|
67848
67926
|
}
|
|
67849
67927
|
console.error(`Simulated ${results.length} sample(s); ${failed.length} failure(s).`);
|
|
67850
67928
|
return failed.length > 0 ? 1 : 0;
|
|
@@ -68268,7 +68346,7 @@ async function runMockVerifyAllRoutes(opts) {
|
|
|
68268
68346
|
// cli/mockCommandRegistration.ts
|
|
68269
68347
|
function registerMockCommands(program2) {
|
|
68270
68348
|
const mock = program2.command("mock").description("API Mock Studio headless commands");
|
|
68271
|
-
mock.command("simulate").description("Run saved simulation samples against a mock definition (side-effect-free)").argument("<file>", "Workspace / server JSON or YAML (or native export envelope)").option("--server <id>", "Server id (defaults to activeServerId or first)").option("-o, --output <path>", "Write JSON results to file").option("--junit <path>", "Write JUnit XML results to file").action(async (file, opts) => {
|
|
68349
|
+
mock.command("simulate").description("Run saved simulation samples against a mock definition (side-effect-free)").argument("<file>", "Workspace / server JSON or YAML (or native export envelope)").option("--server <id>", "Server id (defaults to activeServerId or first)").option("-o, --output <path|json|junit>", "Write JSON results to file, or print `json`/`junit` to stdout").option("--junit <path>", "Write JUnit XML results to file").action(async (file, opts) => {
|
|
68272
68350
|
const code = await runMockSimulate({ file, serverId: opts.server, output: opts.output, junit: opts.junit });
|
|
68273
68351
|
process.exit(code);
|
|
68274
68352
|
});
|
|
@@ -68344,15 +68422,18 @@ var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
|
68344
68422
|
var pkg = JSON.parse(readFileSync7(resolve5(__dirname, "..", "package.json"), "utf-8"));
|
|
68345
68423
|
var program = new Command();
|
|
68346
68424
|
program.name("redfireforge").description("RedfireForge CLI \u2014 run API performance tests from YAML/JSON files").version(pkg.version);
|
|
68347
|
-
program.command("run").description("Execute a test file").argument("<file>", "Path to a .yaml, .yml, or .json test file").option("-c, --concurrency <n>", "Number of concurrent requests", parseInt).option("-i, --iterations <n>", "Number of iterations (how many times each test runs)", parseInt).option("-m, --mode <mode>", "Execution mode: sequential, batch, pool, load-profile").option("--timeout <sec>", "Per-request timeout in seconds", parseInt).option("--retries <n>", "Retry count on failure", parseInt).option("--retry-delay <ms>", "Delay between retries in milliseconds", parseInt).option("--duration <sec>", "Duration in seconds (load-profile mode)", parseInt).option("--base-url <url>", "Override the base URL for all tests").option("--data <file>", "External data file (CSV or JSON) for parameterized testing").option("--scenario <name>", "Run only the test matching this name (used with --data)").option("--env <name>", "Environment name (metadata only)").option("--error-policy <policy>", "Error policy: continue, stop-first, stop-threshold").option("--max-errors <n>", "Stop after N errors (threshold mode)", parseInt).option("--max-error-rate <pct>", "Stop at error rate % (threshold mode)", parseFloat).option("--fail-on-error", "Exit code 1 if any request fails (HTTP or validation)").option("--fail-threshold <pct>", "Exit code 1 if error rate exceeds this %", parseFloat).option("-o, --output <path>", "Write JSON report to file").option("--junit <path>", "Write JUnit XML report to file").option("--markdown <path>", "Write Markdown report to file").option("--data-rows-summary <path>", "Write data row summary JSON (CI/CD format)").option("--tags <tags>", "Run only data rows with these tags (comma-separated)").option("--tag-mode <mode>", "Tag matching mode: any (default) or all", "any").option("--scenario-tags <tags>", "Run only scenarios with these tags (comma-separated)").option("--scenario-tag-mode <mode>", "Scenario tag matching mode: any (default) or all", "any").option("--sla-config <path>", "JSON file of SLA targets to evaluate after the run (SlaTarget[])").option("--fail-on-sla", "Exit code 4 if any SLA violations are detected (requires --sla-config)").option("--compare-baseline <id>", `Compare run against a saved baseline. Use "latest-baseline" to pick the most recent one automatically, or pass the runId of a specific saved baseline.`).option("--fail-on-regression", "Exit code 2 (regression only) or 3 (also test failures) when regressions are detected").option("--save-baseline", "Save this run as a new baseline after completion (only when no failures or regressions)").option("--baseline-label <label>", "Human-readable label for the saved baseline").option("--baselines-dir <dir>", `Directory for the baseline store (default: ${DEFAULT_BASELINES_DIR})`).option("--comparison-report <path>", "Write the Markdown comparison report to a file").option("-q, --quiet", "Suppress progress output").action(async (filePath, opts) => {
|
|
68425
|
+
program.command("run").description("Execute a test file").argument("<file>", "Path to a .yaml, .yml, or .json test file").option("-c, --concurrency <n>", "Number of concurrent requests", parseInt).option("-i, --iterations <n>", "Number of iterations (how many times each test runs)", parseInt).option("-m, --mode <mode>", "Execution mode: sequential, batch, pool, load-profile").option("--timeout <sec>", "Per-request timeout in seconds", parseInt).option("--retries <n>", "Retry count on failure", parseInt).option("--retry-delay <ms>", "Delay between retries in milliseconds", parseInt).option("--duration <sec>", "Duration in seconds (load-profile mode)", parseInt).option("--base-url <url>", "Override the base URL for all tests").option("--data <file>", "External data file (CSV or JSON) for parameterized testing").option("--scenario <name>", "Run only the test matching this name (used with --data)").option("--env <name>", "Environment name (metadata only)").option("--error-policy <policy>", "Error policy: continue, stop-first, stop-threshold").option("--max-errors <n>", "Stop after N errors (threshold mode)", parseInt).option("--max-error-rate <pct>", "Stop at error rate % (threshold mode)", parseFloat).option("--fail-on-error", "Exit code 1 if any request fails (HTTP or validation)").option("--fail-threshold <pct>", "Exit code 1 if error rate exceeds this %", parseFloat).option("-o, --output <path|json|junit>", "Write JSON report to file, or print `json`/`junit` to stdout for CI").option("--junit <path>", "Write JUnit XML report to file").option("--markdown <path>", "Write Markdown report to file").option("--data-rows-summary <path>", "Write data row summary JSON (CI/CD format)").option("--tags <tags>", "Run only data rows with these tags (comma-separated)").option("--tag-mode <mode>", "Tag matching mode: any (default) or all", "any").option("--scenario-tags <tags>", "Run only scenarios with these tags (comma-separated)").option("--scenario-tag-mode <mode>", "Scenario tag matching mode: any (default) or all", "any").option("--sla-config <path>", "JSON file of SLA targets to evaluate after the run (SlaTarget[])").option("--fail-on-sla", "Exit code 4 if any SLA violations are detected (requires --sla-config)").option("--compare-baseline <id>", `Compare run against a saved baseline. Use "latest-baseline" to pick the most recent one automatically, or pass the runId of a specific saved baseline.`).option("--fail-on-regression", "Exit code 2 (regression only) or 3 (also test failures) when regressions are detected").option("--save-baseline", "Save this run as a new baseline after completion (only when no failures or regressions)").option("--baseline-label <label>", "Human-readable label for the saved baseline").option("--baselines-dir <dir>", `Directory for the baseline store (default: ${DEFAULT_BASELINES_DIR})`).option("--comparison-report <path>", "Write the Markdown comparison report to a file").option("-q, --quiet", "Suppress progress output").action(async (filePath, opts) => {
|
|
68348
68426
|
try {
|
|
68427
|
+
const outputTarget = resolveOutputTarget(opts.output);
|
|
68428
|
+
const stdoutFormat = stdoutFormatOf(outputTarget);
|
|
68429
|
+
const quiet = Boolean(opts.quiet) || stdoutFormat !== null;
|
|
68349
68430
|
const absPath = resolve5(filePath);
|
|
68350
68431
|
const file = loadTestFile(absPath);
|
|
68351
68432
|
let externalDataSource;
|
|
68352
68433
|
if (opts.data) {
|
|
68353
68434
|
const dataPath = resolve5(opts.data);
|
|
68354
68435
|
externalDataSource = loadDataFile(dataPath);
|
|
68355
|
-
if (!
|
|
68436
|
+
if (!quiet) {
|
|
68356
68437
|
console.log(` Data: ${basename2(dataPath)} (${externalDataSource.rows.length} rows)`);
|
|
68357
68438
|
}
|
|
68358
68439
|
}
|
|
@@ -68362,7 +68443,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68362
68443
|
throw new Error(`No test found matching --scenario "${opts.scenario}"`);
|
|
68363
68444
|
}
|
|
68364
68445
|
}
|
|
68365
|
-
if (!
|
|
68446
|
+
if (!quiet) {
|
|
68366
68447
|
console.log(`
|
|
68367
68448
|
Loading: ${basename2(absPath)}`);
|
|
68368
68449
|
console.log(` Tests: ${file.tests.length}`);
|
|
@@ -68378,7 +68459,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68378
68459
|
if (scTags.length === 0) return false;
|
|
68379
68460
|
return tagMode === "any" ? filterTags.some((t) => scTags.includes(t)) : filterTags.every((t) => scTags.includes(t));
|
|
68380
68461
|
});
|
|
68381
|
-
if (!
|
|
68462
|
+
if (!quiet) {
|
|
68382
68463
|
console.log(` Scenario tags: ${filterTags.join(", ")} (mode: ${tagMode}, ${scenarios.length}/${before} scenarios matched)`);
|
|
68383
68464
|
}
|
|
68384
68465
|
if (scenarios.length === 0) {
|
|
@@ -68392,7 +68473,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68392
68473
|
const before = scenarios.length;
|
|
68393
68474
|
const result = filterScenariosByRowTags(scenarios, filterTags, tagMode);
|
|
68394
68475
|
scenarios = result.scenarios;
|
|
68395
|
-
if (!
|
|
68476
|
+
if (!quiet) {
|
|
68396
68477
|
console.log(` Tags: ${filterTags.join(", ")} (mode: ${tagMode}, ${result.matchingRowCount} matching rows, ${scenarios.length}/${before} scenarios retained)`);
|
|
68397
68478
|
if (result.droppedScenarioNames.length > 0) {
|
|
68398
68479
|
console.log(` Dropped: ${result.droppedScenarioNames.join(", ")} (no rows matched the tag filter)`);
|
|
@@ -68415,7 +68496,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68415
68496
|
maxErrors: opts.maxErrors,
|
|
68416
68497
|
maxErrorRate: opts.maxErrorRate
|
|
68417
68498
|
});
|
|
68418
|
-
if (!
|
|
68499
|
+
if (!quiet) {
|
|
68419
68500
|
console.log(` Mode: ${config.executionMode} (C:${config.concurrency} I:${config.iterations})`);
|
|
68420
68501
|
const paramTests = scenarios.filter((s2) => s2.dataSource && s2.dataSource.rows.length > 0);
|
|
68421
68502
|
if (paramTests.length > 0) {
|
|
@@ -68426,12 +68507,12 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68426
68507
|
}
|
|
68427
68508
|
const abortController = new AbortController();
|
|
68428
68509
|
process.on("SIGINT", () => {
|
|
68429
|
-
if (!
|
|
68510
|
+
if (!quiet) console.log("\n Aborting...");
|
|
68430
68511
|
abortController.abort();
|
|
68431
68512
|
});
|
|
68432
68513
|
let lastPrinted = 0;
|
|
68433
68514
|
const onProgress = (completed, total, _results, meta2) => {
|
|
68434
|
-
if (
|
|
68515
|
+
if (quiet) return;
|
|
68435
68516
|
const now = Date.now();
|
|
68436
68517
|
if (now - lastPrinted < 500 && completed < total) return;
|
|
68437
68518
|
lastPrinted = now;
|
|
@@ -68448,41 +68529,47 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68448
68529
|
const { results } = await runTest(config, scenarios, onProgress, abortController.signal);
|
|
68449
68530
|
const elapsed = performance.now() - t0;
|
|
68450
68531
|
const summary = computeMetrics(results, elapsed);
|
|
68451
|
-
if (!
|
|
68532
|
+
if (!quiet) {
|
|
68452
68533
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
68453
68534
|
}
|
|
68454
|
-
|
|
68535
|
+
if (!stdoutFormat) {
|
|
68536
|
+
printConsoleSummary(summary, config, results);
|
|
68537
|
+
}
|
|
68455
68538
|
const suiteName = file.name || basename2(absPath, ".yaml").replace(/\.yml$|\.json$/, "");
|
|
68456
68539
|
const meta = { name: file.name, env: opts.env || file.env, file: basename2(absPath) };
|
|
68457
68540
|
const slaTargets = opts.slaConfig ? loadSlaTargetFile(resolve5(opts.slaConfig)) : void 0;
|
|
68458
68541
|
if (slaTargets) {
|
|
68459
68542
|
config.slaTargets = slaTargets;
|
|
68460
68543
|
}
|
|
68461
|
-
if (
|
|
68544
|
+
if (stdoutFormat === "json") {
|
|
68545
|
+
process.stdout.write(JSON.stringify(buildCiJsonReport(results, elapsed), null, 2) + "\n");
|
|
68546
|
+
} else if (stdoutFormat === "junit") {
|
|
68547
|
+
process.stdout.write(buildJunitXml(results, summary, suiteName) + "\n");
|
|
68548
|
+
} else if (outputTarget?.kind === "file") {
|
|
68462
68549
|
const report = buildJsonReport(results, summary, config, meta);
|
|
68463
|
-
writeFileSync3(resolve5(
|
|
68464
|
-
console.log(` JSON report: ${
|
|
68550
|
+
writeFileSync3(resolve5(outputTarget.path), JSON.stringify(report, null, 2));
|
|
68551
|
+
console.log(` JSON report: ${outputTarget.path}`);
|
|
68465
68552
|
}
|
|
68466
68553
|
if (opts.junit) {
|
|
68467
68554
|
const xml = buildJunitXml(results, summary, suiteName);
|
|
68468
68555
|
writeFileSync3(resolve5(opts.junit), xml);
|
|
68469
|
-
console.log(` JUnit XML: ${opts.junit}`);
|
|
68556
|
+
if (!quiet) console.log(` JUnit XML: ${opts.junit}`);
|
|
68470
68557
|
}
|
|
68471
68558
|
if (opts.markdown) {
|
|
68472
68559
|
const md = buildMarkdownReport(summary, config, meta, results);
|
|
68473
68560
|
writeFileSync3(resolve5(opts.markdown), md);
|
|
68474
|
-
console.log(` Markdown: ${opts.markdown}`);
|
|
68561
|
+
if (!quiet) console.log(` Markdown: ${opts.markdown}`);
|
|
68475
68562
|
}
|
|
68476
68563
|
if (opts.dataRowsSummary) {
|
|
68477
68564
|
const rowSummary = buildDataRowSummary(results);
|
|
68478
68565
|
writeFileSync3(resolve5(opts.dataRowsSummary), JSON.stringify(rowSummary, null, 2));
|
|
68479
|
-
console.log(` Data Rows: ${opts.dataRowsSummary}`);
|
|
68566
|
+
if (!quiet) console.log(` Data Rows: ${opts.dataRowsSummary}`);
|
|
68480
68567
|
}
|
|
68481
68568
|
let hasSlaFail = false;
|
|
68482
68569
|
if (slaTargets) {
|
|
68483
68570
|
const checks = evaluateCliSla(summary, results, slaTargets);
|
|
68484
68571
|
hasSlaFail = !!(opts.failOnSla && overallSlaStatus(checks) === "fail");
|
|
68485
|
-
printSlaReport(checks,
|
|
68572
|
+
printSlaReport(checks, stdoutFormat !== null || quiet && !hasSlaFail);
|
|
68486
68573
|
}
|
|
68487
68574
|
let hasRegression = false;
|
|
68488
68575
|
if (opts.compareBaseline) {
|
|
@@ -68491,12 +68578,12 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68491
68578
|
let baselineEntry = null;
|
|
68492
68579
|
if (sentinel === LATEST_BASELINE_SENTINEL) {
|
|
68493
68580
|
baselineEntry = findLatestBaseline(absPath, baselinesDir);
|
|
68494
|
-
if (!baselineEntry && !
|
|
68581
|
+
if (!baselineEntry && !quiet) {
|
|
68495
68582
|
console.warn(` \u26A0 No baselines found for ${basename2(absPath)} \u2014 skipping regression check`);
|
|
68496
68583
|
}
|
|
68497
68584
|
} else {
|
|
68498
68585
|
baselineEntry = findBaselineById(sentinel, baselinesDir);
|
|
68499
|
-
if (!baselineEntry && !
|
|
68586
|
+
if (!baselineEntry && !quiet) {
|
|
68500
68587
|
console.warn(` \u26A0 Baseline not found: "${sentinel}" \u2014 skipping regression check`);
|
|
68501
68588
|
}
|
|
68502
68589
|
}
|
|
@@ -68522,13 +68609,13 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68522
68609
|
};
|
|
68523
68610
|
const comparison = compareRuns(baselineRun, currentRun, DEFAULT_THRESHOLDS);
|
|
68524
68611
|
printComparisonSummary(comparison, {
|
|
68525
|
-
quiet
|
|
68612
|
+
quiet,
|
|
68526
68613
|
baselineLabel: baselineEntry.label
|
|
68527
68614
|
});
|
|
68528
68615
|
if (opts.comparisonReport) {
|
|
68529
68616
|
const md = buildComparisonMarkdown(comparison, baselineEntry.label);
|
|
68530
68617
|
writeFileSync3(resolve5(opts.comparisonReport), md);
|
|
68531
|
-
if (!
|
|
68618
|
+
if (!quiet) {
|
|
68532
68619
|
console.log(` Comparison: ${opts.comparisonReport}`);
|
|
68533
68620
|
}
|
|
68534
68621
|
}
|
|
@@ -68548,7 +68635,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68548
68635
|
summary
|
|
68549
68636
|
};
|
|
68550
68637
|
addCliBaseline(entry, baselinesDir);
|
|
68551
|
-
if (!
|
|
68638
|
+
if (!quiet) {
|
|
68552
68639
|
console.log(` Baseline saved${entry.label ? ` (${entry.label})` : ""}: ${entry.runId}`);
|
|
68553
68640
|
}
|
|
68554
68641
|
}
|
|
@@ -68559,7 +68646,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68559
68646
|
process.exit(testFail ? 3 : 2);
|
|
68560
68647
|
}
|
|
68561
68648
|
if (testFail) {
|
|
68562
|
-
if (overThreshold) {
|
|
68649
|
+
if (overThreshold && !stdoutFormat) {
|
|
68563
68650
|
console.log(` Error rate ${summary.errorRate}% exceeds threshold ${opts.failThreshold}%`);
|
|
68564
68651
|
}
|
|
68565
68652
|
process.exit(1);
|
|
@@ -68571,14 +68658,17 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68571
68658
|
process.exit(1);
|
|
68572
68659
|
}
|
|
68573
68660
|
});
|
|
68574
|
-
program.command("workflow").description("Execute a workflow file as a performance test").argument("<file>", "Path to a workflow .yaml, .yml, or .json file").option("-i, --iterations <n>", "Total number of workflow iterations", (v) => parseInt(v, 10)).option("-c, --concurrency <n>", "Number of concurrent iterations", (v) => parseInt(v, 10)).option("--var <vars...>", "Set workflow variables (format: name=value)").option("--timeout <sec>", "Per-request timeout in seconds", (v) => parseInt(v, 10)).option("--error-policy <policy>", "Error policy: continue, stop-first, stop-threshold").option("--max-errors <n>", "Stop after N errors (threshold mode)", (v) => parseInt(v, 10)).option("--max-error-rate <pct>", "Stop at error rate % (threshold mode)", (v) => parseFloat(v)).option("--base-url <url>", "Base URL for HTTP nodes with relative paths").option("--trace-level <level>", "Trace capture level: minimal, standard, full, debug (default: standard)").option("--trace-output <path>", "Write the full execution trace (per-node/per-iteration) as JSON to file").option("--fail-on-error", "Exit code 1 if any request fails").option("--fail-threshold <pct>", "Exit code 1 if error rate exceeds this %", (v) => parseFloat(v)).option("-o, --output <path>", "Write JSON report to file").option("--junit <path>", "Write JUnit XML report to file").option("--markdown <path>", "Write Markdown report to file").option("-q, --quiet", "Suppress progress output").action(async (filePath, opts) => {
|
|
68661
|
+
program.command("workflow").description("Execute a workflow file as a performance test").argument("<file>", "Path to a workflow .yaml, .yml, or .json file").option("-i, --iterations <n>", "Total number of workflow iterations", (v) => parseInt(v, 10)).option("-c, --concurrency <n>", "Number of concurrent iterations", (v) => parseInt(v, 10)).option("--var <vars...>", "Set workflow variables (format: name=value)").option("--timeout <sec>", "Per-request timeout in seconds", (v) => parseInt(v, 10)).option("--error-policy <policy>", "Error policy: continue, stop-first, stop-threshold").option("--max-errors <n>", "Stop after N errors (threshold mode)", (v) => parseInt(v, 10)).option("--max-error-rate <pct>", "Stop at error rate % (threshold mode)", (v) => parseFloat(v)).option("--base-url <url>", "Base URL for HTTP nodes with relative paths").option("--trace-level <level>", "Trace capture level: minimal, standard, full, debug (default: standard)").option("--trace-output <path>", "Write the full execution trace (per-node/per-iteration) as JSON to file").option("--fail-on-error", "Exit code 1 if any request fails").option("--fail-threshold <pct>", "Exit code 1 if error rate exceeds this %", (v) => parseFloat(v)).option("-o, --output <path|json|junit>", "Write JSON report to file, or print `json`/`junit` to stdout for CI").option("--junit <path>", "Write JUnit XML report to file").option("--markdown <path>", "Write Markdown report to file").option("-q, --quiet", "Suppress progress output").action(async (filePath, opts) => {
|
|
68575
68662
|
try {
|
|
68663
|
+
const outputTarget = resolveOutputTarget(opts.output);
|
|
68664
|
+
const stdoutFormat = stdoutFormatOf(outputTarget);
|
|
68665
|
+
const quiet = Boolean(opts.quiet) || stdoutFormat !== null;
|
|
68576
68666
|
const absPath = resolve5(filePath);
|
|
68577
68667
|
if (!existsSync5(absPath)) {
|
|
68578
68668
|
throw new Error(`Workflow file not found: ${absPath}`);
|
|
68579
68669
|
}
|
|
68580
68670
|
const workflow = loadWorkflowFile(absPath);
|
|
68581
|
-
if (!
|
|
68671
|
+
if (!quiet) {
|
|
68582
68672
|
console.log(`
|
|
68583
68673
|
Loading: ${basename2(absPath)}`);
|
|
68584
68674
|
console.log(` Workflow: ${workflow.name}`);
|
|
@@ -68597,7 +68687,7 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68597
68687
|
variables[name2] = value;
|
|
68598
68688
|
}
|
|
68599
68689
|
}
|
|
68600
|
-
if (!
|
|
68690
|
+
if (!quiet && Object.keys(variables).length > 0) {
|
|
68601
68691
|
console.log(` Variables: ${Object.keys(variables).length}`);
|
|
68602
68692
|
for (const [k, v] of Object.entries(variables)) {
|
|
68603
68693
|
const display = v.length > 40 ? v.slice(0, 37) + "..." : v;
|
|
@@ -68606,13 +68696,13 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68606
68696
|
}
|
|
68607
68697
|
const iterations = opts.iterations ?? 10;
|
|
68608
68698
|
const concurrency = opts.concurrency ?? 1;
|
|
68609
|
-
if (!
|
|
68699
|
+
if (!quiet) {
|
|
68610
68700
|
console.log(` Mode: workflow (I:${iterations} C:${concurrency})`);
|
|
68611
68701
|
console.log("");
|
|
68612
68702
|
}
|
|
68613
68703
|
const abortController = new AbortController();
|
|
68614
68704
|
process.on("SIGINT", () => {
|
|
68615
|
-
if (!
|
|
68705
|
+
if (!quiet) console.log("\n Aborting...");
|
|
68616
68706
|
abortController.abort();
|
|
68617
68707
|
});
|
|
68618
68708
|
const breaker = new CircuitBreaker({
|
|
@@ -68622,7 +68712,7 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68622
68712
|
});
|
|
68623
68713
|
let lastPrinted = 0;
|
|
68624
68714
|
const onProgress = (completed, total, _results, _meta) => {
|
|
68625
|
-
if (
|
|
68715
|
+
if (quiet) return;
|
|
68626
68716
|
const now = Date.now();
|
|
68627
68717
|
if (now - lastPrinted < 500 && completed < total) return;
|
|
68628
68718
|
lastPrinted = now;
|
|
@@ -68630,7 +68720,7 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68630
68720
|
process.stdout.write(`\r Progress: ${completed}/${total} iterations (${pct}%)`);
|
|
68631
68721
|
};
|
|
68632
68722
|
const baseUrl = opts.baseUrl?.trim();
|
|
68633
|
-
if (!
|
|
68723
|
+
if (!quiet && baseUrl) {
|
|
68634
68724
|
console.log(` Base URL: ${baseUrl}`);
|
|
68635
68725
|
}
|
|
68636
68726
|
const t0 = performance.now();
|
|
@@ -68655,12 +68745,18 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68655
68745
|
});
|
|
68656
68746
|
const elapsed = performance.now() - t0;
|
|
68657
68747
|
const summary = computeMetrics(results, elapsed);
|
|
68658
|
-
if (!
|
|
68748
|
+
if (!quiet) {
|
|
68659
68749
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
68660
68750
|
}
|
|
68661
|
-
|
|
68751
|
+
if (!stdoutFormat) {
|
|
68752
|
+
printWorkflowConsoleSummary(summary, workflow, iterations, concurrency, results);
|
|
68753
|
+
}
|
|
68662
68754
|
const meta = { name: workflow.name, file: basename2(absPath) };
|
|
68663
|
-
if (
|
|
68755
|
+
if (stdoutFormat === "json") {
|
|
68756
|
+
process.stdout.write(JSON.stringify(buildWorkflowCiJsonReport(results, iterations, elapsed), null, 2) + "\n");
|
|
68757
|
+
} else if (stdoutFormat === "junit") {
|
|
68758
|
+
process.stdout.write(buildWorkflowJunitXml(results, summary, workflow.name, iterations) + "\n");
|
|
68759
|
+
} else if (outputTarget?.kind === "file") {
|
|
68664
68760
|
const report = buildJsonReport(results, summary, {
|
|
68665
68761
|
concurrency,
|
|
68666
68762
|
iterations,
|
|
@@ -68675,29 +68771,31 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68675
68771
|
maxErrors: opts.maxErrors ?? 10,
|
|
68676
68772
|
maxErrorRate: opts.maxErrorRate ?? 50
|
|
68677
68773
|
}, meta);
|
|
68678
|
-
writeFileSync3(resolve5(
|
|
68679
|
-
console.log(` JSON report: ${
|
|
68774
|
+
writeFileSync3(resolve5(outputTarget.path), JSON.stringify(report, null, 2));
|
|
68775
|
+
console.log(` JSON report: ${outputTarget.path}`);
|
|
68680
68776
|
}
|
|
68681
68777
|
if (opts.junit) {
|
|
68682
68778
|
const xml = buildWorkflowJunitXml(results, summary, workflow.name, iterations);
|
|
68683
68779
|
writeFileSync3(resolve5(opts.junit), xml);
|
|
68684
|
-
console.log(` JUnit XML: ${opts.junit}`);
|
|
68780
|
+
if (!quiet) console.log(` JUnit XML: ${opts.junit}`);
|
|
68685
68781
|
}
|
|
68686
68782
|
if (opts.markdown) {
|
|
68687
68783
|
const md = buildWorkflowMarkdownReport(summary, workflow, iterations, concurrency, results);
|
|
68688
68784
|
writeFileSync3(resolve5(opts.markdown), md);
|
|
68689
|
-
console.log(` Markdown: ${opts.markdown}`);
|
|
68785
|
+
if (!quiet) console.log(` Markdown: ${opts.markdown}`);
|
|
68690
68786
|
}
|
|
68691
68787
|
if (opts.traceOutput) {
|
|
68692
68788
|
writeFileSync3(resolve5(opts.traceOutput), JSON.stringify(trace, null, 2));
|
|
68693
|
-
console.log(` Trace: ${opts.traceOutput}`);
|
|
68789
|
+
if (!quiet) console.log(` Trace: ${opts.traceOutput}`);
|
|
68694
68790
|
}
|
|
68695
68791
|
const passed = summary.failedRequests === 0 && summary.failedValidations === 0;
|
|
68696
68792
|
if (opts.failOnError && !passed) {
|
|
68697
68793
|
process.exit(1);
|
|
68698
68794
|
}
|
|
68699
68795
|
if (opts.failThreshold != null && summary.errorRate > opts.failThreshold) {
|
|
68700
|
-
|
|
68796
|
+
if (!stdoutFormat) {
|
|
68797
|
+
console.log(` Error rate ${summary.errorRate}% exceeds threshold ${opts.failThreshold}%`);
|
|
68798
|
+
}
|
|
68701
68799
|
process.exit(1);
|
|
68702
68800
|
}
|
|
68703
68801
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redfireforge-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "RedfireForge CLI — API Performance Testing from the Command Line",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
"workflow",
|
|
15
15
|
"cli"
|
|
16
16
|
],
|
|
17
|
-
"homepage": "https://github.com/redfireforge/
|
|
17
|
+
"homepage": "https://github.com/redfireforge/redfireforge-public#readme",
|
|
18
18
|
"bugs": {
|
|
19
|
-
"url": "https://github.com/redfireforge/
|
|
19
|
+
"url": "https://github.com/redfireforge/redfireforge-public/issues"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/redfireforge/
|
|
23
|
+
"url": "git+https://github.com/redfireforge/redfireforge-public.git"
|
|
24
24
|
},
|
|
25
25
|
"license": "AGPL-3.0-or-later",
|
|
26
26
|
"author": "RedfireForge",
|