148 lines
6.3 KiB
TypeScript
148 lines
6.3 KiB
TypeScript
import { test } from "node:test";
|
||
import assert from "node:assert";
|
||
import {
|
||
classifyTestFiles,
|
||
aggregateRadiography,
|
||
classifyFromCounts,
|
||
redundancyCandidates,
|
||
} from "../../../scripts/quality/mutation-radiography.mjs";
|
||
|
||
// ── classifyTestFiles: the plan's canonical fixture ──────────────────────────
|
||
// 2 testFiles that kill mutants + 1 that kills nothing.
|
||
// m1 killed ONLY by A -> A gets a unique kill
|
||
// m2 killed by A AND B -> both get a shared kill
|
||
// m3 survived -> nobody
|
||
// Universe = [A, B, C]; C never appears in any killedBy -> empty.
|
||
test("classifies unique / redundant / empty test files from killedBy (file-name killedBy)", () => {
|
||
const report = {
|
||
files: {
|
||
"open-sse/utils/error.ts": {
|
||
mutants: [
|
||
{ id: "m1", status: "Killed", killedBy: ["A.test.ts"] },
|
||
{ id: "m2", status: "Killed", killedBy: ["A.test.ts", "B.test.ts"] },
|
||
{ id: "m3", status: "Survived", killedBy: [] },
|
||
],
|
||
},
|
||
},
|
||
};
|
||
const allTestFiles = ["A.test.ts", "B.test.ts", "C.test.ts"];
|
||
const r = classifyTestFiles(report, allTestFiles);
|
||
assert.equal(r["A.test.ts"].class, "unique"); // mata m1 sozinho
|
||
assert.equal(r["A.test.ts"].uniqueKills, 1);
|
||
assert.equal(r["A.test.ts"].sharedKills, 1);
|
||
assert.equal(r["B.test.ts"].class, "redundant"); // só m2 (compartilhado)
|
||
assert.equal(r["B.test.ts"].uniqueKills, 0);
|
||
assert.equal(r["C.test.ts"].class, "empty"); // não mata nada
|
||
});
|
||
|
||
// ── id resolution: real Stryker tap-runner reports use numeric test ids in
|
||
// killedBy and a testFiles{} section mapping id -> file name. ────────────────
|
||
test("resolves numeric killedBy ids to file names via the testFiles section", () => {
|
||
const report = {
|
||
testFiles: {
|
||
"tests/unit/x.test.ts": { tests: [{ id: "0", name: "tests/unit/x.test.ts" }] },
|
||
"tests/unit/y.test.ts": { tests: [{ id: "1", name: "tests/unit/y.test.ts" }] },
|
||
"tests/unit/z.test.ts": { tests: [{ id: "2", name: "tests/unit/z.test.ts" }] },
|
||
},
|
||
files: {
|
||
"src/m.ts": {
|
||
mutants: [
|
||
{ id: "m1", status: "Killed", killedBy: ["0"] }, // x alone -> unique
|
||
{ id: "m2", status: "Killed", killedBy: ["0", "1"] }, // x + y shared
|
||
],
|
||
},
|
||
},
|
||
};
|
||
// allTestFiles defaults to the testFiles keys when omitted.
|
||
const r = classifyTestFiles(report);
|
||
assert.equal(r["tests/unit/x.test.ts"].class, "unique");
|
||
assert.equal(r["tests/unit/y.test.ts"].class, "redundant");
|
||
assert.equal(r["tests/unit/z.test.ts"].class, "empty");
|
||
});
|
||
|
||
// ── overlapping (🟡): kills ≥1 unique but the majority of its kills are shared.
|
||
test("classifies overlapping when shared kills outnumber unique kills", () => {
|
||
const report = {
|
||
files: {
|
||
"src/m.ts": {
|
||
mutants: [
|
||
{ id: "m1", status: "Killed", killedBy: ["D"] }, // D unique
|
||
{ id: "m2", status: "Killed", killedBy: ["D", "E"] }, // D shared
|
||
{ id: "m3", status: "Killed", killedBy: ["D", "E", "F"] }, // D shared
|
||
],
|
||
},
|
||
},
|
||
};
|
||
const r = classifyTestFiles(report, ["D", "E", "F"]);
|
||
assert.equal(r["D"].uniqueKills, 1);
|
||
assert.equal(r["D"].sharedKills, 2);
|
||
assert.equal(r["D"].class, "overlapping"); // 2 shared > 1 unique
|
||
assert.equal(r["E"].class, "redundant");
|
||
assert.equal(r["F"].class, "redundant");
|
||
});
|
||
|
||
// ── classifyFromCounts: the pure threshold helper. ───────────────────────────
|
||
test("classifyFromCounts applies the threshold rules", () => {
|
||
assert.equal(classifyFromCounts(0, 0), "empty");
|
||
assert.equal(classifyFromCounts(0, 3), "redundant");
|
||
assert.equal(classifyFromCounts(2, 1), "unique"); // shared not > unique
|
||
assert.equal(classifyFromCounts(1, 1), "unique"); // tie -> unique
|
||
assert.equal(classifyFromCounts(1, 5), "overlapping"); // shared > unique
|
||
});
|
||
|
||
// ── redundancyCandidates (R1): the prune-candidate list = 🔴 empty ∪ 🟠 redundant.
|
||
// A file is a candidate iff it has ZERO unique kills (kills nothing, OR every mutant it
|
||
// kills is also killed by another file). Files with ≥1 unique kill (🟢/🟡) are NEVER
|
||
// candidates. Under a disableBail run killedBy is COMPLETE, so this list is accurate. ──
|
||
test("redundancyCandidates returns empty ∪ redundant files; never a file with a unique kill", () => {
|
||
const report = {
|
||
testFiles: {
|
||
"tests/unit/x.test.ts": { tests: [{ id: "0", name: "tests/unit/x.test.ts" }] },
|
||
"tests/unit/y.test.ts": { tests: [{ id: "1", name: "tests/unit/y.test.ts" }] },
|
||
"tests/unit/z.test.ts": { tests: [{ id: "2", name: "tests/unit/z.test.ts" }] },
|
||
},
|
||
files: {
|
||
"src/m.ts": {
|
||
mutants: [
|
||
{ id: "m1", status: "Killed", killedBy: ["0"] }, // x alone -> unique (KEEP)
|
||
{ id: "m2", status: "Killed", killedBy: ["0", "1"] }, // x+y; y only ever shared
|
||
],
|
||
},
|
||
},
|
||
};
|
||
const r = redundancyCandidates([report]);
|
||
assert.deepEqual(r.empty, ["tests/unit/z.test.ts"]); // kills nothing
|
||
assert.deepEqual(r.redundant, ["tests/unit/y.test.ts"]); // kills only shared mutants
|
||
assert.deepEqual(r.candidates, ["tests/unit/y.test.ts", "tests/unit/z.test.ts"]);
|
||
assert.ok(!r.candidates.includes("tests/unit/x.test.ts")); // has a unique kill -> safe
|
||
});
|
||
|
||
// ── aggregateRadiography: merge per-batch reports at the FILE level (ids are
|
||
// per-run, so each report is classified independently then summed). A file can
|
||
// be empty in one batch but unique in another -> unique overall. ─────────────
|
||
test("aggregateRadiography sums per-file kills across batches and reclassifies", () => {
|
||
const batchC = {
|
||
files: {
|
||
"src/routeGuard.ts": {
|
||
mutants: [{ id: "c1", status: "Killed", killedBy: ["A"] }], // A unique here
|
||
},
|
||
},
|
||
};
|
||
const batchG = {
|
||
files: {
|
||
"src/chatCore/x.ts": {
|
||
mutants: [
|
||
{ id: "g1", status: "Killed", killedBy: ["A", "B"] }, // A shared, B shared
|
||
],
|
||
},
|
||
},
|
||
};
|
||
// B only ever shares; A is unique in C and shared in G -> A unique overall.
|
||
const agg = aggregateRadiography([batchC, batchG], ["A", "B", "C"]);
|
||
assert.equal(agg["A"].uniqueKills, 1);
|
||
assert.equal(agg["A"].sharedKills, 1);
|
||
assert.equal(agg["A"].class, "unique");
|
||
assert.equal(agg["B"].class, "redundant");
|
||
assert.equal(agg["C"].class, "empty");
|
||
});
|