Skip to content

Commit 8c6da93

Browse files
committed
fix(test): fail startup bench on bad samples
1 parent bbdff39 commit 8c6da93

2 files changed

Lines changed: 135 additions & 2 deletions

File tree

scripts/bench-cli-startup.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
22
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
5+
import { pathToFileURL } from "node:url";
56

67
type CommandCase = {
78
id: string;
@@ -394,6 +395,17 @@ function parseRepeatableFlag(flag: string): string[] {
394395
}
395396

396397
function parsePositiveInt(raw: string | undefined, fallback: number): number {
398+
if (!raw) {
399+
return fallback;
400+
}
401+
const parsed = Number.parseInt(raw, 10);
402+
if (!Number.isFinite(parsed) || parsed < 1) {
403+
return fallback;
404+
}
405+
return parsed;
406+
}
407+
408+
function parseNonNegativeInt(raw: string | undefined, fallback: number): number {
397409
if (!raw) {
398410
return fallback;
399411
}
@@ -747,6 +759,25 @@ function printDelta(primary: SuiteResult, secondary: SuiteResult): void {
747759
}
748760
}
749761

762+
export function collectFailedSamples(result: SuiteResult): string[] {
763+
const failures: string[] = [];
764+
for (const commandCase of result.cases) {
765+
if (commandCase.samples.length === 0) {
766+
failures.push(`${result.entry} ${commandCase.id}: no measured samples`);
767+
continue;
768+
}
769+
for (const [sampleIndex, sample] of commandCase.samples.entries()) {
770+
const label = `${result.entry} ${commandCase.id} sample ${sampleIndex + 1}`;
771+
if (sample.signal !== null) {
772+
failures.push(`${label}: exited via signal ${sample.signal}`);
773+
} else if (sample.exitCode !== 0) {
774+
failures.push(`${label}: exited with code ${String(sample.exitCode)}`);
775+
}
776+
}
777+
}
778+
return failures;
779+
}
780+
750781
async function buildSuiteResult(params: {
751782
entry: string;
752783
options: CliOptions;
@@ -796,7 +827,7 @@ function parseOptions(): CliOptions {
796827
entryPrimary: parseFlagValue("--entry-primary") ?? parseFlagValue("--entry") ?? DEFAULT_ENTRY,
797828
entrySecondary: parseFlagValue("--entry-secondary"),
798829
runs: parsePositiveInt(parseFlagValue("--runs"), DEFAULT_RUNS),
799-
warmup: parsePositiveInt(parseFlagValue("--warmup"), DEFAULT_WARMUP),
830+
warmup: parseNonNegativeInt(parseFlagValue("--warmup"), DEFAULT_WARMUP),
800831
timeoutMs: parsePositiveInt(parseFlagValue("--timeout-ms"), DEFAULT_TIMEOUT_MS),
801832
json: hasFlag("--json"),
802833
output: parseFlagValue("--output"),
@@ -864,6 +895,10 @@ async function main(): Promise<void> {
864895
primary,
865896
secondary: secondary ?? null,
866897
};
898+
const failures = [
899+
...collectFailedSamples(primary),
900+
...(secondary ? collectFailedSamples(secondary) : []),
901+
];
867902

868903
if (options.output) {
869904
mkdirSync(path.dirname(options.output), { recursive: true });
@@ -872,6 +907,12 @@ async function main(): Promise<void> {
872907

873908
if (options.json) {
874909
console.log(JSON.stringify(report, null, 2));
910+
if (failures.length > 0) {
911+
process.exitCode = 1;
912+
for (const failure of failures) {
913+
console.error(`[startup-bench] ${failure}`);
914+
}
915+
}
875916
return;
876917
}
877918

@@ -894,9 +935,28 @@ async function main(): Promise<void> {
894935
printSuite(secondary);
895936
printDelta(primary, secondary);
896937
}
938+
939+
if (failures.length > 0) {
940+
process.exitCode = 1;
941+
console.error("\nFailed startup benchmark samples:");
942+
for (const failure of failures) {
943+
console.error(`- ${failure}`);
944+
}
945+
}
897946
} finally {
898947
rmSync(tmpDir, { recursive: true, force: true });
899948
}
900949
}
901950

902-
await main();
951+
export const testing = {
952+
collectFailedSamples,
953+
parseNonNegativeInt,
954+
parsePositiveInt,
955+
};
956+
957+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
958+
await main().catch((error: unknown) => {
959+
console.error(error instanceof Error ? error.stack : String(error));
960+
process.exit(1);
961+
});
962+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, expect, it } from "vitest";
2+
import { testing } from "../../scripts/bench-cli-startup.ts";
3+
4+
describe("bench-cli-startup", () => {
5+
it("fails reports with no measured samples", () => {
6+
expect(
7+
testing.collectFailedSamples({
8+
entry: "openclaw.mjs",
9+
cases: [
10+
{
11+
id: "version",
12+
name: "--version",
13+
args: ["--version"],
14+
contract: null,
15+
samples: [],
16+
summary: {
17+
sampleCount: 0,
18+
durationMs: { avg: 0, p50: 0, p95: 0, min: 0, max: 0 },
19+
firstOutputMs: null,
20+
maxRssMb: null,
21+
exitSummary: "",
22+
},
23+
},
24+
],
25+
}),
26+
).toEqual(["openclaw.mjs version: no measured samples"]);
27+
});
28+
29+
it("fails reports with nonzero or signaled CLI samples", () => {
30+
const passingSample = {
31+
ms: 10,
32+
firstOutputMs: 5,
33+
maxRssMb: 50,
34+
exitCode: 0,
35+
signal: null,
36+
};
37+
38+
expect(
39+
testing.collectFailedSamples({
40+
entry: "dist/entry.js",
41+
cases: [
42+
{
43+
id: "gatewayStatusJson",
44+
name: "gateway status --json",
45+
args: ["gateway", "status", "--json"],
46+
contract: null,
47+
samples: [
48+
passingSample,
49+
{ ...passingSample, exitCode: 1 },
50+
{ ...passingSample, exitCode: null, signal: "SIGTERM" },
51+
],
52+
summary: {
53+
sampleCount: 3,
54+
durationMs: { avg: 10, p50: 10, p95: 10, min: 10, max: 10 },
55+
firstOutputMs: { avg: 5, p50: 5, p95: 5, min: 5, max: 5 },
56+
maxRssMb: { avg: 50, p50: 50, p95: 50, min: 50, max: 50 },
57+
exitSummary: "code:0x1, code:1x1, signal:SIGTERMx1",
58+
},
59+
},
60+
],
61+
}),
62+
).toEqual([
63+
"dist/entry.js gatewayStatusJson sample 2: exited with code 1",
64+
"dist/entry.js gatewayStatusJson sample 3: exited via signal SIGTERM",
65+
]);
66+
});
67+
68+
it("does not accept zero measured runs", () => {
69+
expect(testing.parsePositiveInt("0", 5)).toBe(5);
70+
expect(testing.parsePositiveInt("1", 5)).toBe(1);
71+
expect(testing.parseNonNegativeInt("0", 1)).toBe(0);
72+
});
73+
});

0 commit comments

Comments
 (0)