vitestChild
uses node:fs
The child side: run the request's tests with Vitest, write their results.
import { inSuiteSlot, machineSlots, slotPool, slotsDir, slotWaits } from "#codebase/tcmts/cli/budget"; import { Codebase } from "#codebase/tcmts/cli/codebase"; import { linkModules, packagesKey, strip, vitestModule } from "#codebase/tcmts/cli/vitest"; import type { Request, VitestLike } from "#codebase/tcmts/cli/vitest"; import { pinnedReacts, pluginsForTests, reactCompiler } from "#codebase/tcmts/cli/web"; import type { TestRun } from "#codebase/tcmts/loader"; import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; export async function vitestChild(requestFile: string): Promise<void> { const request = JSON.parse(fs.readFileSync(requestFile, "utf8")) as Request; const cb = Codebase.find(request.root); const branch = process.env.TCM_BRANCH; // a session's branch (TCM_BRANCH) is this codebase's: the tests' own codebases and tcms aren't on it; and the brief // reports an agent's session asks for (TCM_BRIEF) are its own, not what the code the tests run prints delete process.env.TCM_BRANCH; delete process.env.TCM_BRIEF; // React Compiler, installed before Vitest starts (`reactCompiler`): installing changes the packages it's watching if (request.browserTests.length && pinnedReacts(cb).length) reactCompiler(cb); // Vitest's root is the codebase's test directory, the same each run, so what Vite makes of the packages tests use (its // optimized dependencies, in its node_modules/.vite) is kept from one run to the next; each run's files are its own const root = path.join(cb.lay.tcmDir, "test"); const run = fs.mkdtempSync(path.join(root, "run-")); const own = (file: string) => `${path.basename(run)}/${file}`; linkModules( path.join(cb.lay.tcmDir, "test", "node_modules"), path.join(cb.lay.cacheDir, "node_modules"), { vitest: request.vitest, ...(request.browser ? { "@vitest/browser-playwright": request.browser.provider } : {}), }, ); const vitestImport = path.basename(request.vitest); const projects: Record<string, unknown>[] = []; if (request.node.length) { fs.writeFileSync( path.join(run, "node.test.ts"), vitestModule(run, cb.lay.cacheDir, vitestImport, request.node), ); projects.push({ test: { name: "node", include: [own("node.test.ts")], environment: "node" } }); } if (request.browserTests.length && request.browser) { fs.writeFileSync( path.join(run, "browser.test.ts"), vitestModule(run, cb.lay.cacheDir, vitestImport, request.browserTests), ); const { playwright } = (await import( pathToFileURL(path.join(request.browser.provider, "dist", "index.js")).href )) as { playwright: (o: unknown) => unknown }; const unsaved = new Map(request.unsaved.map((r) => [r.hash, r])); projects.push({ plugins: pluginsForTests( cb, request.browserTests.map((t) => t.hash), (h) => unsaved.get(h) ?? cb.store.getDef(h), (c) => { const stored = cb.store.componentMembers(c); return stored.length ? stored : request.unsaved .filter((r) => r.componentHash === c) .sort((a, b) => a.componentIndex - b.componentIndex); }, ), // (and where it really is: a codebase reached by a symlink is served from its real path) server: { fs: { allow: [cb.lay.tcmDir, fs.realpathSync(cb.lay.tcmDir)] } }, // (one bundle of them for each set of packages the tests reach, which edits to the code rarely change: two runs // at once with different packages would bundle them over each other) cacheDir: path.join(root, "vite", packagesKey(cb, request)), test: { name: "browser", include: [own("browser.test.ts")], // the Chrome that's installed: nothing to download browser: { enabled: true, headless: true, provider: playwright({ launchOptions: { channel: "chrome" } }), instances: [{ browser: "chromium" }], }, }, }); } const { startVitest } = (await import( pathToFileURL(path.join(request.vitest, "dist", "node.js")).href )) as { startVitest: ( filters: string[], options: Record<string, unknown>, vite: Record<string, unknown>, ) => Promise<VitestLike>; }; // a project at a time runs in a slot of the machine's (`slotPool`, shared with the suites every tcm runs), taken // together; inside a suite, in the slot it took, one at a time. (A wait is said where the tcm that started this // writes, fd 3, if it gave one) const slots = inSuiteSlot() ? 1 : Math.min(projects.length || 1, machineSlots()); const release = inSuiteSlot() ? undefined : await slotWaits((l) => { try { fs.writeSync(3, `${l}\n`); } catch {} }).take( slotPool(slotsDir(), machineSlots()), `test definitions of ${request.root}${branch ? ` on ${branch}` : ""}`, slots, ); // (its workers, and what their tests start, run in the slots taken for them: none takes more) process.env.TCM_SUITE_SLOT = "1"; const results: TestRun[] = []; let vitest: VitestLike | undefined; try { vitest = await startVitest( [], { root, watch: false, reporters: [{ onUserConsoleLog() {} }], projects, maxWorkers: slots, }, { configFile: false, logLevel: process.env.TCM_KEEP_TEST_RUN ? "info" : "silent" }, ); if (process.env.TCM_KEEP_TEST_RUN) console.error( JSON.stringify( vitest.state.getFiles().map((f) => ({ tasks: f.tasks.length, errors: f.result?.errors })), null, 1, ), ); const ran = new Set<string>(); for (const file of vitest.state.getFiles()) { for (const task of file.tasks) ran.add(task.name); // a file that couldn't load (a missing module, a syntax error) fails the tests it holds, with why const fileErrors = (file.result?.errors ?? []) .map((e) => strip(e.message ?? String(e))) .join("\n"); if (fileErrors && !file.tasks.length) { for (const t of [...request.node, ...request.browserTests]) if (file.filepath?.includes(request.node.includes(t) ? "node.test" : "browser.test")) results.push({ hash: t.hash, ok: false, // a browser test that couldn't load: why it was put there, as the code it runs is often the reason message: t.browser ? `${fileErrors}\nThe test runs in a browser, since it ${t.browser}. A test runs in one when what it uses or renders needs one; whatever it uses that needs Node then can't load.` : fileErrors, output: "", ms: 0, }); } for (const task of file.tasks) { const errors = task.result?.errors ?? []; results.push({ hash: task.name, ok: task.result?.state === "pass", // with the expected/actual diff, when there is one message: errors .map((e) => strip(`${e.message ?? String(e)}${e.diff ? `\n${e.diff}` : ""}`)) .join("\n") || (task.result?.state === "pass" ? "" : `didn't run (${task.result?.state ?? "skipped"})`), output: (task.logs ?? []).map((l) => l.content.replace(/\n$/, "")).join("\n"), ms: Math.round(task.result?.duration ?? 0), }); } } } finally { fs.writeFileSync(request.results, JSON.stringify(results)); await vitest?.close(); release?.(); fs.rmSync(run, { recursive: true, force: true }); } }
uses
- tcmts.cli.budget.inSuiteSlot
- tcmts.cli.budget.machineSlots
- tcmts.cli.budget.slotPool
- tcmts.cli.budget.slotWaits
- tcmts.cli.budget.slotsDir
- tcmts.cli.codebase.Codebase
- tcmts.cli.vitest.Request
- tcmts.cli.vitest.VitestLike
- tcmts.cli.vitest.linkModules
- tcmts.cli.vitest.packagesKey
- tcmts.cli.vitest.strip
- tcmts.cli.vitest.vitestModule
- tcmts.cli.web.pinnedReacts
- tcmts.cli.web.pluginsForTests
- tcmts.cli.web.reactCompiler
- tcmts.loader.TestRun