@shivaedev/vitest-rerun 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +92 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -0
- package/dist/error.d.ts +11 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +7 -0
- package/dist/error.js.map +1 -0
- package/dist/flakyTests.d.ts +30 -0
- package/dist/flakyTests.d.ts.map +1 -0
- package/dist/flakyTests.js +24 -0
- package/dist/flakyTests.js.map +1 -0
- package/dist/internal/TimedJsonReporter.d.ts +5 -0
- package/dist/internal/TimedJsonReporter.d.ts.map +1 -0
- package/dist/internal/TimedJsonReporter.js +17 -0
- package/dist/internal/TimedJsonReporter.js.map +1 -0
- package/dist/internal/attempt.d.ts +2 -0
- package/dist/internal/attempt.d.ts.map +1 -0
- package/dist/internal/attempt.js +10 -0
- package/dist/internal/attempt.js.map +1 -0
- package/dist/internal/junit.d.ts +8 -0
- package/dist/internal/junit.d.ts.map +1 -0
- package/dist/internal/junit.js +22 -0
- package/dist/internal/junit.js.map +1 -0
- package/dist/internal/main.d.ts +3 -0
- package/dist/internal/main.d.ts.map +1 -0
- package/dist/internal/main.js +7 -0
- package/dist/internal/main.js.map +1 -0
- package/dist/internal/mergeAttempt.d.ts +9 -0
- package/dist/internal/mergeAttempt.d.ts.map +1 -0
- package/dist/internal/mergeAttempt.js +87 -0
- package/dist/internal/mergeAttempt.js.map +1 -0
- package/dist/internal/reportPaths.d.ts +13 -0
- package/dist/internal/reportPaths.d.ts.map +1 -0
- package/dist/internal/reportPaths.js +18 -0
- package/dist/internal/reportPaths.js.map +1 -0
- package/dist/internal/runAttempt.d.ts +8 -0
- package/dist/internal/runAttempt.d.ts.map +1 -0
- package/dist/internal/runAttempt.js +102 -0
- package/dist/internal/runAttempt.js.map +1 -0
- package/dist/internal/snapshot.d.ts +20 -0
- package/dist/internal/snapshot.d.ts.map +1 -0
- package/dist/internal/snapshot.js +22 -0
- package/dist/internal/snapshot.js.map +1 -0
- package/dist/options.d.ts +12 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +40 -0
- package/dist/options.js.map +1 -0
- package/dist/rerun.d.ts +5 -0
- package/dist/rerun.d.ts.map +1 -0
- package/dist/rerun.js +51 -0
- package/dist/rerun.js.map +1 -0
- package/package.json +69 -2
- package/src/cli.ts +7 -0
- package/src/error.ts +10 -0
- package/src/flakyTests.ts +31 -0
- package/src/internal/TimedJsonReporter.ts +17 -0
- package/src/internal/attempt.ts +15 -0
- package/src/internal/junit.ts +25 -0
- package/src/internal/main.ts +10 -0
- package/src/internal/mergeAttempt.ts +105 -0
- package/src/internal/reportPaths.ts +20 -0
- package/src/internal/runAttempt.ts +126 -0
- package/src/internal/snapshot.ts +43 -0
- package/src/options.ts +47 -0
- package/src/rerun.ts +64 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Config, Console, Effect, FileSystem, Schema } from "effect";
|
|
2
|
+
import * as VitestNode from "vitest/node";
|
|
3
|
+
import { failWith } from "#error.ts";
|
|
4
|
+
import { writeFlakyTests } from "#flakyTests.ts";
|
|
5
|
+
import { RerunJunitReporter } from "#internal/junit.ts";
|
|
6
|
+
import { mergeAttempt } from "#internal/mergeAttempt.ts";
|
|
7
|
+
import { attemptPaths, reportPaths } from "#internal/reportPaths.ts";
|
|
8
|
+
import { failedFiles, readSnapshot, saveSnapshot, tests } from "#internal/snapshot.ts";
|
|
9
|
+
import { TimedJsonReporter } from "#internal/TimedJsonReporter.ts";
|
|
10
|
+
const CoverageReport = Schema.Struct({ coverageMap: Schema.optional(Schema.Unknown) });
|
|
11
|
+
const decodeCoverage = Schema.decodeUnknownEffect(Schema.fromJsonString(CoverageReport));
|
|
12
|
+
function promise(message, run) {
|
|
13
|
+
return Effect.tryPromise({ catch: failWith(message), try: run });
|
|
14
|
+
}
|
|
15
|
+
function start(options, reports, cacheDir) {
|
|
16
|
+
return Effect.acquireRelease(promise("Vitest could not start", () => VitestNode.createVitest({
|
|
17
|
+
...options,
|
|
18
|
+
reporters: ["default", new TimedJsonReporter({ outputFile: reports.json }), new RerunJunitReporter(reports.junit)],
|
|
19
|
+
retry: 0,
|
|
20
|
+
watch: false,
|
|
21
|
+
}, cacheDir === undefined ? {} : { cacheDir })), (vitest) => Effect.promise(() => vitest.close()));
|
|
22
|
+
}
|
|
23
|
+
const failedSpecifications = Effect.fn("VitestRerun.failedSpecifications")(function* (vitest, previous, json) {
|
|
24
|
+
const failed = new Set(yield* failedFiles(json));
|
|
25
|
+
yield* promise("Vitest could not prepare the rerun", () => vitest.standalone());
|
|
26
|
+
const specifications = yield* promise("Vitest could not list test files", () => vitest.globTestSpecifications());
|
|
27
|
+
return specifications.flatMap((spec) => {
|
|
28
|
+
const file = previous.files.find((item) => item.id === spec.taskId && failed.has(spec.moduleId) && item.result?.state === "fail");
|
|
29
|
+
if (file === undefined) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const runnable = tests(file.tasks).filter((test) => !["pass", "skip", "todo"].includes(test.result?.state ?? test.mode));
|
|
33
|
+
return [spec.project.createSpecification(spec.moduleId, file.tasks.length === 0 ? {} : { testIds: runnable.map((test) => test.id) }, spec.pool)];
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
const mergeCoverage = Effect.fn("VitestRerun.mergeCoverage")(function* (vitest, outputDir, attempt) {
|
|
37
|
+
const fs = yield* FileSystem.FileSystem;
|
|
38
|
+
const maps = [];
|
|
39
|
+
for (let earlier = 1; earlier <= attempt; earlier += 1) {
|
|
40
|
+
const { coverageMap } = yield* decodeCoverage(yield* fs.readFileString(attemptPaths(outputDir, earlier).json));
|
|
41
|
+
if (coverageMap !== undefined) {
|
|
42
|
+
maps.push(coverageMap);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const provider = yield* promise("Vitest could not load its coverage provider", () => vitest.createCoverageProvider());
|
|
46
|
+
yield* promise("Vitest could not merge coverage", async () => provider?.mergeReports?.(maps));
|
|
47
|
+
});
|
|
48
|
+
const summaryLine = Effect.fn("VitestRerun.summaryLine")(function* (line) {
|
|
49
|
+
const fs = yield* FileSystem.FileSystem;
|
|
50
|
+
const summary = yield* Config.String("GITHUB_STEP_SUMMARY").pipe(Config.withDefault(""));
|
|
51
|
+
yield* Console.log(line);
|
|
52
|
+
if (summary !== "") {
|
|
53
|
+
yield* fs.writeFileString(summary, `${line}\n`, { flag: "a" });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const rerunFailed = Effect.fn("VitestRerun.rerunFailed")(function* (vitest, { attempt, outputDir }) {
|
|
57
|
+
const previous = yield* readSnapshot(attemptPaths(outputDir, attempt - 1).snapshot);
|
|
58
|
+
const final = reportPaths(outputDir);
|
|
59
|
+
const specifications = yield* failedSpecifications(vitest, previous, final.json);
|
|
60
|
+
if (specifications.length === 0) {
|
|
61
|
+
return yield* Effect.fail(failWith("No failed specifications found for the fresh rerun")(undefined));
|
|
62
|
+
}
|
|
63
|
+
yield* promise("Vitest could not rerun the failed tests", () => vitest.runTestSpecifications(specifications));
|
|
64
|
+
const lines = [];
|
|
65
|
+
const merged = mergeAttempt(previous, vitest.state.getFiles(), vitest.config.root, (line) => lines.push(line));
|
|
66
|
+
for (const file of merged.files) {
|
|
67
|
+
vitest.state.collectFiles(vitest.getProjectByName(file.projectName ?? ""), [file]);
|
|
68
|
+
}
|
|
69
|
+
yield* Effect.forEach(lines, summaryLine);
|
|
70
|
+
for (const reporter of [new TimedJsonReporter({ outputFile: final.json }), new RerunJunitReporter(final.junit)]) {
|
|
71
|
+
yield* promise("Vitest could not write the merged reports", async () => {
|
|
72
|
+
await reporter.onInit(vitest);
|
|
73
|
+
await reporter.onTestRunEnd(vitest.state.getTestModules());
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (merged.flaky.length > 0) {
|
|
77
|
+
yield* writeFlakyTests(final.flaky, merged.flaky);
|
|
78
|
+
}
|
|
79
|
+
if (vitest.config.coverage.enabled) {
|
|
80
|
+
yield* mergeCoverage(vitest, outputDir, attempt);
|
|
81
|
+
}
|
|
82
|
+
const snapshot = {
|
|
83
|
+
errors: vitest.state.getUnhandledErrors(),
|
|
84
|
+
failures: merged.failures,
|
|
85
|
+
files: vitest.state.getFiles(),
|
|
86
|
+
flaky: merged.flaky,
|
|
87
|
+
};
|
|
88
|
+
return snapshot;
|
|
89
|
+
});
|
|
90
|
+
export const runAttempt = Effect.fn("VitestRerun.runAttempt")(function* (input) {
|
|
91
|
+
const { filter, options } = VitestNode.parseCLI(["vitest", "run", ...input.vitestArgs]);
|
|
92
|
+
const rerun = input.attempt > 1;
|
|
93
|
+
const { shard: _shard, ...unsharded } = options;
|
|
94
|
+
const reports = rerun ? attemptPaths(input.outputDir, input.attempt) : reportPaths(input.outputDir);
|
|
95
|
+
const vitest = yield* start(rerun ? unsharded : options, reports, rerun ? reportPaths(input.outputDir).rerunCache : undefined);
|
|
96
|
+
const snapshot = rerun
|
|
97
|
+
? yield* rerunFailed(vitest, input)
|
|
98
|
+
: yield* promise("Vitest could not run the tests", () => vitest.start(filter)).pipe(Effect.map(() => ({ errors: vitest.state.getUnhandledErrors(), failures: {}, files: vitest.state.getFiles(), flaky: [] })));
|
|
99
|
+
yield* saveSnapshot(attemptPaths(input.outputDir, input.attempt).snapshot, snapshot);
|
|
100
|
+
return snapshot.files.some((file) => file.result?.state === "fail") || snapshot.errors.length > 0;
|
|
101
|
+
}, Effect.scoped);
|
|
102
|
+
//# sourceMappingURL=runAttempt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runAttempt.js","sourceRoot":"","sources":["../../src/internal/runAttempt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Effect, FileSystem, Schema } from "effect";
|
|
2
|
+
import type { Vitest } from "vitest/node";
|
|
3
|
+
import type { FlakyTest } from "#flakyTests.ts";
|
|
4
|
+
export type Files = ReturnType<Vitest["state"]["getFiles"]>;
|
|
5
|
+
export type Task = Files[number]["tasks"][number];
|
|
6
|
+
export interface FailureHistory {
|
|
7
|
+
readonly attempts: number;
|
|
8
|
+
readonly messages: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export interface Snapshot {
|
|
11
|
+
readonly errors: readonly unknown[];
|
|
12
|
+
readonly failures: Readonly<Record<string, FailureHistory>>;
|
|
13
|
+
readonly files: Files;
|
|
14
|
+
readonly flaky: readonly FlakyTest[];
|
|
15
|
+
}
|
|
16
|
+
export declare const failedFiles: (path: string) => Effect.Effect<string[], import("effect/PlatformError").PlatformError | Schema.SchemaError, FileSystem.FileSystem>;
|
|
17
|
+
export declare function tests(tasks: readonly Task[]): Task[];
|
|
18
|
+
export declare const saveSnapshot: (path: string, snapshot: Snapshot) => Effect.Effect<void, import("effect/PlatformError").PlatformError, FileSystem.FileSystem>;
|
|
19
|
+
export declare const readSnapshot: (path: string) => Effect.Effect<Snapshot, import("effect/PlatformError").PlatformError, FileSystem.FileSystem>;
|
|
20
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/internal/snapshot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;AAC5D,MAAM,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;AAElD,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;CACrC;AAKD,eAAO,MAAM,WAAW,qIAItB,CAAC;AAEH,wBAAgB,KAAK,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG,IAAI,EAAE,CAEpD;AAED,eAAO,MAAM,YAAY,gIAGvB,CAAC;AAEH,eAAO,MAAM,YAAY,gHAIvB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { deserialize, serialize } from "node:v8";
|
|
2
|
+
import { Effect, FileSystem, Schema } from "effect";
|
|
3
|
+
const Report = Schema.Struct({ testResults: Schema.Array(Schema.Struct({ name: Schema.String, status: Schema.Literals(["failed", "passed"]) })) });
|
|
4
|
+
const decodeReport = Schema.decodeUnknownEffect(Schema.fromJsonString(Report));
|
|
5
|
+
export const failedFiles = Effect.fn("VitestRerun.failedFiles")(function* (path) {
|
|
6
|
+
const fs = yield* FileSystem.FileSystem;
|
|
7
|
+
const report = yield* decodeReport(yield* fs.readFileString(path));
|
|
8
|
+
return [...new Set(report.testResults.filter((file) => file.status === "failed").map((file) => file.name))];
|
|
9
|
+
});
|
|
10
|
+
export function tests(tasks) {
|
|
11
|
+
return tasks.flatMap((task) => (task.type === "suite" ? tests(task.tasks) : [task]));
|
|
12
|
+
}
|
|
13
|
+
export const saveSnapshot = Effect.fn("VitestRerun.saveSnapshot")(function* (path, snapshot) {
|
|
14
|
+
const fs = yield* FileSystem.FileSystem;
|
|
15
|
+
yield* fs.writeFile(path, serialize(snapshot));
|
|
16
|
+
});
|
|
17
|
+
export const readSnapshot = Effect.fn("VitestRerun.readSnapshot")(function* (path) {
|
|
18
|
+
const fs = yield* FileSystem.FileSystem;
|
|
19
|
+
const snapshot = deserialize(yield* fs.readFile(path));
|
|
20
|
+
return snapshot;
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/internal/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAmBpD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AACnJ,MAAM,YAAY,GAAG,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;AAE/E,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,EAAE,CAAC,yBAAyB,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAY;IACtF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC;IACnE,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC7G,CAAC,CAAC,CAAC;AAEH,MAAM,UAAU,KAAK,CAAC,KAAsB;IAC3C,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACtF,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAY,EAAE,QAAkB;IAC5G,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,KAAK,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;AAChD,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAY;IACxF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,MAAM,QAAQ,GAAa,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IACjE,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { VitestRerunError } from "#error.ts";
|
|
3
|
+
export interface RerunOptions {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly outputDir: string;
|
|
6
|
+
readonly reruns: number;
|
|
7
|
+
readonly vitestArgs: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_OUTPUT_DIR = "test-results";
|
|
10
|
+
export declare const DEFAULT_RERUNS = 1;
|
|
11
|
+
export declare function parseOptions(args: readonly string[], cwd: string): Effect.Effect<RerunOptions, VitestRerunError>;
|
|
12
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,cAAc,IAAI,CAAC;AAShC,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,gBAAgB,CAAC,CAyBhH"}
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { VitestRerunError } from "#error.ts";
|
|
4
|
+
export const DEFAULT_OUTPUT_DIR = "test-results";
|
|
5
|
+
export const DEFAULT_RERUNS = 1;
|
|
6
|
+
function option(arg, name, next) {
|
|
7
|
+
if (arg === name && next !== undefined) {
|
|
8
|
+
return { consumed: 2, value: next };
|
|
9
|
+
}
|
|
10
|
+
return arg.startsWith(`${name}=`) ? { consumed: 1, value: arg.slice(name.length + 1) } : undefined;
|
|
11
|
+
}
|
|
12
|
+
export function parseOptions(args, cwd) {
|
|
13
|
+
let outputDir = DEFAULT_OUTPUT_DIR;
|
|
14
|
+
let reruns = String(DEFAULT_RERUNS);
|
|
15
|
+
const vitestArgs = [];
|
|
16
|
+
for (let index = 0; index < args.length;) {
|
|
17
|
+
const arg = args[index] ?? "";
|
|
18
|
+
if (arg === "--") {
|
|
19
|
+
vitestArgs.push(...args.slice(index + 1));
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
const output = option(arg, "--output-dir", args[index + 1]);
|
|
23
|
+
const count = option(arg, "--reruns", args[index + 1]);
|
|
24
|
+
if (output !== undefined) {
|
|
25
|
+
outputDir = output.value;
|
|
26
|
+
}
|
|
27
|
+
else if (count === undefined) {
|
|
28
|
+
vitestArgs.push(arg);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
reruns = count.value;
|
|
32
|
+
}
|
|
33
|
+
index += output?.consumed ?? count?.consumed ?? 1;
|
|
34
|
+
}
|
|
35
|
+
if (!/^\d+$/u.test(reruns)) {
|
|
36
|
+
return Effect.fail(new VitestRerunError({ message: `--reruns expects a whole number, got ${JSON.stringify(reruns)}` }));
|
|
37
|
+
}
|
|
38
|
+
return Effect.succeed({ cwd, outputDir: resolve(cwd, outputDir), reruns: Number(reruns), vitestArgs });
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAS7C,MAAM,CAAC,MAAM,kBAAkB,GAAG,cAAc,CAAC;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC,SAAS,MAAM,CAAC,GAAW,EAAE,IAAY,EAAE,IAAwB;IAClE,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACxC,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACrC,CAAC;IACD,OAAO,GAAG,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACpG,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAuB,EAAE,GAAW;IAChE,IAAI,SAAS,GAAG,kBAAkB,CAAC;IACnC,IAAI,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC;IACpC,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,GAAI,CAAC;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC9B,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YAClB,UAAU,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1C,MAAM;QACP,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;QAC5D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;QACvD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC1B,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC;QAC1B,CAAC;aAAM,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAChC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;aAAM,CAAC;YACP,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC;QACtB,CAAC;QACD,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,KAAK,EAAE,QAAQ,IAAI,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5B,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,gBAAgB,CAAC,EAAE,OAAO,EAAE,wCAAwC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACzH,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;AACxG,CAAC"}
|
package/dist/rerun.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Effect, FileSystem } from "effect";
|
|
2
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
3
|
+
import type { RerunOptions } from "#options.ts";
|
|
4
|
+
export declare const rerun: (options: RerunOptions) => Effect.Effect<number, import("effect/PlatformError").PlatformError | import("effect/Schema").SchemaError, ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem>;
|
|
5
|
+
//# sourceMappingURL=rerun.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rerun.d.ts","sourceRoot":"","sources":["../src/rerun.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,MAAM,EAAE,UAAU,EAAU,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAgB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAGnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA2ChD,eAAO,MAAM,KAAK,uMAahB,CAAC"}
|
package/dist/rerun.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { extname, join } from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Console, Effect, FileSystem, Option } from "effect";
|
|
5
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
6
|
+
import { ATTEMPT_REPORT, attemptPaths, reportPaths } from "#internal/reportPaths.ts";
|
|
7
|
+
import { failedFiles, readSnapshot } from "#internal/snapshot.ts";
|
|
8
|
+
const ATTEMPT_MODULE = fileURLToPath(new URL(`./internal/attempt${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
|
|
9
|
+
const removeReports = Effect.fn("VitestRerun.removeReports")(function* (outputDir) {
|
|
10
|
+
const fs = yield* FileSystem.FileSystem;
|
|
11
|
+
yield* fs.makeDirectory(outputDir, { recursive: true });
|
|
12
|
+
const { flaky, json, junit } = reportPaths(outputDir);
|
|
13
|
+
const attempts = (yield* fs.readDirectory(outputDir)).filter((name) => ATTEMPT_REPORT.test(name));
|
|
14
|
+
yield* Effect.forEach([flaky, json, junit, ...attempts.map((name) => join(outputDir, name))], (path) => fs.remove(path, { force: true }));
|
|
15
|
+
});
|
|
16
|
+
const spawnAttempt = Effect.fn("VitestRerun.spawnAttempt")(function* (options, attempt) {
|
|
17
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
18
|
+
const command = ChildProcess.make(process.execPath, [...process.execArgv, ATTEMPT_MODULE, String(attempt), options.outputDir, ...options.vitestArgs], { cwd: options.cwd, extendEnv: true, stderr: "inherit", stdin: "inherit", stdout: "inherit" });
|
|
19
|
+
return yield* spawner.exitCode(command).pipe(Effect.map((code) => Option.some(Number(code))), Effect.catch((error) => Effect.as(Console.error(`vitest-rerun: attempt ${attempt} ended without an exit code: ${error.message}`), Option.none())));
|
|
20
|
+
});
|
|
21
|
+
const rerunnable = Effect.fn("VitestRerun.rerunnable")(function* (outputDir, attempt) {
|
|
22
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23
|
+
const { snapshot } = attemptPaths(outputDir, attempt);
|
|
24
|
+
const { json } = reportPaths(outputDir);
|
|
25
|
+
if (!((yield* fs.exists(snapshot)) && (yield* fs.exists(json)))) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return (yield* readSnapshot(snapshot)).errors.length === 0 && (yield* failedFiles(json)).length > 0;
|
|
29
|
+
});
|
|
30
|
+
const keepFirstAttempt = Effect.fn("VitestRerun.keepFirstAttempt")(function* (outputDir) {
|
|
31
|
+
const fs = yield* FileSystem.FileSystem;
|
|
32
|
+
const final = reportPaths(outputDir);
|
|
33
|
+
const first = attemptPaths(outputDir, 1);
|
|
34
|
+
yield* fs.copyFile(final.json, first.json);
|
|
35
|
+
yield* fs.copyFile(final.junit, first.junit);
|
|
36
|
+
});
|
|
37
|
+
export const rerun = Effect.fn("VitestRerun.rerun")(function* (options) {
|
|
38
|
+
yield* removeReports(options.outputDir);
|
|
39
|
+
let code = yield* spawnAttempt(options, 1);
|
|
40
|
+
for (let attempt = 1; attempt <= options.reruns; attempt += 1) {
|
|
41
|
+
if (!(Option.exists(code, (status) => status !== 0) && (yield* rerunnable(options.outputDir, attempt)))) {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
if (attempt === 1) {
|
|
45
|
+
yield* keepFirstAttempt(options.outputDir);
|
|
46
|
+
}
|
|
47
|
+
code = yield* spawnAttempt(options, attempt + 1);
|
|
48
|
+
}
|
|
49
|
+
return Option.getOrElse(code, () => 1);
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=rerun.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rerun.js","sourceRoot":"","sources":["../src/rerun.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/vitest-rerun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Reruns failed Vitest tests in a fresh process and reports which tests were flaky",
|
|
5
|
-
"
|
|
5
|
+
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/vitest-rerun#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/ShivaeDev/platform/issues"
|
|
8
|
+
},
|
|
6
9
|
"repository": {
|
|
7
10
|
"type": "git",
|
|
8
11
|
"url": "git+https://github.com/ShivaeDev/platform.git",
|
|
9
12
|
"directory": "packages/vitest-rerun"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"type": "module",
|
|
17
|
+
"imports": {
|
|
18
|
+
"#test/*": "./src/test-support/*",
|
|
19
|
+
"#*.ts": {
|
|
20
|
+
"source": "./src/*.ts",
|
|
21
|
+
"types": "./dist/*.d.ts",
|
|
22
|
+
"default": "./dist/*.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
"./*.ts": {
|
|
27
|
+
"source": "./src/*.ts",
|
|
28
|
+
"types": "./dist/*.d.ts",
|
|
29
|
+
"default": "./dist/*.js"
|
|
30
|
+
},
|
|
31
|
+
"./internal/*": null,
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"bin": {
|
|
35
|
+
"vitest-rerun": "./dist/cli.js"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"src",
|
|
40
|
+
"!src/**/*.test.ts",
|
|
41
|
+
"!src/**/*.spec.ts",
|
|
42
|
+
"!src/**/test-support",
|
|
43
|
+
"CHANGELOG.md",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@effect/platform-node": "4.0.4",
|
|
48
|
+
"@effect/platform-node-shared": "4.0.4",
|
|
49
|
+
"@effect/vitest": "4.0.4",
|
|
50
|
+
"@shivaedev/effect-test": "0.7.1",
|
|
51
|
+
"@shivaedev/quality": "0.33.2",
|
|
52
|
+
"@shivaedev/test-story": "0.18.1",
|
|
53
|
+
"@types/node": "24.19.2",
|
|
54
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
55
|
+
"effect": "4.0.4",
|
|
56
|
+
"typescript": "7.0.2",
|
|
57
|
+
"vitest": "5.0.5"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@effect/platform-node": "4.0.4",
|
|
61
|
+
"@effect/platform-node-shared": "4.0.4",
|
|
62
|
+
"effect": "4.0.4",
|
|
63
|
+
"vitest": "5.0.5"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=24"
|
|
67
|
+
},
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"access": "public",
|
|
70
|
+
"provenance": true
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
|
|
74
|
+
"ready": "pnpm typecheck && pnpm test && pnpm build",
|
|
75
|
+
"test": "vitest run",
|
|
76
|
+
"typecheck": "tsc --noEmit"
|
|
10
77
|
}
|
|
11
78
|
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { NodeServices } from "@effect/platform-node";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import { main } from "#internal/main.ts";
|
|
6
|
+
|
|
7
|
+
process.exitCode = await Effect.runPromise(main(process.argv.slice(2), process.cwd()).pipe(Effect.provide(NodeServices.layer)));
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
|
|
3
|
+
export class VitestRerunError extends Data.TaggedError("VitestRerunError")<{
|
|
4
|
+
readonly message: string;
|
|
5
|
+
readonly cause?: unknown;
|
|
6
|
+
}> {}
|
|
7
|
+
|
|
8
|
+
export function failWith(message: string) {
|
|
9
|
+
return (cause: unknown): VitestRerunError => new VitestRerunError({ cause, message });
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Effect, FileSystem, Schema } from "effect";
|
|
2
|
+
import type { Task } from "#internal/snapshot.ts";
|
|
3
|
+
|
|
4
|
+
export const FlakyTest = Schema.Struct({
|
|
5
|
+
attempts: Schema.Number,
|
|
6
|
+
classname: Schema.String,
|
|
7
|
+
file: Schema.String,
|
|
8
|
+
messages: Schema.Array(Schema.String),
|
|
9
|
+
name: Schema.String,
|
|
10
|
+
});
|
|
11
|
+
export type FlakyTest = typeof FlakyTest.Type;
|
|
12
|
+
|
|
13
|
+
export const FlakyTests = Schema.Array(FlakyTest);
|
|
14
|
+
|
|
15
|
+
export function testcases(tasks: readonly Task[], ancestors = ""): { readonly name: string; readonly task: Task }[] {
|
|
16
|
+
return tasks.flatMap((task) => {
|
|
17
|
+
const name = ancestors === "" ? task.name : `${ancestors} > ${task.name}`;
|
|
18
|
+
return task.type === "suite" ? testcases(task.tasks, name) : [{ name, task }];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function failureMessage(task: Task): string {
|
|
23
|
+
return (task.result?.errors ?? []).map((error) => error.message).join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const encode = Schema.encodeEffect(Schema.fromJsonString(FlakyTests));
|
|
27
|
+
|
|
28
|
+
export const writeFlakyTests = Effect.fn("VitestRerun.writeFlakyTests")(function* (path: string, flaky: readonly FlakyTest[]) {
|
|
29
|
+
const fs = yield* FileSystem.FileSystem;
|
|
30
|
+
yield* fs.writeFileString(path, `${yield* encode(flaky)}\n`);
|
|
31
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { experimental_getRunnerTask, JsonReporter, type TestModule } from "vitest/node";
|
|
3
|
+
|
|
4
|
+
export class TimedJsonReporter extends JsonReporter {
|
|
5
|
+
override async onTestRunEnd(modules: readonly TestModule[]) {
|
|
6
|
+
await super.onTestRunEnd([...modules]);
|
|
7
|
+
const path = this.options.outputFile;
|
|
8
|
+
if (path === undefined) {
|
|
9
|
+
throw new Error("Timed JSON reports require an output file");
|
|
10
|
+
}
|
|
11
|
+
const report = JSON.parse(readFileSync(path, "utf8"));
|
|
12
|
+
for (const [index, module] of modules.entries()) {
|
|
13
|
+
report.testResults[index].durationMs = experimental_getRunnerTask(module).result?.duration ?? 0;
|
|
14
|
+
}
|
|
15
|
+
writeFileSync(path, JSON.stringify(report));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { NodeServices } from "@effect/platform-node";
|
|
3
|
+
import { Console, Effect } from "effect";
|
|
4
|
+
import { runAttempt } from "#internal/runAttempt.ts";
|
|
5
|
+
|
|
6
|
+
const [attempt = "1", outputDir = ".", ...vitestArgs] = process.argv.slice(2);
|
|
7
|
+
const failed = await Effect.runPromise(
|
|
8
|
+
runAttempt({ attempt: Number(attempt), outputDir, vitestArgs }).pipe(
|
|
9
|
+
Effect.catchTag("VitestRerunError", (error) => Effect.as(Console.error(`vitest-rerun: ${error.message}`, error.cause), true)),
|
|
10
|
+
Effect.provide(NodeServices.layer),
|
|
11
|
+
),
|
|
12
|
+
);
|
|
13
|
+
if (failed) {
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import { JUnitReporter, type Vitest } from "vitest/node";
|
|
3
|
+
|
|
4
|
+
export function junitClassname(file: string, project: string): string {
|
|
5
|
+
return project === "" ? file : `|${project}| ${file}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class RerunJunitReporter extends JUnitReporter {
|
|
9
|
+
readonly #root: { path: string };
|
|
10
|
+
|
|
11
|
+
constructor(outputFile: string) {
|
|
12
|
+
const root = { path: "" };
|
|
13
|
+
super({
|
|
14
|
+
addFileAttribute: true,
|
|
15
|
+
classnameTemplate: ({ displayName, filepath }) => junitClassname(relative(root.path, filepath), displayName),
|
|
16
|
+
outputFile,
|
|
17
|
+
});
|
|
18
|
+
this.#root = root;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override onInit(vitest: Vitest) {
|
|
22
|
+
this.#root.path = vitest.config.root;
|
|
23
|
+
return super.onInit(vitest);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Console, Effect } from "effect";
|
|
2
|
+
import { parseOptions } from "#options.ts";
|
|
3
|
+
import { rerun } from "#rerun.ts";
|
|
4
|
+
|
|
5
|
+
export function main(args: readonly string[], cwd: string) {
|
|
6
|
+
return parseOptions(args, cwd).pipe(
|
|
7
|
+
Effect.flatMap(rerun),
|
|
8
|
+
Effect.catchTag("VitestRerunError", (error) => Effect.as(Console.error(`vitest-rerun: ${error.message}`), 2)),
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import { type FlakyTest, failureMessage, testcases } from "#flakyTests.ts";
|
|
3
|
+
import { junitClassname } from "#internal/junit.ts";
|
|
4
|
+
import { type FailureHistory, type Files, type Snapshot, type Task, tests } from "#internal/snapshot.ts";
|
|
5
|
+
|
|
6
|
+
type File = Files[number];
|
|
7
|
+
|
|
8
|
+
interface Outcome {
|
|
9
|
+
readonly failures: Record<string, FailureHistory>;
|
|
10
|
+
readonly flaky: FlakyTest[];
|
|
11
|
+
readonly root: string;
|
|
12
|
+
readonly summary: (line: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function mergeTest(first: Task, task: Task) {
|
|
16
|
+
const unresolved = first.result?.state === "fail" && !["pass", "fail"].includes(task.result?.state ?? "");
|
|
17
|
+
if (first.result !== undefined && (first.result.state === "pass" || unresolved)) {
|
|
18
|
+
task.mode = first.mode;
|
|
19
|
+
task.result = first.result;
|
|
20
|
+
task.logs = first.logs ?? [];
|
|
21
|
+
} else if (task.result !== undefined) {
|
|
22
|
+
task.result.duration = (task.result.duration ?? 0) + (first.result?.duration ?? 0);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function restorePassed(original: Task[], rerun: Task[]) {
|
|
27
|
+
for (const task of rerun) {
|
|
28
|
+
const first = original.find((item) => item.id === task.id);
|
|
29
|
+
if (first === undefined) {
|
|
30
|
+
throw new Error(`Rerun collected an unexpected test: ${task.name}`);
|
|
31
|
+
}
|
|
32
|
+
if (task.type === "suite" && first.type === "suite") {
|
|
33
|
+
restorePassed(first.tasks, task.tasks);
|
|
34
|
+
} else {
|
|
35
|
+
mergeTest(first, task);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function finishFile(file: File, next: File, ranTests: boolean) {
|
|
41
|
+
const duration = (file.result?.duration ?? 0) + (next.result?.duration ?? 0);
|
|
42
|
+
if (!ranTests && file.tasks.length > 0 && file.result !== undefined) {
|
|
43
|
+
next.result = { ...file.result };
|
|
44
|
+
}
|
|
45
|
+
if (next.result !== undefined) {
|
|
46
|
+
if (tests(next.tasks).some((test) => test.result?.state === "fail")) {
|
|
47
|
+
next.result.state = "fail";
|
|
48
|
+
}
|
|
49
|
+
next.result.startTime = file.result?.startTime ?? next.result.startTime ?? 0;
|
|
50
|
+
next.result.duration = duration;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function recordAttempts(file: File, retried: Task[], outcome: Outcome) {
|
|
55
|
+
const path = relative(outcome.root, file.filepath);
|
|
56
|
+
const classname = junitClassname(path, file.projectName ?? "");
|
|
57
|
+
for (const { name, task } of testcases(file.tasks).filter((testcase) => testcase.task.result?.state === "fail")) {
|
|
58
|
+
const again = retried.find((item) => item.id === task.id);
|
|
59
|
+
const prior = outcome.failures[task.id] ?? { attempts: 1, messages: [failureMessage(task)] };
|
|
60
|
+
if (again?.result?.state === "pass") {
|
|
61
|
+
delete outcome.failures[task.id];
|
|
62
|
+
outcome.flaky.push({ attempts: prior.attempts + 1, classname, file: path, messages: prior.messages, name });
|
|
63
|
+
outcome.summary(`- Flaky: ${classname} — ${name}`);
|
|
64
|
+
} else if (again?.result?.state === "fail") {
|
|
65
|
+
outcome.failures[task.id] = { attempts: prior.attempts + 1, messages: [...prior.messages, failureMessage(again)] };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mergeFile(file: File, next: File, outcome: Outcome) {
|
|
71
|
+
const original = tests(file.tasks);
|
|
72
|
+
const retried = tests(next.tasks);
|
|
73
|
+
if (original.some((test) => !retried.some((item) => item.id === test.id))) {
|
|
74
|
+
throw new Error(`Rerun did not collect every original test in ${file.filepath}`);
|
|
75
|
+
}
|
|
76
|
+
const ranTests = retried.some(
|
|
77
|
+
(test) => ["pass", "fail"].includes(test.result?.state ?? "") && original.some((item) => item.id === test.id && item.result?.state !== "pass"),
|
|
78
|
+
);
|
|
79
|
+
recordAttempts(file, retried, outcome);
|
|
80
|
+
if (original.length === 0 && next.result?.state === "pass") {
|
|
81
|
+
outcome.summary(`- Flaky: ${junitClassname(relative(outcome.root, file.filepath), file.projectName ?? "")} — collection failure`);
|
|
82
|
+
} else if (original.length > 0) {
|
|
83
|
+
restorePassed(file.tasks, next.tasks);
|
|
84
|
+
}
|
|
85
|
+
finishFile(file, next, ranTests);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface Merged {
|
|
89
|
+
readonly failures: Readonly<Record<string, FailureHistory>>;
|
|
90
|
+
readonly files: Files;
|
|
91
|
+
readonly flaky: readonly FlakyTest[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function mergeAttempt(previous: Snapshot, rerun: Files, root: string, summary: (line: string) => void): Merged {
|
|
95
|
+
const outcome: Outcome = { failures: { ...previous.failures }, flaky: [...previous.flaky], root, summary };
|
|
96
|
+
const files = previous.files.map((file) => {
|
|
97
|
+
const next = rerun.find((item) => item.id === file.id);
|
|
98
|
+
if (next === undefined) {
|
|
99
|
+
return file;
|
|
100
|
+
}
|
|
101
|
+
mergeFile(file, next, outcome);
|
|
102
|
+
return next;
|
|
103
|
+
});
|
|
104
|
+
return { failures: outcome.failures, files, flaky: outcome.flaky };
|
|
105
|
+
}
|