runSuite
uses node:fs
Run a suite's test as a program of its own (passing if it returns), its command being entry: its result, and what it executed
(named as names had it when the run started: #htnfjrhl).
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { binVariable, executed, SUITE_TIMEOUT_MS, tcmsEnded } from "#codebase/tcmts/cli/suites"; import type { Suite } from "#codebase/tcmts/cli/suites"; import { nodeArgs, writeRunner } from "#codebase/tcmts/loader"; import type { TestResult } from "#codebase/tcmts/store"; import { spawn } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; export function runSuite( cb: Codebase, s: Suite, entry: string, names?: ReadonlyMap<string, string>, ): Promise<{ result: TestResult; ran: Record<string, string> }> { const coverage = fs.mkdtempSync(path.join(os.tmpdir(), "tcm-suite-")); const started = Date.now(); // (run from a test, node:test's own setting would make this run report to that test's runner) // (nor TCM_CODEBASE: a suite works in directories of its own; nor what the MCP server set for its own commands, // when a tool runs the tests: TCM_BRIEF's shorter reports, and TCM_BRANCH, the session's branch of this codebase; // nor what a tcm run further out set its commands to, TCM_COMMANDS) const { NODE_TEST_CONTEXT: _, TCM_CODEBASE: __, TCM_BRIEF: ___, TCM_BRANCH: ____, TCM_COMMANDS: _____, ...env } = process.env; const child = spawn( process.execPath, [...nodeArgs(cb.lay), writeRunner(s.hash, cb.source(), cb.lay)], { cwd: cb.lay.root, env: { ...env, [binVariable(s.bin)]: entry, // (what it runs takes no slot of its own: it's in this one, `inSuiteSlot`) TCM_SUITE_SLOT: "1", NODE_V8_COVERAGE: coverage, NO_COLOR: "1", FORCE_COLOR: "0", }, stdio: ["ignore", "pipe", "pipe"], }, ); let out = ""; // a test that hangs (something it started keeps it alive) fails, rather than keeping the run going for good const limit = setTimeout(() => { out += `\nError: timed out: still running after ${SUITE_TIMEOUT_MS / 60_000} minutes\n`; child.kill("SIGKILL"); }, SUITE_TIMEOUT_MS); child.stdout.setEncoding("utf8").on("data", (d: string) => (out += d)); child.stderr.setEncoding("utf8").on("data", (d: string) => (out += d)); return new Promise((resolve) => { child.on("close", async (code) => { clearTimeout(limit); const ok = code === 0; // what it threw: the error's first lines const thrown = ok ? undefined : /^(\w*Error\b.*(?:\n(?! {4}at ).*){0,12})/m.exec(out)?.[1]; const message = ok ? "" : (thrown?.trim() ?? `exited ${code}`); const output = ok ? "" : out.trim().split("\n").slice(0, 80).join("\n"); // what ran, once every tcm it started has ended (one that won't, or code no name follows: what ran isn't known, // so it isn't cached) const ran = ((await tcmsEnded(coverage, 15_000)) && executed(cb, coverage, names)) || {}; fs.rmSync(coverage, { recursive: true, force: true }); resolve({ result: { hash: s.hash, ok, message, output, ms: Date.now() - started }, ran }); }); }); }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.suites.SUITE_TIMEOUT_MS
- tcmts.cli.suites.Suite
- tcmts.cli.suites.binVariable
- tcmts.cli.suites.executed
- tcmts.cli.suites.tcmsEnded
- tcmts.loader.nodeArgs
- tcmts.loader.writeRunner
- tcmts.store.TestResult