Files
2026-07-13 13:39:12 +08:00

148 lines
6.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
});