Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375// Derived from Node.js v24.16.0 (MIT; Copyright Node.js contributors, and Joyent, Inc. and// other Node contributors). See LICENSE-node.// node:internal/test_runner/runner.js: run(). Every option validates exactly like// Node's (same ERR_* codes and messages) regardless of what this kernel can actually// do with it; only after every option has validated does this decide whether it can// run at all.//// run({ isolation: "process" }, the default) refuses (DESIGN.md §Refused, not faked):// Node spawns `node --test-reporter=<path> --test-reporter-destination=...` child// processes per test file and reparses their stdout as a stream of v8.Serializer// frames (FileTest#parseMessage/#processRawBuffer, this file's biggest chunk — all of// it dropped here). This kernel has no CLI flag parser (no --test-reporter, no way to// tell a child "report through this one built-in reporter") and no v8.Serializer. It// refuses once validation finishes, not before: a malformed `options.files` still// throws ERR_INVALID_ARG_TYPE, never the refusal, matching Node's own order (Node// would get just as far before isolation ever comes into play — isolation only// decides which branch executes the already-validated options).//// run({ watch: true }) and run({ coverage: true }) refuse the same way: watch needs a// FilesWatcher over fs.watch's recursive/rename semantics (not implemented — DESIGN.md// §fs), coverage needs the inspector (refused — DESIGN.md §Refused).//// run({ isolation: "none" }) loads every file in THIS process and is fully ported:// globPatterns/the default test-file glob, `files`, cwd, shard, setup(), concurrency/// timeout/signal (all three flow straight into the root Test's own options, which// already validates them), and the exact file-load/placeholder-test protocol Node's// runFiles() closure for isolation:'none' uses (confirmed against real Node 24.16.0 —// see the three cases below). `only`/testNamePatterns/testSkipPatterns validate and// convert exactly like Node's, but (confirmed against real Node 24.16.0, see the// commands this header's companion report cites) **do not actually filter anything**// for isolation:'none' — that's not a shortcut here, it's Node's own real behavior:// runner.js's run() never copies these three options into the `globalOptions` object// isolation:'none' builds its root from (they're consumed only by getRunArgs(), which// builds an isolation:'process' child's `--test-only`/`--test-name-pattern=`/// `--test-skip-pattern=` argv). harness.ts's createTestTree/test.ts's// willBeFilteredByName exist and work (a future CLI parser's --test-name-pattern would// reach them through commandLineDefaults()); run()'s own options just never reach that// path, on purpose, to match.import { invalidArgType, invalidArgValue, outOfRange } from "../errors";import { unsupported } from "../refuse";import type { GuestProcess } from "../process";import { createTestTree, startSubtestAfterBootstrap } from "./harness";import { Test } from "./test";import { TestsStream } from "./tests_stream";import { commandLineDefaults, convertStringToRegExp, kDefaultPattern, type GlobalTestOptions } from "./utils";
/** What runner.ts needs from outside test_runner/: the real guest process (cwd, * stderr, exit — for the "no files matched a literal pattern" case), a glob search * (builtins.ts wires this to fs.ts's globSync), and a way to load one file as a * module in this process (builtins.ts wires this to the same * `globalThis[Symbol.for("nib.runtime")].importFrom` hook a guest's own dynamic * `import()` is rewritten to call — see src/kernel/transform.ts — so a loaded test * file resolves exactly like a real ESM or CJS import, CJS and ESM alike, with no * separate code path here). */export interface RunnerDeps { proc: GuestProcess; globSync: (patterns: string[], cwd: string) => string[]; importFile: (cwd: string, absolutePath: string) => Promise<unknown>;}
// ---- Node's internal/validators.js, kept local to this module (the established// pattern here — see histogram.ts, readline/promises.ts, test.ts's own copy): no// shared validateObject/validateBoolean/validateArray/validateFunction/validateOneOf/// validateInteger/validateStringArray/validatePath exists in src/runtime. ----
function validateObject(value: unknown, name: string): void { if (value === null || Array.isArray(value) || typeof value !== "object") throw invalidArgType(name, "of type object", value);}
function validateBoolean(value: unknown, name: string): void { if (typeof value !== "boolean") throw invalidArgType(name, "of type boolean", value);}
function validateArray(value: unknown, name: string): asserts value is unknown[] { if (!Array.isArray(value)) throw invalidArgType(name, "an instance of Array", value);}
function validateFunction(value: unknown, name: string): void { if (typeof value !== "function") throw invalidArgType(name, "of type function", value);}
function validateOneOf<T>(value: T, name: string, oneOf: readonly T[]): void { if (!oneOf.includes(value)) { const allowed = oneOf.map((v) => (typeof v === "string" ? `'${v}'` : String(v))).join(", "); throw invalidArgValue(name, value, `must be one of: ${allowed}`); }}
function validateInteger(value: unknown, name: string, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER): void { if (typeof value !== "number") throw invalidArgType(name, "of type number", value); if (!Number.isInteger(value)) throw outOfRange(name, "an integer", value); if (value < min || value > max) throw outOfRange(name, `>= ${min} && <= ${max}`, value);}
function validateUint32(value: unknown, name: string): void { if (typeof value !== "number") throw invalidArgType(name, "of type number", value); if (!Number.isInteger(value)) throw outOfRange(name, "an integer", value); if (value < 0 || value > 4_294_967_295) throw outOfRange(name, ">= 0 && <= 4294967295", value);}
function validateString(value: unknown, name: string): void { if (typeof value !== "string") throw invalidArgType(name, "of type string", value);}
function validateStringArray(value: unknown, name: string): void { validateArray(value, name); value.forEach((v, i) => { if (typeof v !== "string") throw invalidArgType(`${name}[${i}]`, "of type string", v); });}
/** Node's internal/fs/utils.js validatePath, trimmed to the string half (a run() * option is never a Buffer/URL here in practice; the type-check message still names * all three, matching Node). */function validatePath(value: unknown, name: string): void { if (typeof value !== "string" && !(value instanceof Uint8Array)) { throw invalidArgType(name, "of type string or an instance of Buffer or URL", value); } if (typeof value === "string" && value.includes("\u0000")) { throw invalidArgValue(name, value, "must be a string, Uint8Array, or URL without null bytes"); }}
/** A pattern with no glob metachar anywhere (Node's Glob#hasMagic): createTestFileList * treats that as a literal filename and exits the process when it finds nothing, * instead of silently running zero files. */const GLOB_MAGIC = /[*?{}[\]]/;
function normalizePatterns(value: unknown, name: string): RegExp[] | null { if (value == null) return null; const arr = Array.isArray(value) ? value : [value]; return arr.map((v, i) => { if (v instanceof RegExp) return v; if (typeof v === "string") return convertStringToRegExp(v, `${name}[${i}]`); throw invalidArgType(`${name}[${i}]`, "of type string or an instance of RegExp", v); });}
function normalizeStringArrayOption(value: unknown, name: string): void { if (value == null) return; validateStringArray(Array.isArray(value) ? value : [value], name);}
/** Resolve a (possibly relative) test-file path against cwd, lexically — no fs access, * matching path.resolve's semantics for an already-POSIX, already-simple path (this * kernel's fs is POSIX-only; see DESIGN.md §path.win32). */function resolveAbs(cwd: string, p: string): string { const base = p.startsWith("/") ? p : `${cwd}/${p}`; const out: string[] = []; for (const seg of base.split("/")) { if (seg === "" || seg === ".") continue; if (seg === "..") out.pop(); else out.push(seg); } return `/${out.join("/")}`;}
interface Shard { index: number; total: number;}
/** Node's createTestFileList: `files` wins outright; otherwise glob `globPatterns` * (default kDefaultPattern) under cwd, excluding node_modules, sorted. A user-supplied * pattern list with zero magic chars and zero matches is Node's "you misspelled a * literal filename" guard — it prints and exits the process rather than running * nothing (confirmed against real Node 24.16.0: `run({ globPatterns: ['nope.js'] })` * prints `Could not find 'nope.js'` to stderr and the process exits 1, never emitting * any test event at all). */function resolveTestFiles(files: string[] | undefined, globPatterns: string[] | undefined, cwd: string, deps: RunnerDeps, shard: Shard | null): string[] { let testFiles: string[]; if (files != null) { testFiles = files; } else { const hasUserSuppliedPattern = globPatterns != null; const patterns = globPatterns && globPatterns.length > 0 ? globPatterns : [kDefaultPattern]; const results = deps.globSync(patterns, cwd); if (hasUserSuppliedPattern && results.length === 0 && patterns.every((p) => !GLOB_MAGIC.test(p))) { deps.proc.stderr.write(`Could not find '${patterns.join(", ")}'\n`); deps.proc.exit(1); } testFiles = [...results].sort(); } return shard ? testFiles.filter((_, index) => index % shard.total === shard.index - 1) : testFiles;}
/** Node's run()'s isolation:'none' runFiles closure: import every test file in * declared order (sequential — `await cascadedLoader.import(...)` per file, never * concurrent), each attaching its own top-level test()/describe() calls to `root` * (harness.ts's currentParent() resolves to `root` because createTestTree just made * it the module's globalRoot). A file that registers zero top-level tests — whether * it threw while loading or just has none — gets a placeholder Test named after its * path; a throw fails that placeholder instead of aborting the whole run (confirmed * against real Node 24.16.0: a syntax/throw-at-load file reports as one failing test * named after the file, and every other file still loads and runs). The placeholder's * startSubtestAfterBootstrap() call is fire-and-forget, same as an ordinary top-level * `test()` call. * * Before any of that, this replaces `root.harness.bootstrapPromise` with one that * only resolves once every file below has loaded (`finishBootstrap()`, after the * loop) — Node's actual fix for the file-loading race, not a kernel-only workaround: * every top-level test(), here or in direct execution, is funneled through * harness.ts's startSubtestAfterBootstrap, which awaits this exact promise before * calling subtest.start(). So no top-level test from any file can start — let alone * finish and trip finalize()'s "every top-level test is done" check — until every * file has had its chance to register its own tests first. */async function runIsolatedNone(root: Test, testFiles: string[], cwd: string, deps: RunnerDeps): Promise<void> { let finishBootstrap!: () => void; const bootstrapSettled = new Promise<void>((resolve) => (finishBootstrap = resolve)); const harness = root.harness!; harness.bootstrapPromise = harness.bootstrapPromise ? Promise.all([harness.bootstrapPromise, bootstrapSettled]).then(() => {}) : bootstrapSettled;
let topLevelTestCount = 0; for (const testFile of testFiles) { const abs = resolveAbs(cwd, testFile); root.entryFile = abs; let threw = false; let importError: unknown; try { await deps.importFile(cwd, abs); } catch (err) { threw = true; importError = err; } if (topLevelTestCount === root.subtests.length) { const subtest = root.createSubtest(Test, testFile, {}, undefined, { loc: [1, 1, abs] }); if (threw) subtest.fail(importError); void startSubtestAfterBootstrap(subtest); } topLevelTestCount = root.subtests.length; } root.entryFile = undefined; finishBootstrap(); await root.processPendingSubtests();}
export function run(optionsArg: unknown, deps: RunnerDeps): TestsStream { // Node's run(options = kEmptyObject): a default parameter only substitutes on // `undefined`, never on `null` — `run(null)` must still fail validateObject(), // confirmed against real Node 24.16.0 (ERR_INVALID_ARG_TYPE, not treated as {}). const raw = optionsArg === undefined ? {} : optionsArg; validateObject(raw, "options"); const options = raw as Record<string, unknown>;
const testNamePatternsOpt = options.testNamePatterns; const testSkipPatternsOpt = options.testSkipPatterns; const shardOpt = options.shard as { index?: unknown; total?: unknown } | null | undefined; const coverageExcludeGlobs = options.coverageExcludeGlobs; const coverageIncludeGlobs = options.coverageIncludeGlobs;
const concurrency = options.concurrency as number | boolean | undefined; const timeout = options.timeout as number | undefined; const signal = options.signal as AbortSignal | undefined; const files = options.files as string[] | undefined; const forceExit = options.forceExit as boolean | undefined; const isolation = (options.isolation as string | undefined) ?? "process"; const watch = options.watch as boolean | undefined; const setup = options.setup as ((reporter: TestsStream) => unknown) | undefined; const globalSetupPath = options.globalSetupPath; const only = options.only as boolean | undefined; const globPatterns = options.globPatterns as string[] | undefined; const coverage = (options.coverage as boolean | undefined) ?? false; const lineCoverage = (options.lineCoverage as number | undefined) ?? 0; const branchCoverage = (options.branchCoverage as number | undefined) ?? 0; const functionCoverage = (options.functionCoverage as number | undefined) ?? 0; const suppliedRandomize = options.randomize as boolean | undefined; const suppliedRandomSeed = options.randomSeed as number | undefined; const execArgv = (options.execArgv as string[] | undefined) ?? []; const argv = (options.argv as string[] | undefined) ?? []; const cwd = (options.cwd as string | undefined) ?? deps.proc.cwd(); const rerunFailuresFilePath = options.rerunFailuresFilePath; const env = options.env;
if (files != null) validateArray(files, "options.files"); if (watch != null) validateBoolean(watch, "options.watch"); if (forceExit != null) { validateBoolean(forceExit, "options.forceExit"); if (forceExit && watch) throw invalidArgValue("options.forceExit", watch, "is not supported with watch mode"); } if (only != null) validateBoolean(only, "options.only"); if (globPatterns != null) validateArray(globPatterns, "options.globPatterns"); if (suppliedRandomize != null) validateBoolean(suppliedRandomize, "options.randomize"); if (suppliedRandomSeed != null) validateUint32(suppliedRandomSeed, "options.randomSeed"); let randomize = suppliedRandomize; const randomSeed = suppliedRandomSeed; if (randomSeed != null) randomize = true; if (watch) { if (randomSeed != null) throw invalidArgValue("options.randomSeed", randomSeed, "is not supported with watch mode"); if (randomize) throw invalidArgValue("options.randomize", randomize, "is not supported with watch mode"); } if (rerunFailuresFilePath != null) { validatePath(rerunFailuresFilePath, "options.rerunFailuresFilePath"); if (randomSeed != null) throw invalidArgValue("options.randomSeed", randomSeed, "is not supported with rerun failures mode"); if (randomize) throw invalidArgValue("options.randomize", randomize, "is not supported with rerun failures mode"); }
validateString(cwd, "options.cwd");
if ((globPatterns?.length ?? 0) > 0 && (files?.length ?? 0) > 0) { throw invalidArgValue("options.globPatterns", globPatterns, "is not supported when specifying 'options.files'"); }
let shard: Shard | null = null; if (shardOpt != null) { validateObject(shardOpt, "options.shard"); shard = { index: shardOpt.index as number, total: shardOpt.total as number }; validateInteger(shard.total, "options.shard.total", 1); validateInteger(shard.index, "options.shard.index", 1, shard.total); if (watch) throw invalidArgValue("options.shard", watch, "shards not supported with watch mode"); }
if (setup != null) validateFunction(setup, "options.setup");
// Validated and converted for error-parity with Node; not wired into harness.config // for isolation:'none' — see this file's header comment. normalizePatterns(testNamePatternsOpt, "options.testNamePatterns"); normalizePatterns(testSkipPatternsOpt, "options.testSkipPatterns");
validateOneOf(isolation, "options.isolation", ["process", "none"]); validateBoolean(coverage, "options.coverage"); normalizeStringArrayOption(coverageExcludeGlobs, "options.coverageExcludeGlobs"); normalizeStringArrayOption(coverageIncludeGlobs, "options.coverageIncludeGlobs"); validateInteger(lineCoverage, "options.lineCoverage", 0, 100); validateInteger(branchCoverage, "options.branchCoverage", 0, 100); validateInteger(functionCoverage, "options.functionCoverage", 0, 100);
validateStringArray(argv, "options.argv"); validateStringArray(execArgv, "options.execArgv");
if (globalSetupPath != null) validatePath(globalSetupPath, "options.globalSetupPath");
if (env != null) { // Node calls validateObject(env) with no `name` here — an internal-errors.js bug // (ERR_INTERNAL_ASSERTION 'name' must be a string, confirmed against real Node // 24.16.0 for any non-object `env`) rather than real behavior worth reproducing. // This gives the useful ERR_INVALID_ARG_TYPE instead. validateObject(env, "options.env"); if (isolation === "none") throw invalidArgValue("options.env", env, "is not supported with isolation='none'"); }
// ---- kernel-specific refusals (DESIGN.md §Refused, not faked) — only after every // option above has validated, matching how far a real call would get in Node too. ---- if (isolation !== "none") { throw unsupported( "test", "run() with isolation: 'process' (the default)", "spawns `node --test-reporter=...` child processes and reads back the v8-serializer reporter's bytes; " + "this kernel has no CLI flag parser and no v8.Serializer. Pass { isolation: 'none' } to run tests in this process", ); } if (coverage) { throw unsupported("test", "run({ coverage: true })", "code coverage needs the inspector, which this kernel does not expose"); } if (watch) { throw unsupported( "test", "run({ watch: true })", "file-watch test runs need a FilesWatcher over fs.watch's recursive/rename semantics, which this kernel does not implement", ); }
const globalOptions: GlobalTestOptions = commandLineDefaults(cwd); const root = createTestTree({ concurrency, timeout, signal }, globalOptions, { autoReport: false }); const testFiles = resolveTestFiles(files, globPatterns, cwd, deps, shard);
void (async () => { if (typeof setup === "function") await setup(root.reporter); await runIsolatedNone(root, testFiles, cwd, deps); })();
return root.reporter;}