Keep full qualification and standalone mutation execution intact, retain distinct reports, and preserve fail-closed quality gates. Related: #3281 Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
101 lines
3.8 KiB
TypeScript
101 lines
3.8 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join, resolve } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
const root = fileURLToPath(new URL("../../../", import.meta.url));
|
|
const runNode = (source: string, ...arguments_: string[]): string =>
|
|
execFileSync(
|
|
process.execPath,
|
|
["--input-type=module", "-e", source, ...arguments_],
|
|
{ cwd: root, encoding: "utf8" },
|
|
);
|
|
|
|
describe("reverse-tools mutation configuration", () => {
|
|
it("loads nonempty targets and distinct report destinations from both actual Stryker configs", () => {
|
|
const result = runNode(
|
|
`const [mainUrl, semanticsUrl] = process.argv.slice(1);
|
|
const main = await import(mainUrl);
|
|
const semantics = await import(semanticsUrl);
|
|
process.stdout.write(JSON.stringify([main.default, semantics.default].map(config => ({
|
|
targets: config.mutate.length,
|
|
json: config.jsonReporter?.fileName,
|
|
html: config.htmlReporter?.fileName,
|
|
dryRunOnly: config.dryRunOnly ?? false,
|
|
reporters: config.reporters,
|
|
}))));`,
|
|
pathToFileURL(join(root, "stryker.config.mjs")).href,
|
|
pathToFileURL(join(root, "stryker.semantics.config.mjs")).href,
|
|
);
|
|
const configs = z
|
|
.array(
|
|
z.object({
|
|
targets: z.number().int().positive(),
|
|
json: z.string().regex(/^reports\/mutation\/.+\.json$/u),
|
|
html: z.string().regex(/^reports\/mutation\/.+\.html$/u),
|
|
dryRunOnly: z.literal(false),
|
|
reporters: z.array(z.string()),
|
|
}),
|
|
)
|
|
.length(2)
|
|
.parse(JSON.parse(result));
|
|
expect(
|
|
new Set(
|
|
configs.flatMap(({ json, html }) => [
|
|
resolve(root, json),
|
|
resolve(root, html),
|
|
]),
|
|
).size,
|
|
).toBe(4);
|
|
for (const config of configs)
|
|
expect(config.reporters).toEqual(
|
|
expect.arrayContaining(["json", "html"]),
|
|
);
|
|
});
|
|
|
|
it("fails closed when an owned mutation method is missing", async () => {
|
|
const directory = await createTestTempDirectory("rea-mutation-selector-");
|
|
const source = join(directory, "source.ts");
|
|
await writeFile(source, "class Present { run() {} }\n");
|
|
|
|
const result = runNode(
|
|
`import { mutationRanges } from "./scripts/lib/reverse-tools-mutation-targets.mjs";
|
|
const [source] = process.argv.slice(1);
|
|
try {
|
|
mutationRanges(source, [{ kind: "method", owner: "Missing", name: "run" }]);
|
|
throw new Error("missing owner unexpectedly selected");
|
|
} catch (error) {
|
|
if (!(error instanceof Error) || !error.message.includes("found 0")) throw error;
|
|
process.stdout.write(error.message);
|
|
}`,
|
|
source,
|
|
);
|
|
expect(result).toMatch(/found 0/u);
|
|
});
|
|
|
|
it("fails closed when an owned mutation method is ambiguous", async () => {
|
|
const directory = await createTestTempDirectory("rea-mutation-selector-");
|
|
const source = join(directory, "source.ts");
|
|
await writeFile(source, "class Owner { run() {} run(value: string) {} }\n");
|
|
|
|
const result = runNode(
|
|
`import { mutationRanges } from "./scripts/lib/reverse-tools-mutation-targets.mjs";
|
|
const [source] = process.argv.slice(1);
|
|
try {
|
|
mutationRanges(source, [{ kind: "method", owner: "Owner", name: "run" }]);
|
|
throw new Error("ambiguous target unexpectedly selected");
|
|
} catch (error) {
|
|
if (!(error instanceof Error) || !error.message.includes("found 2")) throw error;
|
|
process.stdout.write(error.message);
|
|
}`,
|
|
source,
|
|
);
|
|
expect(result).toMatch(/found 2/u);
|
|
});
|
|
});
|