Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/run-load-tests.mjs
T

153 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
// Safe runner for the k6 load-test suite.
//
// SKIPs cleanly (exit 0) when the k6 binary is missing or the target URL is
// unreachable, so this script is safe to wire into CI lanes that may run on
// machines without k6 installed. Only fails (non-zero exit) when k6 itself
// reports a threshold or runtime failure.
import path from "node:path";
import url from "node:url";
import { spawnSync } from "node:child_process";
const DEFAULT_TARGET_URL = "http://localhost:3000";
const DEFAULT_SCENARIO_PATH = "tests/load/scenarios.js";
const REACHABILITY_TIMEOUT_SECONDS = 3;
/**
* Test whether the k6 binary is available on PATH.
* Returns the trimmed version string on success, null on failure.
*/
export function detectK6Binary({ spawn = spawnSync } = {}) {
const result = spawn("k6", ["version"], { encoding: "utf8" });
if (result.error || result.status !== 0) {
return null;
}
return (result.stdout || "").trim();
}
/**
* Test whether the configured target URL responds to an HTTP request.
* Uses curl with a short timeout so the runner does not hang when the
* target environment is offline. Returns true on reachable, false otherwise.
*/
export function isTargetReachable(targetUrl, { spawn = spawnSync, timeoutSeconds = REACHABILITY_TIMEOUT_SECONDS } = {}) {
const result = spawn(
"curl",
[
"-fsS",
"--max-time",
String(timeoutSeconds),
"-o",
"/dev/null",
"-w",
"%{http_code}",
targetUrl,
],
{ encoding: "utf8" },
);
if (result.error) {
return false;
}
// curl -fsS exits non-zero on HTTP >= 400, but for reachability we accept
// any successful TCP+HTTP roundtrip. Re-check by looking at the captured
// status code in stdout when curl exited non-zero.
if (result.status === 0) {
return true;
}
const code = (result.stdout || "").trim();
return /^[1-5]\d\d$/.test(code);
}
/**
* Parse argv into a configuration object.
* --target <url> override K6_TARGET_URL
* --scenario <path> override the default composite scenario script
* --skip-reachability skip the curl probe (useful when the target is
* only reachable from within a Docker network)
*/
export function parseArgs(argv) {
const config = {
targetUrl: null,
scenarioPath: null,
skipReachability: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--target" || arg === "--target-url") {
config.targetUrl = argv[index + 1] || null;
index += 1;
} else if (arg === "--scenario") {
config.scenarioPath = argv[index + 1] || null;
index += 1;
} else if (arg === "--skip-reachability") {
config.skipReachability = true;
}
}
return config;
}
/**
* Resolve the final runtime configuration from argv + env, applying defaults.
*/
export function resolveConfig({ argv = [], env = process.env } = {}) {
const parsed = parseArgs(argv);
const targetUrl = parsed.targetUrl || env.K6_TARGET_URL || DEFAULT_TARGET_URL;
const scenarioPath = parsed.scenarioPath || DEFAULT_SCENARIO_PATH;
return {
targetUrl,
scenarioPath,
skipReachability: parsed.skipReachability,
};
}
/**
* Run the load-test suite. Returns an object describing the outcome so the
* CLI wrapper can map it to an exit code.
*/
export function runLoadTests({
argv = [],
env = process.env,
spawn = spawnSync,
stdout = (line) => process.stdout.write(`${line}\n`),
stderr = (line) => process.stderr.write(`${line}\n`),
} = {}) {
const config = resolveConfig({ argv, env });
const version = detectK6Binary({ spawn });
if (!version) {
stdout(
"SKIP k6 binary not installed; download from https://k6.io/docs/getting-started/installation/",
);
return { status: "skip", reason: "binary-missing", exitCode: 0 };
}
if (!config.skipReachability && !isTargetReachable(config.targetUrl, { spawn })) {
stdout(`SKIP target ${config.targetUrl} not reachable`);
return { status: "skip", reason: "target-unreachable", exitCode: 0 };
}
stdout(`PASS k6 detected (${version}); running ${config.scenarioPath} against ${config.targetUrl}`);
const result = spawn("k6", ["run", config.scenarioPath], {
stdio: "inherit",
env: { ...env, K6_TARGET_URL: config.targetUrl },
});
if (result.error) {
stderr(`FAIL k6 run failed to start: ${result.error.message}`);
return { status: "fail", reason: "spawn-error", exitCode: 1 };
}
if (typeof result.status === "number" && result.status !== 0) {
stderr(`FAIL k6 run exited with status ${result.status}`);
return { status: "fail", reason: "non-zero-exit", exitCode: result.status };
}
return { status: "pass", reason: "ok", exitCode: 0 };
}
const isCli = process.argv[1] && url.fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isCli) {
const outcome = runLoadTests({ argv: process.argv.slice(2) });
process.exit(outcome.exitCode);
}