Files
bun.sh/test/js/node/harness.ts
Don Isaac 0372ca5c0a test(node): get test-assert.js working (#15698)
Co-authored-by: Don Isaac <don@bun.sh>
Co-authored-by: DonIsaac <DonIsaac@users.noreply.github.com>
2025-01-10 00:45:43 +00:00

377 lines
10 KiB
TypeScript

/**
* @note this file patches `node:test` via the require cache.
*/
import { AnyFunction } from "bun";
import os from "node:os";
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);
hideFromStackTrace(expect);
// Assert
const strictEqual = (...args: Parameters<typeof assertNode.strictEqual>) => {
assertNode.strictEqual(...args);
expect(true).toBe(true);
};
const notStrictEqual = (...args: Parameters<typeof assertNode.notStrictEqual>) => {
assertNode.notStrictEqual(...args);
expect(true).toBe(true);
};
const deepStrictEqual = (...args: Parameters<typeof assertNode.deepStrictEqual>) => {
assertNode.deepStrictEqual(...args);
expect(true).toBe(true);
};
const throws = (...args: Parameters<typeof assertNode.throws>) => {
assertNode.throws(...args);
expect(true).toBe(true);
};
const ok = (...args: Parameters<typeof assertNode.ok>) => {
assertNode.ok(...args);
expect(true).toBe(true);
};
const ifError = (...args: Parameters<typeof assertNode.ifError>) => {
assertNode.ifError(...args);
expect(true).toBe(true);
};
const match = (...args: Parameters<typeof assertNode.match>) => {
assertNode.match(...args);
expect(true).toBe(true);
};
interface NodeAssert {
(args: any): void;
strictEqual: typeof strictEqual;
deepStrictEqual: typeof deepStrictEqual;
notStrictEqual: typeof notStrictEqual;
throws: typeof throws;
ok: typeof ok;
ifError: typeof ifError;
match: typeof match;
}
const assert = function (...args: any[]) {
// @ts-ignore
assertNode(...args);
} as NodeAssert;
hideFromStackTrace(strictEqual);
hideFromStackTrace(notStrictEqual);
hideFromStackTrace(deepStrictEqual);
hideFromStackTrace(throws);
hideFromStackTrace(ok);
hideFromStackTrace(ifError);
hideFromStackTrace(match);
hideFromStackTrace(assert);
Object.assign(assert, {
strictEqual,
deepStrictEqual,
notStrictEqual,
throws,
ok,
ifError,
match,
});
// End assert
const createCallCheckCtx = (done: DoneCb) => {
var timers: Timer[] = [];
const createDone = createDoneDotAll(done, undefined, timers);
// const mustCallChecks = [];
// failed.forEach(function (context) {
// console.log(
// "Mismatched %s function calls. Expected %s, actual %d.",
// context.name,
// context.messageSegment,
// context.actual
// );
// console.log(context.stack.split("\n").slice(2).join("\n"));
// });
// TODO: Implement this to be exact only
function mustCall(fn?: (...args: any[]) => any, exact?: number) {
return mustCallAtLeast(fn!, exact!);
}
function closeTimers() {
timers.forEach(t => clearTimeout(t));
}
function mustNotCall(reason: string = "function should not have been called", optionalCb?: (err?: any) => void) {
const localDone = createDone();
timers.push(setTimeout(() => localDone(), 200));
return () => {
closeTimers();
if (optionalCb) optionalCb.apply(undefined, reason ? [reason] : []);
done(new Error(reason));
};
}
function mustSucceed(fn: () => any, exact?: number) {
return mustCall(function (err, ...args) {
ifError(err);
// @ts-ignore
if (typeof fn === "function") return fn(...(args as []));
}, exact);
}
function mustCallAtLeast(fn: AnyFunction, minimum: number) {
return _mustCallInner(fn, minimum, "minimum");
}
function _mustCallInner(fn: AnyFunction, criteria = 1, field: string) {
// @ts-ignore
if (process._exiting) throw new Error("Cannot use common.mustCall*() in process exit handler");
if (typeof fn === "number") {
criteria = fn;
fn = noop;
} else if (fn === undefined) {
fn = noop;
}
if (typeof criteria !== "number") throw new TypeError(`Invalid ${field} value: ${criteria}`);
let actual = 0;
let expected = criteria;
// mustCallChecks.push(context);
const done = createDone();
const _return = (...args: any[]) => {
try {
// @ts-ignore
const result = fn(...args);
actual++;
if (actual >= expected) {
closeTimers();
done();
}
return result;
} catch (err) {
if (err instanceof Error) done(err);
else if (err?.toString) done(new Error(err?.toString()));
else {
console.error("Unknown error", err);
done(new Error("Unknown error"));
}
closeTimers();
}
};
// Function instances have own properties that may be relevant.
// Let's replicate those properties to the returned function.
// Refs: https://tc39.es/ecma262/#sec-function-instances
Object.defineProperties(_return, {
name: {
value: fn.name,
writable: false,
enumerable: false,
configurable: true,
},
length: {
value: fn.length,
writable: false,
enumerable: false,
configurable: true,
},
});
return _return;
}
return {
mustSucceed,
mustCall,
mustCallAtLeast,
mustNotCall,
closeTimers,
};
};
function createDoneDotAll(done: DoneCb, globalTimeout?: number, timers: Timer[] = []) {
let toComplete = 0;
let completed = 0;
const globalTimer = globalTimeout
? (timers.push(
setTimeout(() => {
console.log("Global Timeout");
done(new Error("Timed out!"));
}, globalTimeout),
),
timers[timers.length - 1])
: undefined;
function createDoneCb(timeout?: number) {
toComplete += 1;
const timer =
timeout !== undefined
? (timers.push(
setTimeout(() => {
console.log("Timeout");
done(new Error("Timed out!"));
}, timeout),
),
timers[timers.length - 1])
: timeout;
return (result?: Error) => {
if (timer) clearTimeout(timer);
if (globalTimer) clearTimeout(globalTimer);
if (result instanceof Error) {
done(result);
return;
}
completed += 1;
if (completed === toComplete) {
done();
}
};
}
return createDoneCb;
}
return {
expect,
test,
it,
describe,
beforeAll,
afterAll,
beforeEach,
afterEach,
createDoneDotAll,
strictEqual,
notStrictEqual,
deepStrictEqual,
throws,
ok,
ifError,
createCallCheckCtx,
match,
assert,
mock,
};
}
declare namespace Bun {
function jest(path: string): typeof import("bun:test");
}
const normalized = os.platform() === "win32" ? Bun.main.replaceAll("\\", "/") : Bun.main;
if (normalized.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</* requiring file */ string, Context> = {};
// @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,
};
}