Skip to content
Source
Sign inSign up
Open History to see the last commit

cached-tests.ts

118 lines4.2 KB

1// @effect-diagnostics nodeBuiltinImport:off - Vite loads this before dependency installation and service initialization.
2import * as NodeCrypto from "node:crypto";
3import * as NodeFS from "node:fs";
4import * as NodePath from "node:path";
5​
6interface CachedTestTask {
7 command: string;
8 cwd?: string;
9 dependsOn?: Array<string>;
10 cache:
11 | false
12 | {
13 input: Array<string | { auto: true }>;
14 output: Array<string>;
15 };
17​
18export function matchesTestPath(file: string, filters: Array<string>) {
19 return (
20 filters.length === 0 ||
21 filters.some((filter) => filter === "" || file === filter || file.startsWith(`${filter}/`))
22 );
24​
25export function cachedTestTasks(
26 raw: string | undefined,
27 root = NodePath.resolve(import.meta.dirname, "../.."),
28): Record<string, CachedTestTask> {
29 if (!raw) return {};
30 const request: unknown = JSON.parse(raw);
31 if (
32 typeof request !== "object" ||
33 request === null ||
34 !("files" in request) ||
35 !Array.isArray(request.files) ||
36 !("groupSize" in request) ||
37 typeof request.groupSize !== "number" ||
38 !Number.isSafeInteger(request.groupSize) ||
39 request.groupSize < 1
40 ) {
41 throw new Error("Invalid cached test request");
42 }
43 const uncached = "uncached" in request ? request.uncached : [];
44 if (!Array.isArray(uncached) || !uncached.every((file) => typeof file === "string")) {
45 throw new Error("Invalid uncached test files");
46 }
47 const files: Array<string> = [];
48 for (const file of request.files) {
49 if (
50 typeof file !== "string" ||
51 NodePath.isAbsolute(file) ||
52 file.split(/[\\/]/).includes("..") ||
53 !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(file)
54 ) {
55 throw new Error("Cached tests must be test files inside the checkout");
56 }
57 files.push(file);
58 }
59 const tasks: Record<string, CachedTestTask> = {};
60 const sorted = [...new Set(files)].sort();
61 const packages = new Map<string, Array<string>>();
62 for (const file of sorted) {
63 let directory = NodePath.dirname(NodePath.join(root, file));
64 while (directory !== root && !NodeFS.existsSync(NodePath.join(directory, "package.json"))) {
65 directory = NodePath.dirname(directory);
66 }
67 const entries = packages.get(directory) ?? [];
68 entries.push(file);
69 packages.set(directory, entries);
70 }
71 for (const [directory, entries] of packages) {
72 const manifest: unknown = JSON.parse(
73 NodeFS.readFileSync(NodePath.join(directory, "package.json"), "utf8"),
74 );
75 const command =
76 directory !== root &&
77 typeof manifest === "object" &&
78 manifest !== null &&
79 "scripts" in manifest &&
80 typeof manifest.scripts === "object" &&
81 manifest.scripts !== null &&
82 "test" in manifest.scripts &&
83 typeof manifest.scripts.test === "string"
84 ? manifest.scripts.test
85 : "vp test run";
86 if (!command.startsWith("vp test run")) {
87 throw new Error(`Unsupported test command in ${directory}`);
88 }
89 const cacheable = entries.filter((file) => !uncached.includes(file));
90 const groups: Array<Array<string>> = [];
91 for (let index = 0; index < cacheable.length; index += request.groupSize) {
92 groups.push(cacheable.slice(index, index + request.groupSize));
93 }
94 groups.push(...entries.filter((file) => uncached.includes(file)).map((file) => [file]));
95 for (const group of groups) {
96 const key = NodeCrypto.createHash("sha256").update(group.join("\0")).digest("hex");
97 tasks[`test-cache:${key}`] = {
98 command: `${command} ${group.map((file) => `'${NodePath.relative(directory, NodePath.join(root, file)).replaceAll("'", "'\"'\"'")}'`).join(" ")}`,
99 ...(directory === root ? {} : { cwd: NodePath.relative(root, directory) }),
100 cache: uncached.includes(group[0]!)
101 ? false
102 : {
103 // Vitest reads and rewrites these caches while running otherwise unchanged tests.
104 input: [
105 { auto: true },
106 "!**/node_modules/.vite/**",
107 "!**/node_modules/.vitest-cache/**",
108 ],
109 output: [],
110 },
111 };
112 }
113 }
114 return {
115 ...tasks,
116 "test-cache": { command: 'node -e ""', dependsOn: Object.keys(tasks), cache: false },
117 };