#!/usr/bin/env -S deno run --allow-all /** * In-house mutation tester — "tests for your tests". * * Copies the checkout into `.mutation-runs//work`, mutates * binary/logical/assignment operators in the copied source file(s), runs the * mapped test file(s), and reports which mutants SURVIVED (were not caught by * any assertion). It is the real version of the heuristic in * `test-quality-audit.ts`: instead of guessing which assertions look weak, it * proves which code changes your tests fail to notice. * * The operator tables and AST walk are derived from Mutasaurus (MIT); the * execution model is our own. The child process still mutates in place inside * the copied checkout so import-map aliases bind to the mutant — see * scripts/mutation/LICENSE.mutasaurus.md. * * Usage: deno task mutation [options] */ import { DEFAULT_DEADLINE, parseArgs } from "./mutation/args.ts"; import { expand } from "./mutation/expand.ts"; import { runIsolatedMutationCommand } from "./mutation/isolation.ts"; import { isSnapshotChild, runSnapshotChild, } from "./mutation/snapshot-child.ts"; const USAGE = `Usage: deno task mutation [options] deno task mutation --source --test [--source …] [--test …] Mutates operators in the source file(s), runs the mapped test file(s), and reports which mutants survived (were NOT caught by your tests). Options: --exhaustive Try every operator replacement, not just one per operator. --harness Build static assets and start stripe-mock first (needed for tests that import the app / Stripe; slower). --jobs Concurrent test-file batches per mutant (default: CPU-aware, or MUTATION_JOBS when set). Static gates use separate CPU-aware workers, capped at 4; set MUTATION_STATIC_JOBS to lower that cap. --deadline Fail the whole run if it is still going after this long (default ${DEFAULT_DEADLINE}). A guard against a mutant that hangs the tests; it never decides a mutant. -h, --help Show this help. Examples: deno task mutation src/shared/dates.ts test/shared/dates.test.ts deno task mutation 'src/shared/forms/definition.ts' 'test/shared/forms/definition/*.test.ts' --exhaustive`; // Ends by returning, never by Deno.exit: as a snapshot child this runs // inside runSnapshotChild, whose clean-up must go ahead after the work. const main = async (): Promise => { const args = parseArgs(Deno.args); if (args.error !== null) { console.error(args.error); return 1; } if (args.help || args.sources.length === 0 || args.tests.length === 0) { console.log(USAGE); return args.help ? 0 : 1; } const sourceFiles = await expand(args.sources); const testFiles = await expand(args.tests); if (sourceFiles.length === 0) { console.error("No source files matched."); return 1; } if (testFiles.length === 0) { console.error("No test files matched."); return 1; } const { runMutationTesting } = await import("./mutation/runner.ts"); return await runMutationTesting({ ...(args.batchJobs === undefined ? {} : { batchJobs: args.batchJobs }), deadline: args.deadline, exhaustive: args.exhaustive, sourceFiles, testFiles, useHarness: args.useHarness, }); }; if (import.meta.main) { if (isSnapshotChild()) { Deno.exit(await runSnapshotChild(main)); } else { Deno.exit(await runIsolatedMutationCommand(Deno.args)); } }