@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +92 -1
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +7 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/error.d.ts +11 -0
  9. package/dist/error.d.ts.map +1 -0
  10. package/dist/error.js +7 -0
  11. package/dist/error.js.map +1 -0
  12. package/dist/flakyTests.d.ts +30 -0
  13. package/dist/flakyTests.d.ts.map +1 -0
  14. package/dist/flakyTests.js +24 -0
  15. package/dist/flakyTests.js.map +1 -0
  16. package/dist/internal/TimedJsonReporter.d.ts +5 -0
  17. package/dist/internal/TimedJsonReporter.d.ts.map +1 -0
  18. package/dist/internal/TimedJsonReporter.js +17 -0
  19. package/dist/internal/TimedJsonReporter.js.map +1 -0
  20. package/dist/internal/attempt.d.ts +2 -0
  21. package/dist/internal/attempt.d.ts.map +1 -0
  22. package/dist/internal/attempt.js +10 -0
  23. package/dist/internal/attempt.js.map +1 -0
  24. package/dist/internal/junit.d.ts +8 -0
  25. package/dist/internal/junit.d.ts.map +1 -0
  26. package/dist/internal/junit.js +22 -0
  27. package/dist/internal/junit.js.map +1 -0
  28. package/dist/internal/main.d.ts +3 -0
  29. package/dist/internal/main.d.ts.map +1 -0
  30. package/dist/internal/main.js +7 -0
  31. package/dist/internal/main.js.map +1 -0
  32. package/dist/internal/mergeAttempt.d.ts +9 -0
  33. package/dist/internal/mergeAttempt.d.ts.map +1 -0
  34. package/dist/internal/mergeAttempt.js +87 -0
  35. package/dist/internal/mergeAttempt.js.map +1 -0
  36. package/dist/internal/reportPaths.d.ts +13 -0
  37. package/dist/internal/reportPaths.d.ts.map +1 -0
  38. package/dist/internal/reportPaths.js +18 -0
  39. package/dist/internal/reportPaths.js.map +1 -0
  40. package/dist/internal/runAttempt.d.ts +8 -0
  41. package/dist/internal/runAttempt.d.ts.map +1 -0
  42. package/dist/internal/runAttempt.js +102 -0
  43. package/dist/internal/runAttempt.js.map +1 -0
  44. package/dist/internal/snapshot.d.ts +20 -0
  45. package/dist/internal/snapshot.d.ts.map +1 -0
  46. package/dist/internal/snapshot.js +22 -0
  47. package/dist/internal/snapshot.js.map +1 -0
  48. package/dist/options.d.ts +12 -0
  49. package/dist/options.d.ts.map +1 -0
  50. package/dist/options.js +40 -0
  51. package/dist/options.js.map +1 -0
  52. package/dist/rerun.d.ts +5 -0
  53. package/dist/rerun.d.ts.map +1 -0
  54. package/dist/rerun.js +51 -0
  55. package/dist/rerun.js.map +1 -0
  56. package/package.json +69 -2
  57. package/src/cli.ts +7 -0
  58. package/src/error.ts +10 -0
  59. package/src/flakyTests.ts +31 -0
  60. package/src/internal/TimedJsonReporter.ts +17 -0
  61. package/src/internal/attempt.ts +15 -0
  62. package/src/internal/junit.ts +25 -0
  63. package/src/internal/main.ts +10 -0
  64. package/src/internal/mergeAttempt.ts +105 -0
  65. package/src/internal/reportPaths.ts +20 -0
  66. package/src/internal/runAttempt.ts +126 -0
  67. package/src/internal/snapshot.ts +43 -0
  68. package/src/options.ts +47 -0
  69. 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"}
@@ -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"}
@@ -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.0.0",
3
+ "version": "0.1.1",
4
4
  "description": "Reruns failed Vitest tests in a fresh process and reports which tests were flaky",
5
- "license": "MIT",
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
+ }