vitestChild

tcmts.cli.vitest.vitestChild · #c3uuul7g · term · runs on the server

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

used by