runTests

tcmts.loader.runTests · #vuprjaqq · term · runs on the server

uses node:path

Run tests (functions that pass by returning without throwing, and fail on false) in one child process, capturing each one's console output.

import { exportId } from "#codebase/tcmts/core/hash";
import {
  materialize, moduleFile, nodeArgs, runnerStdout, TEST_TIMEOUT_MS, writeIfChanged,
} from "#codebase/tcmts/loader";
import type { DefSource, Layout, TestRun } from "#codebase/tcmts/loader";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

export function runTests(tests: { hash: string; componentHash: string }[], src: DefSource, lay: Layout): TestRun[] {
  if (!tests.length) return [];
  materialize(tests.map((t) => t.hash), src, lay);
  const runner = path.join(lay.cacheDir, "test_runner.ts");
  const out = path.join(lay.cacheDir, "test_results.json");
  fs.rmSync(out, { force: true });
  const list = tests.map((t) => [t.hash, `./${moduleFile(t.componentHash, src)}`, exportId(t.hash)]);
  writeIfChanged(
    runner,
    `import { inspect } from "node:util";
import { writeFileSync } from "node:fs";
const tests: [string, string, string][] = ${JSON.stringify(list)};
const results: { hash: string; ok: boolean; message: string; output: string; ms: number }[] = [];
const original = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
const save = () => writeFileSync(${JSON.stringify(out)}, JSON.stringify(results));
for (const [hash, file, id] of tests) {
  const output: string[] = [];
  for (const k of Object.keys(original)) {
    (console as unknown as Record<string, unknown>)[k] = (...args: unknown[]) => output.push(args.map((a) => (typeof a === "string" ? a : inspect(a))).join(" "));
  }
  const start = performance.now();
  let ok = true;
  let message = "";
  let timer: NodeJS.Timeout | undefined;
  try {
    const test = (await import(file))[id];
    if (typeof test !== "function") throw new Error("a test must be a function");
    const timeout = new Promise((_, reject) => (timer = setTimeout(() => reject(new Error("timed out after ${TEST_TIMEOUT_MS / 1000}s")), ${TEST_TIMEOUT_MS})));
    if ((await Promise.race([test(), timeout])) === false) throw new Error("returned false");
  } catch (e) {
    ok = false;
    message = e instanceof Error ? e.message : inspect(e);
  } finally {
    clearTimeout(timer);
    Object.assign(console, original);
  }
  results.push({ hash, ok, message, output: output.join("\\n"), ms: Math.round(performance.now() - start) });
  save();
}
save();
`,
  );
  spawnSync(process.execPath, [...nodeArgs(lay), runner], { stdio: ["ignore", runnerStdout, "inherit"], timeout: 5 * 60_000 });
  const done: TestRun[] = fs.existsSync(out) ? (JSON.parse(fs.readFileSync(out, "utf8")) as TestRun[]) : [];
  // a test that crashed the runner (e.g. process.exit) is the first one without a result
  return tests.map(
    (t) =>
      done.find((d) => d.hash === t.hash) ?? {
        hash: t.hash,
        ok: false,
        message: "no result: the test runner crashed or exited before this test finished",
        output: "",
        ms: 0,
        infrastructure: true,
      },
  );
}

uses