From 5ef7251f1df958a8aed6445ed00540dc264da5e0 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Tue, 10 Dec 2024 16:00:10 -0800 Subject: [PATCH] test: mock 'node:test' module in node test harness --- test/js/node/bunfig.toml | 1 + test/js/node/harness.ts | 139 +++++++++++++++++++++++++++++++++++---- 2 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 test/js/node/bunfig.toml diff --git a/test/js/node/bunfig.toml b/test/js/node/bunfig.toml new file mode 100644 index 00000000000000..cac7f387d5cc4a --- /dev/null +++ b/test/js/node/bunfig.toml @@ -0,0 +1 @@ +preload = ["./harness.ts"] diff --git a/test/js/node/harness.ts b/test/js/node/harness.ts index f8f20089a1d2a5..f723a749ac1e9e 100644 --- a/test/js/node/harness.ts +++ b/test/js/node/harness.ts @@ -1,11 +1,14 @@ -import { AnyFunction } from "bun"; -import { hideFromStackTrace } from "harness"; +/** + * @note this file patches `node:test` via the require cache. + */ +import {AnyFunction} from "bun"; +import {hideFromStackTrace} from "harness"; import assertNode from "node:assert"; type DoneCb = (err?: Error) => any; function noop() {} export function createTest(path: string) { - const { expect, test, it, describe, beforeAll, afterAll, beforeEach, afterEach, mock } = Bun.jest(path); + const {expect, test, it, describe, beforeAll, afterAll, beforeEach, afterEach, mock} = Bun.jest(path); hideFromStackTrace(expect); @@ -201,11 +204,11 @@ export function createTest(path: string) { let completed = 0; const globalTimer = globalTimeout ? (timers.push( - setTimeout(() => { - console.log("Global Timeout"); - done(new Error("Timed out!")); - }, globalTimeout), - ), + setTimeout(() => { + console.log("Global Timeout"); + done(new Error("Timed out!")); + }, globalTimeout), + ), timers[timers.length - 1]) : undefined; function createDoneCb(timeout?: number) { @@ -213,11 +216,11 @@ export function createTest(path: string) { const timer = timeout !== undefined ? (timers.push( - setTimeout(() => { - console.log("Timeout"); - done(new Error("Timed out!")); - }, timeout), - ), + setTimeout(() => { + console.log("Timeout"); + done(new Error("Timed out!")); + }, timeout), + ), timers[timers.length - 1]) : timeout; return (result?: Error) => { @@ -262,3 +265,113 @@ export function createTest(path: string) { declare namespace Bun { function jest(path: string): typeof import("bun:test"); } + +if (Bun.main.includes("node/test/parallel")) { + function createMockNodeTestModule() { + + interface TestError extends Error { + testStack: string[]; + } + type Context = { + filename: string; + testStack: string[]; + failures: Error[]; + successes: number; + addFailure(err: unknown): TestError; + recordSuccess(): void; + } + const contexts: Record = {} + + // @ts-ignore + let activeSuite: Context = undefined; + + function createContext(key: string): Context { + return { + filename: key, // duplicate for ease-of-use + // entered each time describe, it, etc is called + testStack: [], + failures: [], + successes: 0, + addFailure(err: unknown) { + const error: TestError = (err instanceof Error ? err : new Error(err as any)) as any; + error.testStack = this.testStack; + const testMessage = `Test failed: ${this.testStack.join(" > ")}`; + error.message = testMessage + "\n" + error.message; + this.failures.push(error); + console.error(error); + return error; + }, + recordSuccess() { + const fullname = this.testStack.join(" > "); + console.log("✅ Test passed:", fullname); + this.successes++; + } + } + } + + function getContext() { + const key: string = Bun.main;// module.parent?.filename ?? require.main?.filename ?? __filename; + return activeSuite = (contexts[key] ??= createContext(key)); + } + + async function test(label: string | Function, fn?: Function | undefined) { + if (typeof fn !== "function" && typeof label === "function") { + fn = label; + label = fn.name; + } + const ctx = getContext(); + try { + ctx.testStack.push(label as string); + await fn(); + ctx.recordSuccess(); + } catch (err) { + const error = ctx.addFailure(err); + throw error; + } finally { + ctx.testStack.pop(); + } + } + + function describe(labelOrFn: string | Function, maybeFn?: Function) { + const [label, fn] = (typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn]); + if (typeof fn !== "function") throw new TypeError("Second argument to describe() must be a function."); + + getContext().testStack.push(label); + try { + fn(); + } catch (e) { + getContext().addFailure(e); + throw e + } finally { + getContext().testStack.pop(); + } + + const failures = getContext().failures.length; + const successes = getContext().successes; + console.error(`describe("${label}") finished with ${successes} passed and ${failures} failed tests.`); + if (failures > 0) { + throw new Error(`${failures} tests failed.`); + } + + } + + return { + test, + describe, + } + + } + + require.cache["node:test"] ??= { + exports: createMockNodeTestModule(), + loaded: true, + isPreloading: false, + id: "node:test", + parent: require.main, + filename: "node:test", + children: [], + path: "node:test", + paths: [], + require, + }; +}