项目文件夹

文件
2026-07-13 13:39:12 +08:00

148 行
6.3 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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");
});