mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
Compare every on/off combination of requested Cargo features over the Boolean-lattice edges, in Gray-code build order, with a matrix index of configs, edge comparisons and provenance. Single runs record explicit feature sets as schemaVersion 2 with resolved defaults and build environments. Edge comparisons stay diagnostic; edges clean apart from the intended toggle are marked controlled. Validate with 68 focused tests, CLI fault checks, dry runs, and container smoke matrices for branch-b pan cases. AI-assisted-by: muse-spark-1.3-contributor-free
706 lines
20 KiB
JavaScript
706 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("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],
|
|
],
|
|
);
|
|
}
|
|
});
|