mirror of
https://github.com/Heretek-AI/IUMBTEMS.git
synced 2026-10-11 21:09:53 +00:00
fix(gates): test-aware mutation cache fingerprint (Refs #169)
This commit is contained in:
@@ -35,8 +35,8 @@ import {
|
||||
generateMutants,
|
||||
type MutationCacheState,
|
||||
type MutationOutcome,
|
||||
mutantCacheKey,
|
||||
mutationFindings,
|
||||
mutationFingerprint,
|
||||
} from "./mutation.ts"
|
||||
import { parseOutput } from "./parsers.ts"
|
||||
import { scanFileForSecrets } from "./secrets.ts"
|
||||
@@ -385,7 +385,12 @@ async function runMutationCheck(
|
||||
const absolute = path.join(check.dir, file)
|
||||
const source = await readFile(absolute, "utf8").catch(() => undefined)
|
||||
if (source === undefined) continue
|
||||
const hash = mutantCacheKey(file, source)
|
||||
const hash = await mutationFingerprint(
|
||||
(candidate) => readFile(path.join(check.dir, candidate), "utf8").catch(() => undefined),
|
||||
file,
|
||||
source,
|
||||
check.files.filter((candidate) => isTestFile(candidate)),
|
||||
)
|
||||
const entry = next[file]
|
||||
if (entry?.hash === hash) {
|
||||
const killed = new Set(entry.killed)
|
||||
|
||||
@@ -142,6 +142,27 @@ export function mutantCacheKey(file: string, content: string): string {
|
||||
return sha256(`${file}\n${content}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache fingerprint for one source file: its own content plus the content of
|
||||
* the in-scope test files. Weakening (or strengthening) the tests changes
|
||||
* the fingerprint, so a stale verdict can never green-light edited tests —
|
||||
* the suite reruns.
|
||||
*/
|
||||
export async function mutationFingerprint(
|
||||
readFile: (file: string) => Promise<string | undefined>,
|
||||
file: string,
|
||||
source: string,
|
||||
testFiles: readonly string[],
|
||||
): Promise<string> {
|
||||
const tests: string[] = []
|
||||
for (const candidate of testFiles) {
|
||||
if (candidate === file) continue
|
||||
const content = await readFile(candidate)
|
||||
if (content !== undefined) tests.push(`${candidate}\n${content}`)
|
||||
}
|
||||
return mutantCacheKey(file, `${source}\n${tests.join("\n")}`)
|
||||
}
|
||||
|
||||
export function evaluateMutations(
|
||||
outcomes: readonly MutationOutcome[],
|
||||
threshold = MUTATION_KILL_THRESHOLD,
|
||||
@@ -186,6 +207,8 @@ export interface MutationGateOptions {
|
||||
readonly runTests: (mutant: Mutant) => Promise<{ killed: boolean }>
|
||||
/** Reused across runs: unchanged files skip the suite. */
|
||||
readonly cache?: MutationCacheState
|
||||
/** Test files whose content joins the cache fingerprint (edited tests rerun the suite). */
|
||||
readonly testFiles?: readonly string[]
|
||||
readonly threshold?: number
|
||||
readonly maxPerFile?: number
|
||||
}
|
||||
@@ -203,7 +226,7 @@ export async function runMutationGate(options: MutationGateOptions): Promise<Mut
|
||||
for (const file of options.files) {
|
||||
const source = await options.readFile(file)
|
||||
if (source === undefined) continue
|
||||
const hash = mutantCacheKey(file, source)
|
||||
const hash = await mutationFingerprint(options.readFile, file, source, options.testFiles ?? [])
|
||||
const entry = cache[file]
|
||||
if (entry && entry.hash === hash) {
|
||||
cached.push(file)
|
||||
|
||||
@@ -148,4 +148,29 @@ describe("runMutationGate", () => {
|
||||
expect(mutantCacheKey("src/a.ts", "a + b")).not.toBe(mutantCacheKey("src/a.ts", "a - b"))
|
||||
expect(mutantCacheKey("src/a.ts", "same")).toBe(mutantCacheKey("src/a.ts", "same"))
|
||||
})
|
||||
|
||||
test("changing a test file invalidates the cache: the suite reruns", async () => {
|
||||
let suite = "strong"
|
||||
let runs = 0
|
||||
const options = {
|
||||
files: ["src/sum.ts"],
|
||||
readFile: async (file: string) =>
|
||||
file === "src/sum.ts" ? "export function sum(a: number, b: number): number {\n return a + b\n}\n" : suite,
|
||||
runTests: async () => {
|
||||
runs++
|
||||
return { killed: true }
|
||||
},
|
||||
testFiles: ["test/sum.test.ts"],
|
||||
}
|
||||
const first = await runMutationGate(options)
|
||||
expect(runs).toBeGreaterThan(0)
|
||||
// Same tests: cached, no rerun.
|
||||
await runMutationGate({ ...options, cache: first.cache })
|
||||
expect(runs).toBe(first.cache["src/sum.ts"]!.total)
|
||||
// Weakened tests: the fingerprint changes, so the suite reruns.
|
||||
suite = "weakened"
|
||||
const third = await runMutationGate({ ...options, cache: first.cache })
|
||||
expect(third.cached).not.toContain("src/sum.ts")
|
||||
expect(runs).toBeGreaterThan(first.cache["src/sum.ts"]!.total)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user