2026-10-28 19:17:38 +01:00

718 lines
20 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
readOptions,
bounded,
main,
recordAttempt,
finishCases,
defaultFeatures,
planFeatureMatrix,
validateFeatureSelection,
recordAmbientEnv,
BUILD_ENV,
classifyConfigResult,
ab,
} from "../../renderer-benchmarks.js";
test("build provenance expands the renderer's declared default features", () => {
assert.deepEqual(defaultFeatures({ default: [] }), ["default"]);
assert.deepEqual(
defaultFeatures({ default: ["fast"], fast: ["shared"], shared: ["fast"] }),
["default", "fast", "shared"],
);
assert.throws(
() => defaultFeatures({ default: ["profile"], profile: [] }),
/diagnostic/,
);
});
import {
collectCases,
deriveSeed,
validateMetrics,
} from "../../../resources/wasm-playground/benchmarks/contract.js";
import { scenarios } from "../../../resources/wasm-playground/scenes/index.js";
test("CLI validates bounded numbers, commands and URLs before execution", () => {
assert.equal(readOptions([]).repetitions, 10);
for (const args of [
["--repetitions", "NaN"],
["--warmups", "-1"],
["--seed", "4294967296"],
["--base-url", "file:///tmp"],
["--unknown"],
["compare", "one.json"],
]) {
assert.throws(() => readOptions(args));
}
assert.equal(
readOptions(["compare", "before.json", "after.json", "--diagnostic"])
.diagnostic,
true,
);
});
test("feature matrix plans Gray-ordered configurations and lattice edges", () => {
assert.deepEqual(planFeatureMatrix(["branch-b"]), {
configurations: [
{ slug: "0", features: [] },
{ slug: "1", features: ["branch-b"] },
],
edges: [{ from: "0", to: "1", toggledFeature: "branch-b" }],
});
const two = planFeatureMatrix(["a", "b"]);
assert.deepEqual(
two.configurations.map((c) => c.slug),
["00", "01", "11", "10"],
);
assert.deepEqual(
two.configurations.map((c) => c.features),
[[], ["b"], ["a", "b"], ["a"]],
);
assert.deepEqual(
two.edges.map((e) => `${e.from}__${e.to}:${e.toggledFeature}`).sort(),
["00__01:b", "00__10:a", "01__11:a", "10__11:b"],
);
const three = planFeatureMatrix(["a", "b", "c"]);
assert.equal(three.configurations.length, 8);
assert.equal(three.edges.length, 12);
assert.equal(new Set(three.edges.map((e) => `${e.from}__${e.to}`)).size, 12);
for (const config of three.configurations) {
assert.equal(
three.edges.filter((e) => e.from === config.slug || e.to === config.slug)
.length,
3,
);
}
});
test("ab help needs no features and dry run is ab-only", async () => {
assert.equal(readOptions(["ab", "--help"]).command, "ab");
assert.equal(await main(["ab", "--help"]), 0);
for (const args of [
["run", "--dry-run"],
["compare", "a.json", "b.json", "--dry-run"],
]) {
assert.throws(() => readOptions(args), /dry-run/);
}
});
test("CLI parses feature selection for run and ab", () => {
assert.deepEqual(readOptions(["run"]).features, []);
assert.deepEqual(readOptions(["run", "--features", "b,a"]).features, [
"a",
"b",
]);
assert.deepEqual(readOptions(["run", "--features", "a,a,, b "]).features, [
"a",
"b",
]);
assert.deepEqual(readOptions(["run", "--features", "profile"]).features, [
"profile",
]);
const ab = readOptions(["ab", "--features", "branch-b"]);
assert.deepEqual(ab.features, ["branch-b"]);
assert.equal(ab.maxFeatures, 3);
assert.equal(ab.confirm, false);
assert.equal(ab.dryRun, false);
assert.equal(
readOptions(["ab", "--features", "a", "--confirm", "--dry-run"]).confirm,
true,
);
for (const args of [
["ab"],
["ab", "--features", ""],
["ab", "--features", "a", "--max-features", "0"],
["ab", "--features", "a", "--max-features", "7"],
["ab", "--features", "Bad_Name"],
["ab", "--features", "profile"],
["ab", "--features", "stats-foo"],
["ab", "--features", "a", "extra.json"],
["run", "--features", "a", "extra.json"],
]) {
assert.throws(() => readOptions(args));
}
});
test("--no-build skips the renderer build only for run", () => {
assert.equal(readOptions(["run"]).noBuild, false);
assert.equal(readOptions(["run", "--no-build"]).noBuild, true);
for (const args of [
["ab", "--features", "branch-b", "--no-build"],
["compare", "a.json", "b.json", "--no-build"],
["run", "--no-build", "--features", "branch-b"],
]) {
assert.throws(() => readOptions(args), /--no-build/);
}
});
test("feature selection validates against the renderer manifest", () => {
const definitions = { default: [], "branch-a": [], "branch-b": [] };
assert.deepEqual(validateFeatureSelection(definitions, ["branch-b"], "ab"), [
"default",
]);
assert.deepEqual(validateFeatureSelection(definitions, [], "run"), [
"default",
]);
assert.throws(
() => validateFeatureSelection({ default: ["branch-b"] }, [], "ab"),
/empty default feature set/,
);
assert.deepEqual(
validateFeatureSelection({ default: ["branch-b"] }, [], "run"),
["branch-b", "default"],
);
assert.throws(
() => validateFeatureSelection(definitions, ["nope"], "run"),
/Unknown Cargo feature/,
);
for (const command of ["run", "ab"]) {
assert.throws(
() =>
validateFeatureSelection(
{ default: ["shared"], shared: [] },
["shared"],
command,
),
/already enabled by default/,
);
}
});
test("build environment recording excludes volatile values", () => {
assert.equal("VERSION" in BUILD_ENV, false);
assert.equal(BUILD_ENV.PENPOT_WASM_FUNCTION_NAMES, "0");
assert.deepEqual(
recordAmbientEnv({
RUSTFLAGS: "--deny warnings",
CARGO_HOME: "/home/penpot/.cargo",
CARGO_PROFILE_RELEASE_DEBUG: "true",
EMSDK: "/opt/emsdk",
EMSDK_CLANG: "clang",
PATH: "/usr/bin",
VERSION: "volatile",
PENPOT_WASM_PREPARED: "1",
}),
{
RUSTFLAGS: "--deny warnings",
CARGO_HOME: "/home/penpot/.cargo",
CARGO_PROFILE_RELEASE_DEBUG: "true",
EMSDK: "/opt/emsdk",
EMSDK_CLANG: "clang",
},
);
});
test("classifier separates continuation from matrix stops", () => {
const complete = {
status: "complete",
metadata: { build: { durationMs: 5 } },
cases: [{ attempts: [{}] }],
};
assert.deepEqual(classifyConfigResult(complete), {
outcome: "continue",
status: "complete",
});
assert.deepEqual(classifyConfigResult({ ...complete, status: "failed" }), {
outcome: "continue",
status: "failed",
});
assert.deepEqual(classifyConfigResult(null), {
outcome: "stop",
status: "unattempted",
error: "configuration output is missing or invalid",
});
assert.deepEqual(classifyConfigResult({ status: "interrupted" }), {
outcome: "stop",
status: "interrupted",
});
assert.deepEqual(
classifyConfigResult({
status: "failed",
metadata: { build: {} },
cases: [],
}),
{
outcome: "stop",
status: "failed",
error: "build or preflight failed before any suite ran",
},
);
assert.deepEqual(
classifyConfigResult({
status: "failed",
metadata: { build: { durationMs: 5 } },
cases: [],
}),
{
outcome: "stop",
status: "failed",
error: "suite recorded no attempts",
},
);
});
const abDefinitions = { default: [], "branch-a": [], "branch-b": [] };
async function abTempRoot() {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "renderer-ab-test-"));
return path.join(tmp, "matrix");
}
function abOptions(argv) {
return readOptions(argv);
}
function stubManifest(definitions = abDefinitions) {
return async () => definitions;
}
function stubRun(failSlugs = new Set()) {
const calls = [];
const runFn = async (options) => {
calls.push(options);
const slug = path.basename(options.output, ".json");
const failed = failSlugs.has(slug);
const result = failed
? {
schemaVersion: 2,
runId: `run-${slug}`,
status: "failed",
error: "./build failed (1)",
metadata: { build: { mode: "release", features: [] } },
cases: [],
}
: {
schemaVersion: 2,
runId: `run-${slug}`,
status: "complete",
metadata: {
build: {
mode: "release",
features: options.features,
durationMs: 5,
},
},
cases: [
{
id: "rects/default/pan",
status: "complete",
attempts: [{ index: 0, warmup: false, status: "ok" }],
},
],
};
await fs.mkdir(path.dirname(options.output), { recursive: true });
await fs.writeFile(options.output, JSON.stringify(result));
return failed ? 1 : 0;
};
return { calls, runFn };
}
function stubCompare(extra = []) {
const calls = [];
const compareFn = async (baseline, candidate, { diagnostic } = {}) => {
calls.push({ baseline, candidate, diagnostic });
return {
schemaVersion: 1,
compatible: false,
diagnostic: diagnostic ?? false,
mismatches: [
{
path: "metadata.build.features",
baseline: baseline.metadata.build.features,
candidate: candidate.metadata.build.features,
},
...extra,
],
warnings: [],
cases: [],
};
};
return { calls, compareFn };
}
test("ab refuses an existing output root before any preflight", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "renderer-ab-test-"));
const nonEmpty = path.join(tmp, "full");
await fs.mkdir(nonEmpty);
await fs.writeFile(path.join(nonEmpty, "x"), "x");
const filePath = path.join(tmp, "file.json");
await fs.writeFile(filePath, "{}");
const mustNotRun = () => {
throw new Error("must not be called");
};
for (const output of [nonEmpty, filePath]) {
await assert.rejects(
ab(abOptions(["ab", "--features", "branch-b", "--output", output]), {
runFn: mustNotRun,
compareFn: mustNotRun,
manifestFn: mustNotRun,
}),
/already exists|non-empty/,
);
}
await fs.rm(tmp, { recursive: true, force: true });
});
test("ab dry run prints the plan without building", async () => {
const output = await abTempRoot();
const mustNotRun = () => {
throw new Error("must not be called");
};
const code = await ab(
abOptions([
"ab",
"--features",
"branch-b",
"--filter",
"rects/default/pan",
"--dry-run",
"--output",
output,
]),
{ runFn: mustNotRun, compareFn: mustNotRun, manifestFn: stubManifest() },
);
assert.equal(code, 0);
await assert.rejects(fs.access(output), /ENOENT/);
await fs.rm(path.dirname(output), { recursive: true, force: true });
});
test("ab confirm gate blocks wide matrices and empty filters", async () => {
const runFn = () => {
throw new Error("must not be called");
};
const wide = await abTempRoot();
const narrow = await abTempRoot();
await assert.rejects(
ab(
abOptions([
"ab",
"--features",
"f1,f2,f3,f4",
"--max-features",
"4",
"--filter",
"rects",
"--output",
wide,
]),
{
runFn,
manifestFn: stubManifest({
default: [],
f1: [],
f2: [],
f3: [],
f4: [],
}),
},
),
/confirm/,
);
await assert.rejects(
ab(abOptions(["ab", "--features", "branch-b", "--output", narrow]), {
runFn,
manifestFn: stubManifest(),
}),
/confirm/,
);
await fs.rm(path.dirname(wide), { recursive: true, force: true });
await fs.rm(path.dirname(narrow), { recursive: true, force: true });
});
test("ab runs the matrix and writes configs, comparisons and index", async () => {
const output = await abTempRoot();
const { calls: runCalls, runFn } = stubRun();
const { calls: compareCalls, compareFn } = stubCompare();
const before = process.listenerCount("SIGINT");
const code = await ab(
abOptions([
"ab",
"--features",
"branch-b",
"--filter",
"rects/default/pan",
"--output",
output,
]),
{ runFn, compareFn, manifestFn: stubManifest() },
);
assert.equal(code, 0);
assert.equal(process.listenerCount("SIGINT"), before);
assert.deepEqual(
runCalls.map((options) => options.features),
[[], ["branch-b"]],
);
assert.equal(compareCalls.length, 1);
assert.equal(compareCalls[0].diagnostic, true);
const first = JSON.parse(
await fs.readFile(path.join(output, "configs", "0.json"), "utf8"),
);
assert.equal(first.runId, "run-0");
const comparison = JSON.parse(
await fs.readFile(path.join(output, "comparisons", "0__1.json"), "utf8"),
);
assert.deepEqual(comparison.edge, {
from: "0",
to: "1",
toggledFeature: "branch-b",
});
assert.deepEqual(
comparison.mismatches.filter((entry) => entry.expected),
[
{
path: "metadata.build.features",
baseline: [],
candidate: ["branch-b"],
expected: true,
toggledFeature: "branch-b",
},
],
);
const matrix = JSON.parse(
await fs.readFile(path.join(output, "matrix.json"), "utf8"),
);
assert.equal(matrix.schemaVersion, 1);
assert.deepEqual(matrix.features, ["branch-b"]);
assert.deepEqual(
matrix.configurations.map((c) => c.status),
["complete", "complete"],
);
assert.deepEqual(
matrix.comparisons.map((c) => [c.from, c.to, c.status]),
[["0", "1", "compared"]],
);
assert.equal(matrix.status, "complete");
assert.equal(comparison.controlled, true);
assert.equal(matrix.comparisons[0].controlled, true);
await fs.rm(path.dirname(output), { recursive: true, force: true });
});
test("ab marks edges with unexpected diffs uncontrolled", async () => {
const output = await abTempRoot();
const { runFn } = stubRun();
const { compareFn } = stubCompare([
{ path: "metadata.build.env", baseline: {}, candidate: {} },
]);
const code = await ab(
abOptions([
"ab",
"--features",
"branch-b",
"--filter",
"rects/default/pan",
"--output",
output,
]),
{ runFn, compareFn, manifestFn: stubManifest() },
);
assert.equal(code, 0);
const comparison = JSON.parse(
await fs.readFile(path.join(output, "comparisons", "0__1.json"), "utf8"),
);
assert.equal(comparison.controlled, false);
const matrix = JSON.parse(
await fs.readFile(path.join(output, "matrix.json"), "utf8"),
);
assert.equal(matrix.comparisons[0].controlled, false);
await fs.rm(path.dirname(output), { recursive: true, force: true });
});
test("ab interruption between configurations still writes the matrix", async () => {
const output = await abTempRoot();
const controller = new AbortController();
const { runFn } = stubRun();
const interruptAfterFirst = async (options) => {
const code = await runFn(options);
controller.abort(new Error("stopped"));
return code;
};
const { compareFn } = stubCompare();
const code = await ab(
abOptions([
"ab",
"--features",
"a,b",
"--max-features",
"2",
"--filter",
"rects",
"--confirm",
"--output",
output,
]),
{
runFn: interruptAfterFirst,
compareFn,
manifestFn: stubManifest({ default: [], a: [], b: [] }),
signal: controller.signal,
},
);
assert.equal(code, 1);
const matrix = JSON.parse(
await fs.readFile(path.join(output, "matrix.json"), "utf8"),
);
assert.deepEqual(
matrix.configurations.map((c) => [c.slug, c.status]),
[
["00", "complete"],
["01", "unattempted"],
["11", "unattempted"],
["10", "unattempted"],
],
);
assert.ok(matrix.comparisons.every((c) => c.status === "not-attempted"));
assert.equal(matrix.status, "failed");
await fs.rm(path.dirname(output), { recursive: true, force: true });
});
test("ab stops on build failure and marks the rest unattempted", async () => {
const output = await abTempRoot();
const { runFn } = stubRun(new Set(["01"]));
const { compareFn } = stubCompare();
const code = await ab(
abOptions([
"ab",
"--features",
"b,a",
"--max-features",
"2",
"--filter",
"rects",
"--confirm",
"--output",
output,
]),
{
runFn,
compareFn,
manifestFn: stubManifest({ default: [], a: [], b: [] }),
},
);
assert.equal(code, 1);
const matrix = JSON.parse(
await fs.readFile(path.join(output, "matrix.json"), "utf8"),
);
assert.deepEqual(
matrix.configurations.map((c) => [c.slug, c.status]),
[
["00", "complete"],
["01", "failed"],
["11", "unattempted"],
["10", "unattempted"],
],
);
assert.deepEqual(
matrix.comparisons.map((c) => [c.from, c.to, c.status]),
[
["00", "10", "not-attempted"],
["00", "01", "compared"],
["01", "11", "not-attempted"],
["10", "11", "not-attempted"],
],
);
assert.equal(matrix.status, "failed");
await fs.rm(path.dirname(output), { recursive: true, force: true });
});
test("case collector retains a single scene seed across lifecycle and interaction groups", () => {
const cases = collectCases(scenarios, 42);
assert.equal(cases.length, 21);
const rectangles = cases.filter((c) => c.scenarioId === "rects");
assert.equal(new Set(rectangles.map((c) => c.sceneSeed)).size, 1);
assert.equal(rectangles[0].sceneSeed, deriveSeed(42, "rects/default"));
assert.throws(() => collectCases(scenarios, 42, "missing"), /No matching/);
assert.throws(
() => collectCases([scenarios[0], scenarios[0]], 42),
/duplicate/,
);
});
test("case collector rejects missing interactions and non-JSON case values", () => {
const scenario = scenarios[0];
const invalid = {
...scenario,
cases: [{ ...scenario.cases[0], interactions: {} }],
};
assert.throws(() => collectCases([invalid], 42), /Incomplete/);
assert.throws(
() =>
collectCases(
[
{
...scenario,
cases: [{ ...scenario.cases[0], params: { count: Infinity } }],
},
],
42,
),
/Non-serializable/,
);
});
test("watchdog bounds unresolved work and propagates cancellation", async () => {
await assert.rejects(bounded(new Promise(() => {}), 10), /watchdog/);
const abort = new AbortController();
const operation = bounded(new Promise(() => {}), 10000, abort.signal);
abort.abort(new Error("stopped"));
await assert.rejects(operation, /stopped/);
assert.equal(await bounded(Promise.resolve(42), 100), 42);
});
test("invalid aggregates retain an explicit reason", () => {
assert.deepEqual(
validateMetrics({ uploadMs: 0, timeToFullMs: NaN, activeInteractionMs: 2 }),
{
uploadMs: "below-timer-resolution",
timeToFullMs: "non-finite",
},
);
});
test("help does not need a renderer build or browser", async () => {
assert.equal(await main(["--help"]), 0);
});
test("changed environment is stored as a failed observation", () => {
const c = {
status: "running",
attempts: [],
environment: { webgl: { renderer: "A" } },
};
recordAttempt(
c,
{
status: "ok",
metrics: { uploadMs: 1 },
environment: { webgl: { renderer: "B" } },
},
0,
0,
);
assert.equal(c.attempts[0].status, "failed");
assert.match(c.attempts[0].error, /environment changed/);
assert.equal(c.status, "failed");
});
test("fatal failure and interruption preserve partial and unattempted counts", () => {
for (const status of ["failed", "interrupted"]) {
const cases = [
{ status: "complete", attempts: [{ status: "ok" }, { status: "ok" }] },
{ status: "running", attempts: [{ status: "ok" }] },
{ status: "unattempted", attempts: [] },
];
finishCases(cases, 2, status);
assert.deepEqual(
cases.map((c) => [c.status, c.unattempted]),
[
["complete", 0],
[status, 1],
["unattempted", 2],
],
);
}
});