redfireforge-cli 0.8.2 → 0.8.4
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 +153 -59
- 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
|
@@ -63458,6 +63458,59 @@ function buildJsonReport(results, summary, config, meta) {
|
|
|
63458
63458
|
projectName: meta.name
|
|
63459
63459
|
};
|
|
63460
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
|
+
}
|
|
63461
63514
|
function escapeXml(s2) {
|
|
63462
63515
|
return s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
63463
63516
|
}
|
|
@@ -63655,26 +63708,26 @@ function computePerStepStats(results) {
|
|
|
63655
63708
|
}
|
|
63656
63709
|
return stats;
|
|
63657
63710
|
}
|
|
63658
|
-
function
|
|
63711
|
+
function groupResultsByIteration(results, iterations) {
|
|
63659
63712
|
const byIteration = /* @__PURE__ */ new Map();
|
|
63660
63713
|
for (const r of results) {
|
|
63661
63714
|
const idx = r.iterationIndex ?? 0;
|
|
63662
63715
|
if (!byIteration.has(idx)) byIteration.set(idx, []);
|
|
63663
63716
|
byIteration.get(idx).push(r);
|
|
63664
63717
|
}
|
|
63665
|
-
const
|
|
63718
|
+
const grouped = [];
|
|
63666
63719
|
for (let i = 0; i < iterations; i++) {
|
|
63667
|
-
|
|
63668
|
-
const allPassed = iterResults.every((r) => r.passed);
|
|
63669
|
-
const totalDuration = iterResults.reduce((sum, r) => sum + r.responseTimeMs, 0);
|
|
63670
|
-
stats.push({
|
|
63671
|
-
index: i,
|
|
63672
|
-
passed: allPassed,
|
|
63673
|
-
durationMs: totalDuration,
|
|
63674
|
-
stepCount: iterResults.length
|
|
63675
|
-
});
|
|
63720
|
+
grouped.push(byIteration.get(i) || []);
|
|
63676
63721
|
}
|
|
63677
|
-
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
|
+
}));
|
|
63678
63731
|
}
|
|
63679
63732
|
function formatRowLabel(r) {
|
|
63680
63733
|
return r.dataRowLabel || r.dataRowId || "?";
|
|
@@ -63894,6 +63947,22 @@ function buildWorkflowMarkdownReport(summary, workflow, iterations, concurrency,
|
|
|
63894
63947
|
return lines.join("\n");
|
|
63895
63948
|
}
|
|
63896
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
|
+
|
|
63897
63966
|
// cli/slaEval.ts
|
|
63898
63967
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
63899
63968
|
function getMetricFromSummary(metric, summary) {
|
|
@@ -67063,7 +67132,7 @@ var ApiMockNetworkListener = class {
|
|
|
67063
67132
|
handler
|
|
67064
67133
|
);
|
|
67065
67134
|
} catch (err) {
|
|
67066
|
-
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 });
|
|
67067
67136
|
}
|
|
67068
67137
|
} else {
|
|
67069
67138
|
server = http.createServer(handler);
|
|
@@ -67814,6 +67883,8 @@ function sleep4(ms) {
|
|
|
67814
67883
|
|
|
67815
67884
|
// cli/mockCommands.ts
|
|
67816
67885
|
async function runMockSimulate(opts) {
|
|
67886
|
+
const outputTarget = resolveOutputTarget(opts.output);
|
|
67887
|
+
const stdoutFormat = stdoutFormatOf(outputTarget);
|
|
67817
67888
|
const raw = asWorkspace(loadDefinitionFile(opts.file));
|
|
67818
67889
|
const loaded = cliLoadAndValidate(raw);
|
|
67819
67890
|
if (reportValidation(loaded.validationErrors)) return 1;
|
|
@@ -67832,10 +67903,7 @@ async function runMockSimulate(opts) {
|
|
|
67832
67903
|
failed: failed.length,
|
|
67833
67904
|
results
|
|
67834
67905
|
};
|
|
67835
|
-
const
|
|
67836
|
-
if (opts.output) writeFileSync2(resolve4(opts.output), json, "utf8");
|
|
67837
|
-
else console.log(json);
|
|
67838
|
-
if (opts.junit) {
|
|
67906
|
+
const buildJunit = () => {
|
|
67839
67907
|
const cases = results.map((r) => {
|
|
67840
67908
|
const name2 = r.sampleId;
|
|
67841
67909
|
if (isFailedSimulation(r)) {
|
|
@@ -67843,12 +67911,18 @@ async function runMockSimulate(opts) {
|
|
|
67843
67911
|
}
|
|
67844
67912
|
return `<testcase classname="api-mock" name="${escapeXml2(name2)}"/>`;
|
|
67845
67913
|
}).join("\n");
|
|
67846
|
-
|
|
67914
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
67847
67915
|
<testsuite name="api-mock-simulate" tests="${results.length}" failures="${failed.length}">
|
|
67848
67916
|
${cases}
|
|
67849
67917
|
</testsuite>
|
|
67850
67918
|
`;
|
|
67851
|
-
|
|
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");
|
|
67852
67926
|
}
|
|
67853
67927
|
console.error(`Simulated ${results.length} sample(s); ${failed.length} failure(s).`);
|
|
67854
67928
|
return failed.length > 0 ? 1 : 0;
|
|
@@ -68272,7 +68346,7 @@ async function runMockVerifyAllRoutes(opts) {
|
|
|
68272
68346
|
// cli/mockCommandRegistration.ts
|
|
68273
68347
|
function registerMockCommands(program2) {
|
|
68274
68348
|
const mock = program2.command("mock").description("API Mock Studio headless commands");
|
|
68275
|
-
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) => {
|
|
68276
68350
|
const code = await runMockSimulate({ file, serverId: opts.server, output: opts.output, junit: opts.junit });
|
|
68277
68351
|
process.exit(code);
|
|
68278
68352
|
});
|
|
@@ -68348,15 +68422,18 @@ var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
|
68348
68422
|
var pkg = JSON.parse(readFileSync7(resolve5(__dirname, "..", "package.json"), "utf-8"));
|
|
68349
68423
|
var program = new Command();
|
|
68350
68424
|
program.name("redfireforge").description("RedfireForge CLI \u2014 run API performance tests from YAML/JSON files").version(pkg.version);
|
|
68351
|
-
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) => {
|
|
68352
68426
|
try {
|
|
68427
|
+
const outputTarget = resolveOutputTarget(opts.output);
|
|
68428
|
+
const stdoutFormat = stdoutFormatOf(outputTarget);
|
|
68429
|
+
const quiet = Boolean(opts.quiet) || stdoutFormat !== null;
|
|
68353
68430
|
const absPath = resolve5(filePath);
|
|
68354
68431
|
const file = loadTestFile(absPath);
|
|
68355
68432
|
let externalDataSource;
|
|
68356
68433
|
if (opts.data) {
|
|
68357
68434
|
const dataPath = resolve5(opts.data);
|
|
68358
68435
|
externalDataSource = loadDataFile(dataPath);
|
|
68359
|
-
if (!
|
|
68436
|
+
if (!quiet) {
|
|
68360
68437
|
console.log(` Data: ${basename2(dataPath)} (${externalDataSource.rows.length} rows)`);
|
|
68361
68438
|
}
|
|
68362
68439
|
}
|
|
@@ -68366,7 +68443,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68366
68443
|
throw new Error(`No test found matching --scenario "${opts.scenario}"`);
|
|
68367
68444
|
}
|
|
68368
68445
|
}
|
|
68369
|
-
if (!
|
|
68446
|
+
if (!quiet) {
|
|
68370
68447
|
console.log(`
|
|
68371
68448
|
Loading: ${basename2(absPath)}`);
|
|
68372
68449
|
console.log(` Tests: ${file.tests.length}`);
|
|
@@ -68382,7 +68459,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68382
68459
|
if (scTags.length === 0) return false;
|
|
68383
68460
|
return tagMode === "any" ? filterTags.some((t) => scTags.includes(t)) : filterTags.every((t) => scTags.includes(t));
|
|
68384
68461
|
});
|
|
68385
|
-
if (!
|
|
68462
|
+
if (!quiet) {
|
|
68386
68463
|
console.log(` Scenario tags: ${filterTags.join(", ")} (mode: ${tagMode}, ${scenarios.length}/${before} scenarios matched)`);
|
|
68387
68464
|
}
|
|
68388
68465
|
if (scenarios.length === 0) {
|
|
@@ -68396,7 +68473,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68396
68473
|
const before = scenarios.length;
|
|
68397
68474
|
const result = filterScenariosByRowTags(scenarios, filterTags, tagMode);
|
|
68398
68475
|
scenarios = result.scenarios;
|
|
68399
|
-
if (!
|
|
68476
|
+
if (!quiet) {
|
|
68400
68477
|
console.log(` Tags: ${filterTags.join(", ")} (mode: ${tagMode}, ${result.matchingRowCount} matching rows, ${scenarios.length}/${before} scenarios retained)`);
|
|
68401
68478
|
if (result.droppedScenarioNames.length > 0) {
|
|
68402
68479
|
console.log(` Dropped: ${result.droppedScenarioNames.join(", ")} (no rows matched the tag filter)`);
|
|
@@ -68419,7 +68496,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68419
68496
|
maxErrors: opts.maxErrors,
|
|
68420
68497
|
maxErrorRate: opts.maxErrorRate
|
|
68421
68498
|
});
|
|
68422
|
-
if (!
|
|
68499
|
+
if (!quiet) {
|
|
68423
68500
|
console.log(` Mode: ${config.executionMode} (C:${config.concurrency} I:${config.iterations})`);
|
|
68424
68501
|
const paramTests = scenarios.filter((s2) => s2.dataSource && s2.dataSource.rows.length > 0);
|
|
68425
68502
|
if (paramTests.length > 0) {
|
|
@@ -68430,12 +68507,12 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68430
68507
|
}
|
|
68431
68508
|
const abortController = new AbortController();
|
|
68432
68509
|
process.on("SIGINT", () => {
|
|
68433
|
-
if (!
|
|
68510
|
+
if (!quiet) console.log("\n Aborting...");
|
|
68434
68511
|
abortController.abort();
|
|
68435
68512
|
});
|
|
68436
68513
|
let lastPrinted = 0;
|
|
68437
68514
|
const onProgress = (completed, total, _results, meta2) => {
|
|
68438
|
-
if (
|
|
68515
|
+
if (quiet) return;
|
|
68439
68516
|
const now = Date.now();
|
|
68440
68517
|
if (now - lastPrinted < 500 && completed < total) return;
|
|
68441
68518
|
lastPrinted = now;
|
|
@@ -68452,41 +68529,47 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68452
68529
|
const { results } = await runTest(config, scenarios, onProgress, abortController.signal);
|
|
68453
68530
|
const elapsed = performance.now() - t0;
|
|
68454
68531
|
const summary = computeMetrics(results, elapsed);
|
|
68455
|
-
if (!
|
|
68532
|
+
if (!quiet) {
|
|
68456
68533
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
68457
68534
|
}
|
|
68458
|
-
|
|
68535
|
+
if (!stdoutFormat) {
|
|
68536
|
+
printConsoleSummary(summary, config, results);
|
|
68537
|
+
}
|
|
68459
68538
|
const suiteName = file.name || basename2(absPath, ".yaml").replace(/\.yml$|\.json$/, "");
|
|
68460
68539
|
const meta = { name: file.name, env: opts.env || file.env, file: basename2(absPath) };
|
|
68461
68540
|
const slaTargets = opts.slaConfig ? loadSlaTargetFile(resolve5(opts.slaConfig)) : void 0;
|
|
68462
68541
|
if (slaTargets) {
|
|
68463
68542
|
config.slaTargets = slaTargets;
|
|
68464
68543
|
}
|
|
68465
|
-
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") {
|
|
68466
68549
|
const report = buildJsonReport(results, summary, config, meta);
|
|
68467
|
-
writeFileSync3(resolve5(
|
|
68468
|
-
console.log(` JSON report: ${
|
|
68550
|
+
writeFileSync3(resolve5(outputTarget.path), JSON.stringify(report, null, 2));
|
|
68551
|
+
console.log(` JSON report: ${outputTarget.path}`);
|
|
68469
68552
|
}
|
|
68470
68553
|
if (opts.junit) {
|
|
68471
68554
|
const xml = buildJunitXml(results, summary, suiteName);
|
|
68472
68555
|
writeFileSync3(resolve5(opts.junit), xml);
|
|
68473
|
-
console.log(` JUnit XML: ${opts.junit}`);
|
|
68556
|
+
if (!quiet) console.log(` JUnit XML: ${opts.junit}`);
|
|
68474
68557
|
}
|
|
68475
68558
|
if (opts.markdown) {
|
|
68476
68559
|
const md = buildMarkdownReport(summary, config, meta, results);
|
|
68477
68560
|
writeFileSync3(resolve5(opts.markdown), md);
|
|
68478
|
-
console.log(` Markdown: ${opts.markdown}`);
|
|
68561
|
+
if (!quiet) console.log(` Markdown: ${opts.markdown}`);
|
|
68479
68562
|
}
|
|
68480
68563
|
if (opts.dataRowsSummary) {
|
|
68481
68564
|
const rowSummary = buildDataRowSummary(results);
|
|
68482
68565
|
writeFileSync3(resolve5(opts.dataRowsSummary), JSON.stringify(rowSummary, null, 2));
|
|
68483
|
-
console.log(` Data Rows: ${opts.dataRowsSummary}`);
|
|
68566
|
+
if (!quiet) console.log(` Data Rows: ${opts.dataRowsSummary}`);
|
|
68484
68567
|
}
|
|
68485
68568
|
let hasSlaFail = false;
|
|
68486
68569
|
if (slaTargets) {
|
|
68487
68570
|
const checks = evaluateCliSla(summary, results, slaTargets);
|
|
68488
68571
|
hasSlaFail = !!(opts.failOnSla && overallSlaStatus(checks) === "fail");
|
|
68489
|
-
printSlaReport(checks,
|
|
68572
|
+
printSlaReport(checks, stdoutFormat !== null || quiet && !hasSlaFail);
|
|
68490
68573
|
}
|
|
68491
68574
|
let hasRegression = false;
|
|
68492
68575
|
if (opts.compareBaseline) {
|
|
@@ -68495,12 +68578,12 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68495
68578
|
let baselineEntry = null;
|
|
68496
68579
|
if (sentinel === LATEST_BASELINE_SENTINEL) {
|
|
68497
68580
|
baselineEntry = findLatestBaseline(absPath, baselinesDir);
|
|
68498
|
-
if (!baselineEntry && !
|
|
68581
|
+
if (!baselineEntry && !quiet) {
|
|
68499
68582
|
console.warn(` \u26A0 No baselines found for ${basename2(absPath)} \u2014 skipping regression check`);
|
|
68500
68583
|
}
|
|
68501
68584
|
} else {
|
|
68502
68585
|
baselineEntry = findBaselineById(sentinel, baselinesDir);
|
|
68503
|
-
if (!baselineEntry && !
|
|
68586
|
+
if (!baselineEntry && !quiet) {
|
|
68504
68587
|
console.warn(` \u26A0 Baseline not found: "${sentinel}" \u2014 skipping regression check`);
|
|
68505
68588
|
}
|
|
68506
68589
|
}
|
|
@@ -68526,13 +68609,13 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68526
68609
|
};
|
|
68527
68610
|
const comparison = compareRuns(baselineRun, currentRun, DEFAULT_THRESHOLDS);
|
|
68528
68611
|
printComparisonSummary(comparison, {
|
|
68529
|
-
quiet
|
|
68612
|
+
quiet,
|
|
68530
68613
|
baselineLabel: baselineEntry.label
|
|
68531
68614
|
});
|
|
68532
68615
|
if (opts.comparisonReport) {
|
|
68533
68616
|
const md = buildComparisonMarkdown(comparison, baselineEntry.label);
|
|
68534
68617
|
writeFileSync3(resolve5(opts.comparisonReport), md);
|
|
68535
|
-
if (!
|
|
68618
|
+
if (!quiet) {
|
|
68536
68619
|
console.log(` Comparison: ${opts.comparisonReport}`);
|
|
68537
68620
|
}
|
|
68538
68621
|
}
|
|
@@ -68552,7 +68635,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68552
68635
|
summary
|
|
68553
68636
|
};
|
|
68554
68637
|
addCliBaseline(entry, baselinesDir);
|
|
68555
|
-
if (!
|
|
68638
|
+
if (!quiet) {
|
|
68556
68639
|
console.log(` Baseline saved${entry.label ? ` (${entry.label})` : ""}: ${entry.runId}`);
|
|
68557
68640
|
}
|
|
68558
68641
|
}
|
|
@@ -68563,7 +68646,7 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68563
68646
|
process.exit(testFail ? 3 : 2);
|
|
68564
68647
|
}
|
|
68565
68648
|
if (testFail) {
|
|
68566
|
-
if (overThreshold) {
|
|
68649
|
+
if (overThreshold && !stdoutFormat) {
|
|
68567
68650
|
console.log(` Error rate ${summary.errorRate}% exceeds threshold ${opts.failThreshold}%`);
|
|
68568
68651
|
}
|
|
68569
68652
|
process.exit(1);
|
|
@@ -68575,14 +68658,17 @@ program.command("run").description("Execute a test file").argument("<file>", "Pa
|
|
|
68575
68658
|
process.exit(1);
|
|
68576
68659
|
}
|
|
68577
68660
|
});
|
|
68578
|
-
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) => {
|
|
68579
68662
|
try {
|
|
68663
|
+
const outputTarget = resolveOutputTarget(opts.output);
|
|
68664
|
+
const stdoutFormat = stdoutFormatOf(outputTarget);
|
|
68665
|
+
const quiet = Boolean(opts.quiet) || stdoutFormat !== null;
|
|
68580
68666
|
const absPath = resolve5(filePath);
|
|
68581
68667
|
if (!existsSync5(absPath)) {
|
|
68582
68668
|
throw new Error(`Workflow file not found: ${absPath}`);
|
|
68583
68669
|
}
|
|
68584
68670
|
const workflow = loadWorkflowFile(absPath);
|
|
68585
|
-
if (!
|
|
68671
|
+
if (!quiet) {
|
|
68586
68672
|
console.log(`
|
|
68587
68673
|
Loading: ${basename2(absPath)}`);
|
|
68588
68674
|
console.log(` Workflow: ${workflow.name}`);
|
|
@@ -68601,7 +68687,7 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68601
68687
|
variables[name2] = value;
|
|
68602
68688
|
}
|
|
68603
68689
|
}
|
|
68604
|
-
if (!
|
|
68690
|
+
if (!quiet && Object.keys(variables).length > 0) {
|
|
68605
68691
|
console.log(` Variables: ${Object.keys(variables).length}`);
|
|
68606
68692
|
for (const [k, v] of Object.entries(variables)) {
|
|
68607
68693
|
const display = v.length > 40 ? v.slice(0, 37) + "..." : v;
|
|
@@ -68610,13 +68696,13 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68610
68696
|
}
|
|
68611
68697
|
const iterations = opts.iterations ?? 10;
|
|
68612
68698
|
const concurrency = opts.concurrency ?? 1;
|
|
68613
|
-
if (!
|
|
68699
|
+
if (!quiet) {
|
|
68614
68700
|
console.log(` Mode: workflow (I:${iterations} C:${concurrency})`);
|
|
68615
68701
|
console.log("");
|
|
68616
68702
|
}
|
|
68617
68703
|
const abortController = new AbortController();
|
|
68618
68704
|
process.on("SIGINT", () => {
|
|
68619
|
-
if (!
|
|
68705
|
+
if (!quiet) console.log("\n Aborting...");
|
|
68620
68706
|
abortController.abort();
|
|
68621
68707
|
});
|
|
68622
68708
|
const breaker = new CircuitBreaker({
|
|
@@ -68626,7 +68712,7 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68626
68712
|
});
|
|
68627
68713
|
let lastPrinted = 0;
|
|
68628
68714
|
const onProgress = (completed, total, _results, _meta) => {
|
|
68629
|
-
if (
|
|
68715
|
+
if (quiet) return;
|
|
68630
68716
|
const now = Date.now();
|
|
68631
68717
|
if (now - lastPrinted < 500 && completed < total) return;
|
|
68632
68718
|
lastPrinted = now;
|
|
@@ -68634,7 +68720,7 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68634
68720
|
process.stdout.write(`\r Progress: ${completed}/${total} iterations (${pct}%)`);
|
|
68635
68721
|
};
|
|
68636
68722
|
const baseUrl = opts.baseUrl?.trim();
|
|
68637
|
-
if (!
|
|
68723
|
+
if (!quiet && baseUrl) {
|
|
68638
68724
|
console.log(` Base URL: ${baseUrl}`);
|
|
68639
68725
|
}
|
|
68640
68726
|
const t0 = performance.now();
|
|
@@ -68659,12 +68745,18 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68659
68745
|
});
|
|
68660
68746
|
const elapsed = performance.now() - t0;
|
|
68661
68747
|
const summary = computeMetrics(results, elapsed);
|
|
68662
|
-
if (!
|
|
68748
|
+
if (!quiet) {
|
|
68663
68749
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
68664
68750
|
}
|
|
68665
|
-
|
|
68751
|
+
if (!stdoutFormat) {
|
|
68752
|
+
printWorkflowConsoleSummary(summary, workflow, iterations, concurrency, results);
|
|
68753
|
+
}
|
|
68666
68754
|
const meta = { name: workflow.name, file: basename2(absPath) };
|
|
68667
|
-
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") {
|
|
68668
68760
|
const report = buildJsonReport(results, summary, {
|
|
68669
68761
|
concurrency,
|
|
68670
68762
|
iterations,
|
|
@@ -68679,29 +68771,31 @@ program.command("workflow").description("Execute a workflow file as a performanc
|
|
|
68679
68771
|
maxErrors: opts.maxErrors ?? 10,
|
|
68680
68772
|
maxErrorRate: opts.maxErrorRate ?? 50
|
|
68681
68773
|
}, meta);
|
|
68682
|
-
writeFileSync3(resolve5(
|
|
68683
|
-
console.log(` JSON report: ${
|
|
68774
|
+
writeFileSync3(resolve5(outputTarget.path), JSON.stringify(report, null, 2));
|
|
68775
|
+
console.log(` JSON report: ${outputTarget.path}`);
|
|
68684
68776
|
}
|
|
68685
68777
|
if (opts.junit) {
|
|
68686
68778
|
const xml = buildWorkflowJunitXml(results, summary, workflow.name, iterations);
|
|
68687
68779
|
writeFileSync3(resolve5(opts.junit), xml);
|
|
68688
|
-
console.log(` JUnit XML: ${opts.junit}`);
|
|
68780
|
+
if (!quiet) console.log(` JUnit XML: ${opts.junit}`);
|
|
68689
68781
|
}
|
|
68690
68782
|
if (opts.markdown) {
|
|
68691
68783
|
const md = buildWorkflowMarkdownReport(summary, workflow, iterations, concurrency, results);
|
|
68692
68784
|
writeFileSync3(resolve5(opts.markdown), md);
|
|
68693
|
-
console.log(` Markdown: ${opts.markdown}`);
|
|
68785
|
+
if (!quiet) console.log(` Markdown: ${opts.markdown}`);
|
|
68694
68786
|
}
|
|
68695
68787
|
if (opts.traceOutput) {
|
|
68696
68788
|
writeFileSync3(resolve5(opts.traceOutput), JSON.stringify(trace, null, 2));
|
|
68697
|
-
console.log(` Trace: ${opts.traceOutput}`);
|
|
68789
|
+
if (!quiet) console.log(` Trace: ${opts.traceOutput}`);
|
|
68698
68790
|
}
|
|
68699
68791
|
const passed = summary.failedRequests === 0 && summary.failedValidations === 0;
|
|
68700
68792
|
if (opts.failOnError && !passed) {
|
|
68701
68793
|
process.exit(1);
|
|
68702
68794
|
}
|
|
68703
68795
|
if (opts.failThreshold != null && summary.errorRate > opts.failThreshold) {
|
|
68704
|
-
|
|
68796
|
+
if (!stdoutFormat) {
|
|
68797
|
+
console.log(` Error rate ${summary.errorRate}% exceeds threshold ${opts.failThreshold}%`);
|
|
68798
|
+
}
|
|
68705
68799
|
process.exit(1);
|
|
68706
68800
|
}
|
|
68707
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.4",
|
|
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",
|