diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index 45f13c3045..198cad6785 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -174,7 +174,7 @@ Some methods are not optimized yet. ### [`node:test`](https://nodejs.org/api/test.html) -🔴 Not implemented. Use [`bun:test`](https://bun.sh/docs/cli/test) instead. +🟡 Partly implemented. Missing mocks, snapshots, timers. Use [`bun:test`](https://bun.sh/docs/cli/test) instead. ### [`node:trace_events`](https://nodejs.org/api/tracing.html) diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index f5d975bf16..13b318e2a5 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -256,8 +256,10 @@ async function runTests() { for (const testPath of tests) { const absoluteTestPath = join(testsPath, testPath); const title = relative(cwd, absoluteTestPath).replaceAll(sep, "/"); - if (isNodeParallelTest(testPath)) { - const runWithBunTest = title.includes("needs-test") || readFileSync(absoluteTestPath, "utf-8").includes('bun:test'); + if (isNodeTest(testPath)) { + const testContent = readFileSync(absoluteTestPath, "utf-8"); + const runWithBunTest = + title.includes("needs-test") || testContent.includes("bun:test") || testContent.includes("node:test"); const subcommand = runWithBunTest ? "test" : "run"; await runTest(title, async () => { const { ok, error, stdout } = await spawnBun(execPath, { @@ -870,19 +872,26 @@ function isJavaScriptTest(path) { } /** - * @param {string} testPath + * @param {string} path * @returns {boolean} */ -function isNodeParallelTest(testPath) { - return testPath.replaceAll(sep, "/").includes("js/node/test/parallel/"); +function isNodeTest(path) { + // Do not run node tests on macOS x64 in CI + // TODO: Unclear why we decided to do this? + if (isCI && isMacOS && isX64) { + return false; + } + const unixPath = path.replaceAll(sep, "/"); + return unixPath.includes("js/node/test/parallel/") || unixPath.includes("js/node/test/sequential/"); } /** - * @param {string} testPath + * @param {string} path * @returns {boolean} */ -function isNodeSequentialTest(testPath) { - return testPath.replaceAll(sep, "/").includes("js/node/test/sequential/"); +function isClusterTest(path) { + const unixPath = path.replaceAll(sep, "/"); + return unixPath.includes("js/node/cluster/test-") && unixPath.endsWith(".ts"); } /** @@ -890,21 +899,17 @@ function isNodeSequentialTest(testPath) { * @returns {boolean} */ function isTest(path) { - if (isNodeParallelTest(path) && targetDoesRunNodeTests()) return true; - if (isNodeSequentialTest(path) && targetDoesRunNodeTests()) return true; - if (path.replaceAll(sep, "/").startsWith("js/node/cluster/test-") && path.endsWith(".ts")) return true; - return isTestStrict(path); + return isNodeTest(path) || isClusterTest(path) ? true : isTestStrict(path); } +/** + * @param {string} path + * @returns {boolean} + */ function isTestStrict(path) { return isJavaScript(path) && /\.test|spec\./.test(basename(path)); } -function targetDoesRunNodeTests() { - if (isMacOS && isX64) return false; - return true; -} - /** * @param {string} path * @returns {boolean} diff --git a/src/bun.js/bindings/isBuiltinModule.cpp b/src/bun.js/bindings/isBuiltinModule.cpp index 93afd4f70e..56ad9ad13f 100644 --- a/src/bun.js/bindings/isBuiltinModule.cpp +++ b/src/bun.js/bindings/isBuiltinModule.cpp @@ -78,27 +78,31 @@ static constexpr ASCIILiteral builtinModuleNamesSortedLength[] = { "inspector/promises"_s, "_stream_passthrough"_s, "diagnostics_channel"_s, + "node:test"_s, }; namespace Bun { bool isBuiltinModule(const String& namePossiblyWithNodePrefix) { + // First check the original name as-is + for (auto& builtinModule : builtinModuleNamesSortedLength) { + if (namePossiblyWithNodePrefix == builtinModule) + return true; + } + + // If no match found and the name has a "node:" prefix, try without the prefix String name = namePossiblyWithNodePrefix; if (name.startsWith("node:"_s)) { name = name.substringSharingImpl(5); - // bun doesn't have `node:test` as of writing, but this makes sure that - // `node:module` is compatible (`test/parallel/test-module-isBuiltin.js`) - if (name == "test"_s) { - return true; + // Check again with the prefix removed + for (auto& builtinModule : builtinModuleNamesSortedLength) { + if (name == builtinModule) + return true; } } - for (auto& builtinModule : builtinModuleNamesSortedLength) { - if (name == builtinModule) - return true; - } return false; } diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index e469bc4fbc..f67f007001 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -1781,7 +1781,7 @@ pub const ModuleLoader = struct { .already_bundled = true, .hash = 0, .bytecode_cache = if (bytecode_slice.len > 0) bytecode_slice.ptr else null, - .bytecode_cache_size = if (bytecode_slice.len > 0) bytecode_slice.len else 0, + .bytecode_cache_size = bytecode_slice.len, .is_commonjs_module = parse_result.already_bundled.isCommonJS(), }; } @@ -2902,7 +2902,8 @@ pub const HardcodedModule = enum { .{ "stream/promises", .{ .path = "stream/promises" } }, .{ "stream/web", .{ .path = "stream/web" } }, .{ "string_decoder", .{ .path = "string_decoder" } }, - // .{ "test", .{ .path = "test" } }, + // Node.js does not support "test", only "node:test" + // .{ "test", .{ .path = "node:test" } }, .{ "timers", .{ .path = "timers" } }, .{ "timers/promises", .{ .path = "timers/promises" } }, .{ "tls", .{ .path = "tls" } }, diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index 2a1c9bafd2..fa648ea786 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -115,6 +115,7 @@ static constexpr ASCIILiteral builtinModuleNames[] = { "worker_threads"_s, "ws"_s, "zlib"_s, + "node:test"_s, }; template consteval std::size_t countof(T (&)[N]) diff --git a/src/js/node/repl.ts b/src/js/node/repl.ts index d613731db3..a7954e70b7 100644 --- a/src/js/node/repl.ts +++ b/src/js/node/repl.ts @@ -75,6 +75,7 @@ const builtinModules = [ "wasi", "worker_threads", "zlib", + "node:test", ]; export default { diff --git a/src/js/node/test.ts b/src/js/node/test.ts index 01470f3c9c..9475c35226 100644 --- a/src/js/node/test.ts +++ b/src/js/node/test.ts @@ -1,38 +1,621 @@ // Hardcoded module "node:test" +// This follows the Node.js API as described in: https://nodejs.org/api/test.html -const { throwNotImplemented } = require("internal/shared"); +const { jest } = Bun; +const { kEmptyObject, throwNotImplemented } = require("internal/shared"); +const Readable = require("internal/streams/readable"); -function suite() { - throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +const kDefaultName = ""; +const kDefaultFunction = () => {}; +const kDefaultOptions = kEmptyObject; +const kDefaultFilePath = callerSourceOrigin(); + +function run(...args: unknown[]) { + throwNotImplemented("run()", 5090, "Use `bun:test` in the interim."); } -function test() { - throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +function mock(...args: unknown[]) { + throwNotImplemented("mock()", 5090, "Use `bun:test` in the interim."); } -function before() { - throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +/** + * @link https://nodejs.org/api/test.html#class-mockfunctioncontext + */ +class MockFunctionContext { + constructor() { + throwNotImplemented("new MockFunctionContext()", 5090, "Use `bun:test` in the interim."); + } + + get calls() { + throwNotImplemented("calls()", 5090, "Use `bun:test` in the interim."); + return undefined; + } + + callCount() { + throwNotImplemented("callCount()", 5090, "Use `bun:test` in the interim."); + return 0; + } + + mockImplementation(fn: Function) { + throwNotImplemented("mockImplementation()", 5090, "Use `bun:test` in the interim."); + return undefined; + } + + mockImplementationOnce(fn: Function, onCall?: unknown) { + throwNotImplemented("mockImplementationOnce()", 5090, "Use `bun:test` in the interim."); + return undefined; + } + + resetCalls() { + throwNotImplemented("resetCalls()", 5090, "Use `bun:test` in the interim."); + } + + restore() { + throwNotImplemented("restore()", 5090, "Use `bun:test` in the interim."); + } } -function after() { - throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +/** + * @link https://nodejs.org/api/test.html#class-mockmodulecontext + */ +class MockModuleContext { + constructor() { + throwNotImplemented("new MockModuleContext()", 5090, "Use `bun:test` in the interim."); + } + + restore() { + throwNotImplemented("restore()", 5090, "Use `bun:test` in the interim."); + } } -function beforeEach() { - throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +/** + * @link https://nodejs.org/api/test.html#class-mocktracker + */ +class MockTracker { + constructor() { + throwNotImplemented("new MockTracker()", 5090, "Use `bun:test` in the interim."); + } + + fn(original: unknown, implementation: unknown, options: unknown) { + throwNotImplemented("fn()", 5090, "Use `bun:test` in the interim."); + return undefined; + } + + method(object: unknown, methodName: unknown, implementation: unknown, options: unknown) { + throwNotImplemented("method()", 5090, "Use `bun:test` in the interim."); + return undefined; + } + + getter(original: unknown, implementation: unknown, options: unknown) { + throwNotImplemented("getter()", 5090, "Use `bun:test` in the interim."); + return undefined; + } + + setter(original: unknown, implementation: unknown, options: unknown) { + throwNotImplemented("setter()", 5090, "Use `bun:test` in the interim."); + return undefined; + } + + module(specifier: unknown, options: unknown) { + throwNotImplemented("module()", 5090, "Use `bun:test` in the interim."); + return undefined; + } + + reset() { + throwNotImplemented("reset()", 5090, "Use `bun:test` in the interim."); + } + + restoreAll() { + throwNotImplemented("restoreAll()", 5090, "Use `bun:test` in the interim."); + } } -function afterEach() { - throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +class MockTimers { + constructor() { + throwNotImplemented("new MockTimers()", 5090, "Use `bun:test` in the interim."); + } + + enable(options: unknown) { + throwNotImplemented("enable()", 5090, "Use `bun:test` in the interim."); + } + + reset() { + throwNotImplemented("reset()", 5090, "Use `bun:test` in the interim."); + } + + tick(milliseconds: unknown) { + throwNotImplemented("tick()", 5090, "Use `bun:test` in the interim."); + } + + runAll() { + throwNotImplemented("runAll()", 5090, "Use `bun:test` in the interim."); + } + + setTime(milliseconds: unknown) { + throwNotImplemented("setTime()", 5090, "Use `bun:test` in the interim."); + } + + [Symbol.dispose]() { + this.reset(); + } } -export default { - suite, - test, - describe: suite, - it: test, - before, - after, - beforeEach, - afterEach, +/** + * @link https://nodejs.org/api/test.html#class-testsstream + */ +class TestsStream extends Readable { + constructor() { + super(); + throwNotImplemented("new TestsStream()", 5090, "Use `bun:test` in the interim."); + } +} + +function fileSnapshot(value: unknown, path: string, options: { serializers?: Function[] } = kEmptyObject) { + throwNotImplemented("fileSnapshot()", 5090, "Use `bun:test` in the interim."); +} + +function snapshot(value: unknown, options: { serializers?: Function[] } = kEmptyObject) { + throwNotImplemented("snapshot()", 5090, "Use `bun:test` in the interim."); +} + +function register(name: string, fn: Function) { + throwNotImplemented("register()", 5090, "Use `bun:test` in the interim."); +} + +const assert = { + ...require("node:assert"), + fileSnapshot, + snapshot, + // register, }; + +// Delete deprecated methods on assert (required to pass node's tests) +delete assert.AssertionError; +delete assert.CallTracker; +delete assert.strict; + +/** + * @link https://nodejs.org/api/test.html#class-suitecontext + */ +class SuiteContext { + #name: string | undefined; + #filePath: string | undefined; + #abortController?: AbortController; + + constructor(name: string | undefined, filePath: string | undefined) { + this.#name = name; + this.#filePath = filePath || kDefaultFilePath; + } + + get name(): string { + return this.#name!; + } + + get filePath(): string { + return this.#filePath!; + } + + get signal(): AbortSignal { + if (this.#abortController === undefined) { + this.#abortController = new AbortController(); + } + return this.#abortController.signal; + } +} + +/** + * @link https://nodejs.org/api/test.html#class-testcontext + */ +class TestContext { + #insideTest: boolean; + #name: string | undefined; + #filePath: string | undefined; + #parent?: TestContext; + #abortController?: AbortController; + + constructor( + insideTest: boolean, + name: string | undefined, + filePath: string | undefined, + parent: TestContext | undefined, + ) { + this.#insideTest = insideTest; + this.#name = name; + this.#filePath = filePath || parent?.filePath || kDefaultFilePath; + this.#parent = parent; + } + + get signal(): AbortSignal { + if (this.#abortController === undefined) { + this.#abortController = new AbortController(); + } + return this.#abortController.signal; + } + + get name(): string { + return this.#name!; + } + + get fullName(): string { + let fullName = this.#name; + let parent = this.#parent; + while (parent && parent.name) { + fullName = `${parent.name} > ${fullName}`; + parent = parent.#parent; + } + return fullName!; + } + + get filePath(): string { + return this.#filePath!; + } + + diagnostic(message: string) { + console.log(message); + } + + plan(count: number, options: { wait?: boolean } = kEmptyObject) { + throwNotImplemented("plan()", 5090, "Use `bun:test` in the interim."); + } + + get assert() { + return assert; + } + + get mock() { + throwNotImplemented("mock", 5090, "Use `bun:test` in the interim."); + return undefined; + } + + runOnly(value?: boolean) { + throwNotImplemented("runOnly()", 5090, "Use `bun:test` in the interim."); + } + + skip(message?: string) { + throwNotImplemented("skip()", 5090, "Use `bun:test` in the interim."); + } + + todo(message?: string) { + throwNotImplemented("todo()", 5090, "Use `bun:test` in the interim."); + } + + before(arg0: unknown, arg1: unknown) { + const { fn, options } = createHook(arg0, arg1); + const { beforeAll } = bunTest(this); + beforeAll(fn); + } + + after(arg0: unknown, arg1: unknown) { + const { fn, options } = createHook(arg0, arg1); + const { afterAll } = bunTest(this); + afterAll(fn); + } + + beforeEach(arg0: unknown, arg1: unknown) { + const { fn, options } = createHook(arg0, arg1); + const { beforeEach } = bunTest(this); + beforeEach(fn); + } + + afterEach(arg0: unknown, arg1: unknown) { + const { fn, options } = createHook(arg0, arg1); + const { afterEach } = bunTest(this); + afterEach(fn); + } + + waitFor(condition: unknown, options: { timeout?: number } = kEmptyObject) { + throwNotImplemented("waitFor()", 5090, "Use `bun:test` in the interim."); + } + + test(arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = createTest(arg0, arg1, arg2); + + if (this.#insideTest) { + throwNotImplemented("test() inside another test()", 5090, "Use `bun:test` in the interim."); + } + + const { test } = bunTest(this); + if (options.only) { + test.only(name, fn); + } else if (options.todo) { + test.todo(name, fn); + } else if (options.skip) { + test.skip(name, fn); + } else { + test(name, fn); + } + } + + describe(arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = createDescribe(arg0, arg1, arg2); + + if (this.#insideTest) { + throwNotImplemented("describe() inside another test()", 5090, "Use `bun:test` in the interim."); + } + + const { describe } = bunTest(this); + describe(name, fn); + } +} + +function bunTest(ctx: SuiteContext | TestContext) { + return jest(ctx.filePath); +} + +let ctx = new TestContext(false, undefined, kDefaultFilePath, undefined); + +function describe(arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = createDescribe(arg0, arg1, arg2); + const { describe } = bunTest(ctx); + describe(name, fn); +} + +describe.skip = function (arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = createDescribe(arg0, arg1, arg2); + const { describe } = bunTest(ctx); + describe.skip(name, fn); +}; + +describe.todo = function (arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = createDescribe(arg0, arg1, arg2); + const { describe } = bunTest(ctx); + describe.todo(name, fn); +}; + +describe.only = function (arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = createDescribe(arg0, arg1, arg2); + const { describe } = bunTest(ctx); + describe.only(name, fn); +}; + +function test(arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = createTest(arg0, arg1, arg2); + const { test } = bunTest(ctx); + test(name, fn, options); +} + +test.skip = function (arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = createTest(arg0, arg1, arg2); + const { test } = bunTest(ctx); + test.skip(name, fn, options); +}; + +test.todo = function (arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = createTest(arg0, arg1, arg2); + const { test } = bunTest(ctx); + test.todo(name, fn, options); +}; + +test.only = function (arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = createTest(arg0, arg1, arg2); + const { test } = bunTest(ctx); + test.only(name, fn, options); +}; + +function before(arg0: unknown, arg1: unknown) { + const { fn, options } = createHook(arg0, arg1); + const { beforeAll } = bunTest(ctx); + beforeAll(fn); +} + +function after(arg0: unknown, arg1: unknown) { + const { fn, options } = createHook(arg0, arg1); + const { afterAll } = bunTest(ctx); + afterAll(fn); +} + +function beforeEach(arg0: unknown, arg1: unknown) { + const { fn, options } = createHook(arg0, arg1); + const { beforeEach } = bunTest(ctx); + beforeEach(fn); +} + +function afterEach(arg0: unknown, arg1: unknown) { + const { fn, options } = createHook(arg0, arg1); + const { afterEach } = bunTest(ctx); + afterEach(fn); +} + +function isBuiltinModule(filePath: string) { + return filePath.startsWith("node:") || filePath.startsWith("bun:") || filePath.startsWith("[native code]"); +} + +function callerSourceOrigin(): string { + const error = new Error(); + const originalPrepareStackTrace = Error.prepareStackTrace; + let origin: string | undefined; + Error.prepareStackTrace = (_, stack) => { + origin = stack + .find(s => { + const filePath = s.getFileName(); + if (filePath && !isBuiltinModule(filePath)) { + return filePath; + } + return undefined; + }) + ?.getFileName(); + }; + error.stack; + Error.prepareStackTrace = originalPrepareStackTrace; + if (!origin) { + throw new Error("Failed to get caller source origin"); + } + return origin; +} + +function parseTestOptions(arg0: unknown, arg1: unknown, arg2: unknown) { + let name: string; + let options: TestOptions; + let fn: TestFn; + + if (typeof arg0 === "function") { + name = arg0.name || kDefaultName; + fn = arg0 as TestFn; + if (typeof arg1 === "object") { + options = arg1 as TestOptions; + } else { + options = kDefaultOptions; + } + } else if (typeof arg0 === "string") { + name = arg0; + if (typeof arg1 === "object") { + options = arg1 as TestOptions; + if (typeof arg2 === "function") { + fn = arg2 as TestFn; + } else { + fn = kDefaultFunction; + } + } else if (typeof arg1 === "function") { + fn = arg1 as TestFn; + options = kDefaultOptions; + } else { + fn = kDefaultFunction; + options = kDefaultOptions; + } + } else { + name = kDefaultName; + fn = kDefaultFunction; + options = kDefaultOptions; + } + + return { name, options, fn }; +} + +function createTest(arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, options, fn } = parseTestOptions(arg0, arg1, arg2); + + const originalContext = ctx; + const context = new TestContext(true, name, ctx.filePath, originalContext); + + const runTest = (done: (error?: unknown) => void) => { + ctx = context; + const endTest = (error?: unknown) => { + try { + done(error); + } finally { + ctx = originalContext; + } + }; + + let result: unknown; + try { + result = fn(context); + } catch (error) { + endTest(error); + return; + } + if (result instanceof Promise) { + (result as Promise).then(() => endTest()).catch(error => endTest(error)); + } else { + endTest(); + } + }; + + return { name, options, fn: runTest }; +} + +function createDescribe(arg0: unknown, arg1: unknown, arg2: unknown) { + const { name, fn, options } = parseTestOptions(arg0, arg1, arg2); + + const originalContext = ctx; + const context = new TestContext(false, name, ctx.filePath, originalContext); + + const runDescribe = () => { + ctx = context; + const endDescribe = () => { + ctx = originalContext; + }; + + try { + return fn(context); + } finally { + endDescribe(); + } + }; + + return { name, options, fn: runDescribe }; +} + +function parseHookOptions(arg0: unknown, arg1: unknown) { + let fn: HookFn | undefined; + let options: HookOptions; + + if (typeof arg0 === "function") { + fn = arg0 as HookFn; + } else { + fn = kDefaultFunction; + } + + if (typeof arg1 === "object") { + options = arg1 as HookOptions; + } else { + options = kDefaultOptions; + } + + return { fn, options }; +} + +function createHook(arg0: unknown, arg1: unknown) { + const { fn, options } = parseHookOptions(arg0, arg1); + + const runHook = (done: (error?: unknown) => void) => { + let result: unknown; + try { + result = fn(); + } catch (error) { + done(error); + return; + } + if (result instanceof Promise) { + (result as Promise).then(() => done()).catch(error => done(error)); + } else { + done(); + } + }; + + return { options, fn: runHook }; +} + +type TestFn = (ctx: TestContext) => unknown | Promise; +type HookFn = () => unknown | Promise; + +type TestOptions = { + concurrency?: number | boolean | null; + only?: boolean; + signal?: AbortSignal; + skip?: boolean | string; + todo?: boolean | string; + timeout?: number; + plan?: number; +}; + +type HookOptions = { + signal?: AbortSignal; + timeout?: number; +}; + +function setDefaultSnapshotSerializer(serializers: unknown[]) { + throwNotImplemented("setDefaultSnapshotSerializer()", 5090, "Use `bun:test` in the interim."); +} + +function setResolveSnapshotPath(fn: unknown) { + throwNotImplemented("setResolveSnapshotPath()", 5090, "Use `bun:test` in the interim."); +} + +test.describe = describe; +test.suite = describe; +test.test = test; +test.it = test; +test.before = before; +test.after = after; +test.beforeEach = beforeEach; +test.afterEach = afterEach; +test.assert = assert; +test.snapshot = { + setDefaultSnapshotSerializer, + setResolveSnapshotPath, +}; +test.run = run; +test.mock = mock; + +export default test; diff --git a/src/options.zig b/src/options.zig index 913cd801f4..07685a9ba1 100644 --- a/src/options.zig +++ b/src/options.zig @@ -221,6 +221,7 @@ pub const ExternalModules = struct { "stream", "string_decoder", "sys", + "test", "timers", "tls", "trace_events", diff --git a/test/bun.lock b/test/bun.lock index dc59da712d..69a48bf541 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -27,7 +27,7 @@ "bun-plugin-yaml": "0.0.1", "comlink": "4.4.1", "commander": "12.1.0", - "detect-libc": "^2.0.3", + "detect-libc": "2.0.3", "devalue": "5.1.1", "duckdb": "1.1.3", "es-module-lexer": "1.3.0", diff --git a/test/js/node/harness.ts b/test/js/node/harness.ts index 88b4378bf1..f0ea79a79b 100644 --- a/test/js/node/harness.ts +++ b/test/js/node/harness.ts @@ -1,8 +1,6 @@ /** * @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"; @@ -130,11 +128,11 @@ export function createTest(path: string) { }, exact); } - function mustCallAtLeast(fn: AnyFunction, minimum: number) { + function mustCallAtLeast(fn: unknown, minimum: number) { return _mustCallInner(fn, minimum, "minimum"); } - function _mustCallInner(fn: AnyFunction, criteria = 1, field: string) { + function _mustCallInner(fn: unknown, criteria = 1, field: string) { // @ts-ignore if (process._exiting) throw new Error("Cannot use common.mustCall*() in process exit handler"); if (typeof fn === "number") { @@ -266,129 +264,3 @@ export function createTest(path: string) { 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 = {}; - - // @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, - optionsOrFn: Record | Function, - fn?: Function | undefined, - ) { - let options = optionsOrFn; - if (arguments.length === 2) { - assertNode.equal(typeof optionsOrFn, "function", "Second argument to test() must be a function."); - fn = optionsOrFn as Function; - options = {}; - } - if (typeof fn !== "function" && typeof label === "function") { - fn = label; - label = fn.name; - options = {}; - } - - const ctx = getContext(); - const { skip } = options; - - if (skip) return; - 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, maybeFnOrOptions?: Function, maybeFn?: Function) { - const [label, fn] = - typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn ?? maybeFnOrOptions]; - 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, - it: test, - describe, - suite: 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, - }; -} diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index c41ce07ba5..6907b7c177 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -5,7 +5,7 @@ import path from "path"; test("builtinModules exists", () => { expect(Array.isArray(builtinModules)).toBe(true); - expect(builtinModules).toHaveLength(76); + expect(builtinModules).toHaveLength(77); }); test("isBuiltin() works", () => { @@ -17,6 +17,8 @@ test("isBuiltin() works", () => { expect(isBuiltin("events")).toBe(true); expect(isBuiltin("node:events")).toBe(true); expect(isBuiltin("node:bacon")).toBe(false); + expect(isBuiltin("node:test")).toBe(true); + expect(isBuiltin("test")).toBe(false); // "test" does not alias to "node:test" }); test("module.globalPaths exists", () => { diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index 8a2868f0fa..af723f1d50 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -90,20 +90,28 @@ function parseTestFlags(filename = process.argv[1]) { fs.closeSync(fd); const source = buffer.toString('utf8', 0, bytesRead); + const flags = []; const flagStart = source.search(/\/\/ Flags:\s+--/) + 10; + const isNodeTest = source.includes('node:test'); + if (isNodeTest) { + flags.push('test'); + } + if (flagStart === 9) { - return []; + return flags; } let flagEnd = source.indexOf('\n', flagStart); // Normalize different EOL. if (source[flagEnd - 1] === '\r') { flagEnd--; } + return source .substring(flagStart, flagEnd) .split(/\s+/) - .filter(Boolean); + .filter(Boolean) + .concat(flags); } // Check for flags. Skip this for workers (both, the `cluster` module and @@ -130,6 +138,10 @@ if (process.argv.length === 2 && process.env.SKIP_FLAG_CHECK = "1"; break; } + if (flag === "test") { + process.env.SKIP_FLAG_CHECK = "1"; + break; + } console.log( 'NOTE: The test started as a child_process using these flags:', inspect(flags), diff --git a/test/js/node/test/parallel/needs-test/test-url-fileurltopath.js b/test/js/node/test/parallel/needs-test/test-url-fileurltopath.js deleted file mode 100644 index 3b08d06730..0000000000 --- a/test/js/node/test/parallel/needs-test/test-url-fileurltopath.js +++ /dev/null @@ -1,177 +0,0 @@ -'use strict'; -const { isWindows } = require('../../common'); - -const { test } = require('node:test'); -const assert = require('node:assert'); -const url = require('node:url'); - -test('invalid arguments', () => { - for (const arg of [null, undefined, 1, {}, true]) { - assert.throws(() => url.fileURLToPath(arg), { - code: 'ERR_INVALID_ARG_TYPE' - }); - } -}); - -test('input must be a file URL', () => { - assert.throws(() => url.fileURLToPath('https://a/b/c'), { - code: 'ERR_INVALID_URL_SCHEME' - }); -}); - -test('fileURLToPath with host', () => { - const withHost = new URL('file://host/a'); - - if (isWindows) { - assert.strictEqual(url.fileURLToPath(withHost), '\\\\host\\a'); - } else { - assert.throws(() => url.fileURLToPath(withHost), { - code: 'ERR_INVALID_FILE_URL_HOST' - }); - } -}); - -test('fileURLToPath with invalid path', () => { - if (isWindows) { - assert.throws(() => url.fileURLToPath('file:///C:/a%2F/'), { - code: 'ERR_INVALID_FILE_URL_PATH' - }); - assert.throws(() => url.fileURLToPath('file:///C:/a%5C/'), { - code: 'ERR_INVALID_FILE_URL_PATH' - }); - assert.throws(() => url.fileURLToPath('file:///?:/'), { - code: 'ERR_INVALID_FILE_URL_PATH' - }); - } else { - assert.throws(() => url.fileURLToPath('file:///a%2F/'), { - code: 'ERR_INVALID_FILE_URL_PATH' - }); - } -}); - -const windowsTestCases = [ - // Lowercase ascii alpha - { path: 'C:\\foo', fileURL: 'file:///C:/foo' }, - // Uppercase ascii alpha - { path: 'C:\\FOO', fileURL: 'file:///C:/FOO' }, - // dir - { path: 'C:\\dir\\foo', fileURL: 'file:///C:/dir/foo' }, - // trailing separator - { path: 'C:\\dir\\', fileURL: 'file:///C:/dir/' }, - // dot - { path: 'C:\\foo.mjs', fileURL: 'file:///C:/foo.mjs' }, - // space - { path: 'C:\\foo bar', fileURL: 'file:///C:/foo%20bar' }, - // question mark - { path: 'C:\\foo?bar', fileURL: 'file:///C:/foo%3Fbar' }, - // number sign - { path: 'C:\\foo#bar', fileURL: 'file:///C:/foo%23bar' }, - // ampersand - { path: 'C:\\foo&bar', fileURL: 'file:///C:/foo&bar' }, - // equals - { path: 'C:\\foo=bar', fileURL: 'file:///C:/foo=bar' }, - // colon - { path: 'C:\\foo:bar', fileURL: 'file:///C:/foo:bar' }, - // semicolon - { path: 'C:\\foo;bar', fileURL: 'file:///C:/foo;bar' }, - // percent - { path: 'C:\\foo%bar', fileURL: 'file:///C:/foo%25bar' }, - // backslash - { path: 'C:\\foo\\bar', fileURL: 'file:///C:/foo/bar' }, - // backspace - { path: 'C:\\foo\bbar', fileURL: 'file:///C:/foo%08bar' }, - // tab - { path: 'C:\\foo\tbar', fileURL: 'file:///C:/foo%09bar' }, - // newline - { path: 'C:\\foo\nbar', fileURL: 'file:///C:/foo%0Abar' }, - // carriage return - { path: 'C:\\foo\rbar', fileURL: 'file:///C:/foo%0Dbar' }, - // latin1 - { path: 'C:\\fóóbàr', fileURL: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' }, - // Euro sign (BMP code point) - { path: 'C:\\€', fileURL: 'file:///C:/%E2%82%AC' }, - // Rocket emoji (non-BMP code point) - { path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' }, - // UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows) - { path: '\\\\nas\\My Docs\\File.doc', fileURL: 'file://nas/My%20Docs/File.doc' }, -]; - -const posixTestCases = [ - // Lowercase ascii alpha - { path: '/foo', fileURL: 'file:///foo' }, - // Uppercase ascii alpha - { path: '/FOO', fileURL: 'file:///FOO' }, - // dir - { path: '/dir/foo', fileURL: 'file:///dir/foo' }, - // trailing separator - { path: '/dir/', fileURL: 'file:///dir/' }, - // dot - { path: '/foo.mjs', fileURL: 'file:///foo.mjs' }, - // space - { path: '/foo bar', fileURL: 'file:///foo%20bar' }, - // question mark - { path: '/foo?bar', fileURL: 'file:///foo%3Fbar' }, - // number sign - { path: '/foo#bar', fileURL: 'file:///foo%23bar' }, - // ampersand - { path: '/foo&bar', fileURL: 'file:///foo&bar' }, - // equals - { path: '/foo=bar', fileURL: 'file:///foo=bar' }, - // colon - { path: '/foo:bar', fileURL: 'file:///foo:bar' }, - // semicolon - { path: '/foo;bar', fileURL: 'file:///foo;bar' }, - // percent - { path: '/foo%bar', fileURL: 'file:///foo%25bar' }, - // backslash - { path: '/foo\\bar', fileURL: 'file:///foo%5Cbar' }, - // backspace - { path: '/foo\bbar', fileURL: 'file:///foo%08bar' }, - // tab - { path: '/foo\tbar', fileURL: 'file:///foo%09bar' }, - // newline - { path: '/foo\nbar', fileURL: 'file:///foo%0Abar' }, - // carriage return - { path: '/foo\rbar', fileURL: 'file:///foo%0Dbar' }, - // latin1 - { path: '/fóóbàr', fileURL: 'file:///f%C3%B3%C3%B3b%C3%A0r' }, - // Euro sign (BMP code point) - { path: '/€', fileURL: 'file:///%E2%82%AC' }, - // Rocket emoji (non-BMP code point) - { path: '/🚀', fileURL: 'file:///%F0%9F%9A%80' }, -]; - -test('fileURLToPath with windows path', { skip: !isWindows }, () => { - - for (const { path, fileURL } of windowsTestCases) { - const fromString = url.fileURLToPath(fileURL, { windows: true }); - assert.strictEqual(fromString, path); - const fromURL = url.fileURLToPath(new URL(fileURL), { windows: true }); - assert.strictEqual(fromURL, path); - } -}); - -test('fileURLToPath with posix path', { skip: isWindows }, () => { - for (const { path, fileURL } of posixTestCases) { - const fromString = url.fileURLToPath(fileURL, { windows: false }); - assert.strictEqual(fromString, path); - const fromURL = url.fileURLToPath(new URL(fileURL), { windows: false }); - assert.strictEqual(fromURL, path); - } -}); - -const defaultTestCases = isWindows ? windowsTestCases : posixTestCases; - -test('options is null', () => { - const whenNullActual = url.fileURLToPath(new URL(defaultTestCases[0].fileURL), null); - assert.strictEqual(whenNullActual, defaultTestCases[0].path); -}); - -test('defaultTestCases', () => { - for (const { path, fileURL } of defaultTestCases) { - const fromString = url.fileURLToPath(fileURL); - assert.strictEqual(fromString, path); - const fromURL = url.fileURLToPath(new URL(fileURL)); - assert.strictEqual(fromURL, path); - } -}); diff --git a/test/js/node/test/parallel/test-arm-math-illegal-instruction.js b/test/js/node/test/parallel/test-arm-math-illegal-instruction.js index 4bf881d1b3..c4a6ec01ba 100644 --- a/test/js/node/test/parallel/test-arm-math-illegal-instruction.js +++ b/test/js/node/test/parallel/test-arm-math-illegal-instruction.js @@ -1,5 +1,6 @@ 'use strict'; require('../common'); +const { test } = require('node:test'); // This test ensures Math functions don't fail with an "illegal instruction" // error on ARM devices (primarily on the Raspberry Pi 1) @@ -7,9 +8,11 @@ require('../common'); // and https://code.google.com/p/v8/issues/detail?id=4019 // Iterate over all Math functions -Object.getOwnPropertyNames(Math).forEach((functionName) => { - if (!/[A-Z]/.test(functionName)) { - // The function names don't have capital letters. - Math[functionName](-0.5); - } +test('Iterate over all Math functions', () => { + Object.getOwnPropertyNames(Math).forEach((functionName) => { + if (!/[A-Z]/.test(functionName)) { + // The function names don't have capital letters. + Math[functionName](-0.5); + } + }); }); diff --git a/test/js/node/test/parallel/needs-test/test-assert-calltracker-getCalls.js b/test/js/node/test/parallel/test-assert-calltracker-getCalls.js similarity index 98% rename from test/js/node/test/parallel/needs-test/test-assert-calltracker-getCalls.js rename to test/js/node/test/parallel/test-assert-calltracker-getCalls.js index 5332be4abf..9ba9a8fe52 100644 --- a/test/js/node/test/parallel/needs-test/test-assert-calltracker-getCalls.js +++ b/test/js/node/test/parallel/test-assert-calltracker-getCalls.js @@ -1,5 +1,5 @@ 'use strict'; -require('../../common'); +require('../common'); const assert = require('assert'); const { describe, it } = require('node:test'); diff --git a/test/js/node/test/parallel/needs-test/test-assert-checktag.js b/test/js/node/test/parallel/test-assert-checktag.js similarity index 98% rename from test/js/node/test/parallel/needs-test/test-assert-checktag.js rename to test/js/node/test/parallel/test-assert-checktag.js index 04f78f737a..b86a1bde7f 100644 --- a/test/js/node/test/parallel/needs-test/test-assert-checktag.js +++ b/test/js/node/test/parallel/test-assert-checktag.js @@ -1,5 +1,5 @@ 'use strict'; -const { hasCrypto } = require('../../common'); +const { hasCrypto } = require('../common'); const { test } = require('node:test'); const assert = require('assert'); diff --git a/test/js/node/test/parallel/needs-test/test-assert-deep-with-error.js b/test/js/node/test/parallel/test-assert-deep-with-error.js similarity index 99% rename from test/js/node/test/parallel/needs-test/test-assert-deep-with-error.js rename to test/js/node/test/parallel/test-assert-deep-with-error.js index 125d16cc93..f6bc5c6359 100644 --- a/test/js/node/test/parallel/needs-test/test-assert-deep-with-error.js +++ b/test/js/node/test/parallel/test-assert-deep-with-error.js @@ -1,5 +1,5 @@ 'use strict'; -require('../../common'); +require('../common'); const assert = require('assert'); const { test } = require('node:test'); diff --git a/test/js/node/test/parallel/needs-test/test-assert-esm-cjs-message-verify.js b/test/js/node/test/parallel/test-assert-esm-cjs-message-verify.js similarity index 94% rename from test/js/node/test/parallel/needs-test/test-assert-esm-cjs-message-verify.js rename to test/js/node/test/parallel/test-assert-esm-cjs-message-verify.js index fa5cdc0340..23c4880092 100644 --- a/test/js/node/test/parallel/needs-test/test-assert-esm-cjs-message-verify.js +++ b/test/js/node/test/parallel/test-assert-esm-cjs-message-verify.js @@ -1,6 +1,6 @@ 'use strict'; -const { spawnPromisified } = require('../../common'); +const { spawnPromisified } = require('../common'); const assert = require('node:assert'); const { describe, it } = require('node:test'); diff --git a/test/js/node/test/parallel/test-assert-fail-deprecation.js b/test/js/node/test/parallel/test-assert-fail-deprecation.js new file mode 100644 index 0000000000..ab31b08f95 --- /dev/null +++ b/test/js/node/test/parallel/test-assert-fail-deprecation.js @@ -0,0 +1,70 @@ +// Flags: --no-warnings +'use strict'; + +const { expectWarning } = require('../common'); +const assert = require('assert'); +const { test } = require('node:test'); + +expectWarning( + 'DeprecationWarning', + 'assert.fail() with more than one argument is deprecated. ' + + 'Please use assert.strictEqual() instead or only pass a message.', + 'DEP0094' +); + +test('Two args only, operator defaults to "!="', () => { + assert.throws(() => { + assert.fail('first', 'second'); + }, { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: '\'first\' != \'second\'', + operator: '!=', + actual: 'first', + expected: 'second', + generatedMessage: true + }); +}); + +test('Three args', () => { + assert.throws(() => { + assert.fail('ignored', 'ignored', 'another custom message'); + }, { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'another custom message', + operator: 'fail', + actual: 'ignored', + expected: 'ignored', + generatedMessage: false + }); +}); + +test('Three args with custom Error', () => { + assert.throws(() => { + assert.fail(typeof 1, 'object', new TypeError('another custom message')); + }, { + name: 'TypeError', + message: 'another custom message' + }); +}); + +test('No third arg (but a fourth arg)', () => { + assert.throws(() => { + assert.fail('first', 'second', undefined, 'operator'); + }, { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: '\'first\' operator \'second\'', + operator: 'operator', + actual: 'first', + expected: 'second' + }); +}); + +test('The stackFrameFunction should exclude the foo frame', () => { + assert.throws( + function foo() { assert.fail('first', 'second', 'message', '!==', foo); }, + (err) => !/^\s*at\sfoo\b/m.test(err.stack) + ); +}); diff --git a/test/js/node/test/parallel/needs-test/test-assert-fail.js b/test/js/node/test/parallel/test-assert-fail.js similarity index 97% rename from test/js/node/test/parallel/needs-test/test-assert-fail.js rename to test/js/node/test/parallel/test-assert-fail.js index 1d9fbc5784..3211e438a3 100644 --- a/test/js/node/test/parallel/needs-test/test-assert-fail.js +++ b/test/js/node/test/parallel/test-assert-fail.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../common'); +require('../common'); const assert = require('assert'); const { test } = require('node:test'); diff --git a/test/js/node/test/parallel/needs-test/test-assert-if-error.js b/test/js/node/test/parallel/test-assert-if-error.js similarity index 98% rename from test/js/node/test/parallel/needs-test/test-assert-if-error.js rename to test/js/node/test/parallel/test-assert-if-error.js index 32d7456bae..e5b11c9b90 100644 --- a/test/js/node/test/parallel/needs-test/test-assert-if-error.js +++ b/test/js/node/test/parallel/test-assert-if-error.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../common'); +require('../common'); const assert = require('assert'); const { test } = require('node:test'); diff --git a/test/js/node/test/parallel/needs-test/test-assert.js b/test/js/node/test/parallel/test-assert.js similarity index 99% rename from test/js/node/test/parallel/needs-test/test-assert.js rename to test/js/node/test/parallel/test-assert.js index aa302f1f74..678b6ec1e5 100644 --- a/test/js/node/test/parallel/needs-test/test-assert.js +++ b/test/js/node/test/parallel/test-assert.js @@ -23,9 +23,9 @@ const assert = require('node:assert'); -require('../../../harness'); +require('../../harness'); -const {invalidArgTypeHelper} = require('../../common'); +const {invalidArgTypeHelper} = require('../common'); const {inspect} = require('util'); const {test} = require('node:test'); const vm = require('vm'); diff --git a/test/js/node/test/parallel/test-buffer-resizable.js b/test/js/node/test/parallel/test-buffer-resizable.js index 6c31ba5ec8..dcfe6385b6 100644 --- a/test/js/node/test/parallel/test-buffer-resizable.js +++ b/test/js/node/test/parallel/test-buffer-resizable.js @@ -4,39 +4,26 @@ require('../common'); const { Buffer } = require('node:buffer'); const { strictEqual } = require('node:assert'); +const { describe, it } = require('node:test'); -{ - { +describe('Using resizable ArrayBuffer with Buffer...', () => { + it('works as expected', () => { const ab = new ArrayBuffer(10, { maxByteLength: 20 }); const buffer = Buffer.from(ab, 1); - strictEqual(ab.byteLength, 10); - strictEqual(buffer.buffer.byteLength, 10); strictEqual(buffer.byteLength, 9); ab.resize(15); - strictEqual(ab.byteLength, 15); - strictEqual(buffer.buffer.byteLength, 15); strictEqual(buffer.byteLength, 14); ab.resize(5); - strictEqual(ab.byteLength, 5); - strictEqual(buffer.buffer.byteLength, 5); strictEqual(buffer.byteLength, 4); - } -} + }); -{ - { + it('works with the deprecated constructor also', () => { const ab = new ArrayBuffer(10, { maxByteLength: 20 }); const buffer = new Buffer(ab, 1); - strictEqual(ab.byteLength, 10); - strictEqual(buffer.buffer.byteLength, 10); strictEqual(buffer.byteLength, 9); ab.resize(15); - strictEqual(ab.byteLength, 15); - strictEqual(buffer.buffer.byteLength, 15); strictEqual(buffer.byteLength, 14); ab.resize(5); - strictEqual(ab.byteLength, 5); - strictEqual(buffer.buffer.byteLength, 5); strictEqual(buffer.byteLength, 4); - } -} + }); +}); diff --git a/test/js/node/test/parallel/test-file-write-stream5.js b/test/js/node/test/parallel/test-file-write-stream5.js new file mode 100644 index 0000000000..cdc8b52eeb --- /dev/null +++ b/test/js/node/test/parallel/test-file-write-stream5.js @@ -0,0 +1,28 @@ +'use strict'; + +// Test 'uncork' for WritableStream. +// Refs: https://github.com/nodejs/node/issues/50979 + +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const test = require('node:test'); +const tmpdir = require('../common/tmpdir'); + +const filepath = tmpdir.resolve('write_stream.txt'); +tmpdir.refresh(); + +const data = 'data'; + +test('writable stream uncork', () => { + const fileWriteStream = fs.createWriteStream(filepath); + + fileWriteStream.on('finish', common.mustCall(() => { + const writtenData = fs.readFileSync(filepath, 'utf8'); + assert.strictEqual(writtenData, data); + })); + fileWriteStream.cork(); + fileWriteStream.write(data, common.mustCall()); + fileWriteStream.uncork(); + fileWriteStream.end(); +}); diff --git a/test/js/node/test/parallel/test-fs-operations-with-surrogate-pairs.js b/test/js/node/test/parallel/test-fs-operations-with-surrogate-pairs.js index 330741c2b0..d46623e8c2 100644 --- a/test/js/node/test/parallel/test-fs-operations-with-surrogate-pairs.js +++ b/test/js/node/test/parallel/test-fs-operations-with-surrogate-pairs.js @@ -4,7 +4,7 @@ require('../common'); const fs = require('node:fs'); const path = require('node:path'); const assert = require('node:assert'); -const { describe, it } = require('bun:test'); +const { describe, it } = require('node:test'); const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); diff --git a/test/js/node/test/parallel/test-runner-aliases.js b/test/js/node/test/parallel/test-runner-aliases.js new file mode 100644 index 0000000000..1a61da896e --- /dev/null +++ b/test/js/node/test/parallel/test-runner-aliases.js @@ -0,0 +1,8 @@ +'use strict'; +require('../common'); +const { strictEqual } = require('node:assert'); +const test = require('node:test'); + +strictEqual(test.test, test); +strictEqual(test.it, test); +strictEqual(test.describe, test.suite); diff --git a/test/js/node/test/parallel/test-runner-assert.js b/test/js/node/test/parallel/test-runner-assert.js new file mode 100644 index 0000000000..dbbf52f09d --- /dev/null +++ b/test/js/node/test/parallel/test-runner-assert.js @@ -0,0 +1,21 @@ +'use strict'; +require('../common'); +const assert = require('node:assert'); +const test = require('node:test'); + +test('expected methods are on t.assert', (t) => { + const uncopiedKeys = [ + 'AssertionError', + 'CallTracker', + 'strict', + ]; + const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key)); + const expectedKeys = ['snapshot', 'fileSnapshot'].concat(assertKeys).sort(); + assert.deepStrictEqual(Object.keys(t.assert).sort(), expectedKeys); +}); + +test('t.assert.ok correctly parses the stacktrace', (t) => { + // FIXME: AssertionError message is incorrect + // t.assert.throws(() => t.assert.ok(1 === 2), /t\.assert\.ok\(1 === 2\)/); + t.assert.throws(() => t.assert.ok(1 === 2)); +}); diff --git a/test/js/node/test/parallel/test-runner-root-after-with-refed-handles.js b/test/js/node/test/parallel/test-runner-root-after-with-refed-handles.js new file mode 100644 index 0000000000..2149c2dba2 --- /dev/null +++ b/test/js/node/test/parallel/test-runner-root-after-with-refed-handles.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +const { before, after, test } = require('node:test'); +const { createServer } = require('node:http'); + +let server; + +before(common.mustCall(() => { + server = createServer(); + + return new Promise(common.mustCall((resolve, reject) => { + server.listen(0, common.mustCall((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + })); + })); +})); + +after(common.mustCall(() => { + server.close(common.mustCall()); +})); + +test(); diff --git a/test/js/node/test/parallel/test-runner-subtest-after-hook.js b/test/js/node/test/parallel/test-runner-subtest-after-hook.js new file mode 100644 index 0000000000..f288abee24 --- /dev/null +++ b/test/js/node/test/parallel/test-runner-subtest-after-hook.js @@ -0,0 +1,12 @@ +'use strict'; +const common = require('../common'); +const { test } = require('node:test'); + +// Regression test for https://github.com/nodejs/node/issues/51997 +test('after hook should be called with no subtests', (t) => { + const timer = setTimeout(common.mustNotCall(), 2 ** 30); + + t.after(common.mustCall(() => { + clearTimeout(timer); + })); +}); diff --git a/test/js/node/test/parallel/test-runner-typechecking.js b/test/js/node/test/parallel/test-runner-typechecking.js new file mode 100644 index 0000000000..8568b4cb39 --- /dev/null +++ b/test/js/node/test/parallel/test-runner-typechecking.js @@ -0,0 +1,26 @@ +'use strict'; +require('../common'); + +// Return type of shorthands should be consistent +// with the return type of test + +const assert = require('assert'); +const { test, describe, it } = require('node:test'); + +const testOnly = test('only test', { only: true }); +const testTodo = test('todo test', { todo: true }); +const testSkip = test('skip test', { skip: true }); +const testOnlyShorthand = test.only('only test shorthand'); +const testTodoShorthand = test.todo('todo test shorthand'); +const testSkipShorthand = test.skip('skip test shorthand'); + +describe('\'node:test\' and its shorthands should return the same', () => { + it('should return undefined', () => { + assert.strictEqual(testOnly, undefined); + assert.strictEqual(testTodo, undefined); + assert.strictEqual(testSkip, undefined); + assert.strictEqual(testOnlyShorthand, undefined); + assert.strictEqual(testTodoShorthand, undefined); + assert.strictEqual(testSkipShorthand, undefined); + }); +}); diff --git a/test/js/node/test/parallel/needs-test/test-socketaddress.js b/test/js/node/test/parallel/test-socketaddress.js similarity index 99% rename from test/js/node/test/parallel/needs-test/test-socketaddress.js rename to test/js/node/test/parallel/test-socketaddress.js index 2b59d46945..8cfa0f8e84 100644 --- a/test/js/node/test/parallel/needs-test/test-socketaddress.js +++ b/test/js/node/test/parallel/test-socketaddress.js @@ -1,7 +1,7 @@ // Flags: --expose-internals 'use strict'; -const common = require('../../common'); +const common = require('../common'); const { ok, strictEqual, diff --git a/test/js/node/test/parallel/needs-test/test-url-format-invalid-input.js b/test/js/node/test/parallel/test-url-format-invalid-input.js similarity index 96% rename from test/js/node/test/parallel/needs-test/test-url-format-invalid-input.js rename to test/js/node/test/parallel/test-url-format-invalid-input.js index 451c45aed5..ac3d28c841 100644 --- a/test/js/node/test/parallel/needs-test/test-url-format-invalid-input.js +++ b/test/js/node/test/parallel/test-url-format-invalid-input.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../common'); +require('../common'); const assert = require('node:assert'); const url = require('node:url'); diff --git a/test/js/node/test/parallel/test-whatwg-writablestream-close.js b/test/js/node/test/parallel/test-whatwg-writablestream-close.js new file mode 100644 index 0000000000..1b987cf0c4 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-writablestream-close.js @@ -0,0 +1,29 @@ +'use strict'; + +require('../common'); +const { test } = require('node:test'); +const assert = require('node:assert'); + +// https://github.com/nodejs/node/issues/57272 + +test('should throw error when writing after close', async (t) => { + const writable = new WritableStream({ + write(chunk) { + console.log(chunk); + }, + }); + + const writer = writable.getWriter(); + + await writer.write('Hello'); + await writer.close(); + + await assert.rejects( + async () => { + await writer.write('World'); + }, + { + name: 'TypeError', + } + ); +}); diff --git a/test/js/node/test_runner/fixtures/01-harness.js b/test/js/node/test_runner/fixtures/01-harness.js new file mode 100644 index 0000000000..29f4584115 --- /dev/null +++ b/test/js/node/test_runner/fixtures/01-harness.js @@ -0,0 +1,146 @@ +const test = require("node:test"); +const assert = require("node:assert"); + +test("test() is a function", () => { + assert(typeof test === "function", "test() is a function"); +}); + +test("describe() is a function", () => { + assert(typeof test.describe === "function", "describe() is a function"); +}); + +test.describe("TestContext", () => { + test("", t => { + t.assert.ok(typeof t === "object", "test() returns an object"); + }); + + test("name", t => { + t.assert.equal(t.name, "name"); // matches the name of the test + }); + + test("filePath", t => { + t.assert.equal(t.filePath, __filename); + }); + + test("signal", t => { + t.assert.ok(t.signal instanceof AbortSignal); + }); + + test("assert", t => { + t.assert.ok(typeof t.assert === "object", "test() argument has an assert property"); + const actual = Object.keys(t.assert).sort(); + const expected = Object.keys({ ...assert }) + .filter(key => !["CallTracker", "AssertionError", "strict"].includes(key)) + .concat(["fileSnapshot", "snapshot"]) + .sort(); + t.assert.deepEqual(actual, expected, "test() argument is the same as the node:assert module"); + }); + + test("diagnostic()", t => { + t.assert.ok(typeof t.diagnostic === "function", "diagnostic() is a function"); + }); + + test("before()", t => { + t.assert.ok(typeof t.before === "function", "before() is a function"); + }); + + test("after()", t => { + t.assert.ok(typeof t.after === "function", "after() is a function"); + }); + + test("beforeEach()", t => { + t.assert.ok(typeof t.beforeEach === "function", "beforeEach() is a function"); + }); + + test("afterEach()", t => { + t.assert.ok(typeof t.afterEach === "function", "afterEach() is a function"); + }); + + test("test()", t => { + t.assert.ok(typeof t.test === "function", "test() method is a function"); + }); +}); + +test("before() is a function", t => { + t.assert.ok(typeof test.before === "function", "before() is a function"); +}); + +test("after() is a function", t => { + t.assert.ok(typeof test.after === "function", "after() is a function"); +}); + +test("beforeEach() is a function", t => { + t.assert.ok(typeof test.beforeEach === "function", "beforeEach() is a function"); +}); + +test("afterEach() is a function", t => { + t.assert.ok(typeof test.afterEach === "function", "afterEach() is a function"); +}); + +test.describe("test", () => { + test("test()", t => { + t.assert.ok(typeof test === "function", "test() is a function"); + }); + + test("it()", t => { + t.assert.ok(typeof test.it === "function", "test.it() is a function"); + }); + + test("skip()", t => { + t.assert.ok(typeof test.skip === "function", "test.skip() is a function"); + }); + + test("todo()", t => { + t.assert.ok(typeof test.todo === "function", "test.todo() is a function"); + }); + + test("only()", t => { + t.assert.ok(typeof test.only === "function", "test.only() is a function"); + }); + + test("describe()", t => { + t.assert.ok(typeof test.describe === "function", "test.describe() is a function"); + }); + + test("suite()", t => { + t.assert.ok(typeof test.suite === "function", "test.suite() is a function"); + }); +}); + +test.describe("describe", () => { + test("", t => { + t.assert.ok(typeof test.describe === "function", "describe() is a function"); + }); + + test("skip()", t => { + t.assert.ok(typeof test.describe.skip === "function", "describe.skip() is a function"); + }); + + test("todo()", t => { + t.assert.ok(typeof test.describe.todo === "function", "describe.todo() is a function"); + }); + + test("only()", t => { + t.assert.ok(typeof test.describe.only === "function", "describe.only() is a function"); + }); +}); + +test.describe("describe 1", t => { + test("name is correct", t => { + t.assert.equal(t.name, "name is correct"); + }); + + test("fullName is correct", t => { + t.assert.equal(t.fullName, "describe 1 > fullName is correct"); + }); + + test.describe("describe 2", () => { + test("name is correct", t => { + t.assert.equal(t.name, "name is correct"); + }); + + test("fullName is correct", t => { + t.assert.equal(t.fullName, "describe 1 > describe 2 > fullName is correct"); + }); + }); +}); diff --git a/test/js/node/test_runner/fixtures/02-hooks.js b/test/js/node/test_runner/fixtures/02-hooks.js new file mode 100644 index 0000000000..ad316d36b9 --- /dev/null +++ b/test/js/node/test_runner/fixtures/02-hooks.js @@ -0,0 +1,136 @@ +const { describe, test, before, after, beforeEach, afterEach } = require("node:test"); +const { readFileSync } = require("node:fs"); +const { join } = require("node:path"); +const assert = require("node:assert"); + +const expectedFile = readFileSync(join(__dirname, "02-hooks.json"), "utf-8"); +const { node, bun } = JSON.parse(expectedFile); +const order = []; + +before(() => { + order.push("before global"); +}); + +before(async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + order.push("before global async"); +}); + +after(() => { + order.push("after global"); +}); + +after(async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + order.push("after global async"); +}); + +beforeEach(() => { + order.push("beforeEach global"); +}); + +beforeEach(async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + order.push("beforeEach global async"); +}); + +afterEach(() => { + order.push("afterEach global"); +}); + +afterEach(async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + order.push("afterEach global async"); +}); + +describe("execution order", () => { + before(() => { + order.push("before"); + }); + + before(async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + order.push("before"); + }); + + beforeEach(() => { + order.push("beforeEach"); + }); + + beforeEach(async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + order.push("beforeEach"); + }); + + afterEach(() => { + order.push("afterEach"); + }); + + afterEach(async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + order.push("afterEach"); + }); + + after(() => { + order.push("after"); + }); + + after(async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + order.push("after"); + }); + + test("test 1", ({ fullName }) => { + order.push(`test: ${fullName}`); + }); + + describe("describe 1", () => { + before(() => { + order.push("before > describe 1"); + }); + + beforeEach(() => { + order.push("beforeEach > describe 1"); + }); + + afterEach(() => { + order.push("afterEach > describe 1"); + }); + + after(() => { + order.push("after > describe 1"); + }); + + test("test 2", ({ fullName }) => { + order.push(`test: ${fullName}`); + }); + + describe("describe 2", () => { + before(() => { + order.push("before > describe 2"); + }); + + beforeEach(() => { + order.push("beforeEach > describe 2"); + }); + + afterEach(() => { + order.push("afterEach > describe 2"); + }); + + after(() => { + order.push("after > describe 2"); + }); + + test("test 3", ({ fullName }) => { + order.push(`test: ${fullName}`); + }); + }); + }); +}); + +after(() => { + // FIXME: Due to subtle differences between how Node.js and Bun (using `bun test`) run tests, + // this is a snapshot test. You must look at the snapshot to verify the output makes sense. + assert.deepEqual(order, "Bun" in globalThis ? bun : node); +}); diff --git a/test/js/node/test_runner/fixtures/02-hooks.json b/test/js/node/test_runner/fixtures/02-hooks.json new file mode 100644 index 0000000000..bac602ac8e --- /dev/null +++ b/test/js/node/test_runner/fixtures/02-hooks.json @@ -0,0 +1,96 @@ +{ + "node": [ + "before global", + "before global async", + "before", + "before", + "beforeEach global", + "beforeEach global async", + "beforeEach", + "beforeEach", + "test: execution order > test 1", + "afterEach", + "afterEach", + "afterEach global", + "afterEach global async", + "before > describe 1", + "beforeEach global", + "beforeEach global async", + "beforeEach", + "beforeEach", + "beforeEach > describe 1", + "test: execution order > describe 1 > test 2", + "afterEach > describe 1", + "afterEach", + "afterEach", + "afterEach global", + "afterEach global async", + "before > describe 2", + "beforeEach global", + "beforeEach global async", + "beforeEach", + "beforeEach", + "beforeEach > describe 1", + "beforeEach > describe 2", + "test: execution order > describe 1 > describe 2 > test 3", + "afterEach > describe 2", + "afterEach > describe 1", + "afterEach", + "afterEach", + "afterEach global", + "afterEach global async", + "after > describe 2", + "after > describe 1", + "after", + "after", + "after global", + "after global async" + ], + "bun": [ + "before global", + "before global async", + "before", + "before", + "before > describe 1", + "before > describe 2", + "beforeEach global", + "beforeEach global async", + "beforeEach", + "beforeEach", + "beforeEach > describe 1", + "beforeEach > describe 2", + "test: execution order > describe 1 > describe 2 > test 3", + "afterEach > describe 2", + "afterEach > describe 1", + "afterEach", + "afterEach", + "afterEach global", + "afterEach global async", + "after > describe 2", + "beforeEach global", + "beforeEach global async", + "beforeEach", + "beforeEach", + "beforeEach > describe 1", + "test: execution order > describe 1 > test 2", + "afterEach > describe 1", + "afterEach", + "afterEach", + "afterEach global", + "afterEach global async", + "after > describe 1", + "beforeEach global", + "beforeEach global async", + "beforeEach", + "beforeEach", + "test: execution order > test 1", + "afterEach", + "afterEach", + "afterEach global", + "afterEach global async", + "after", + "after", + "after global", + "after global async" + ] +} diff --git a/test/js/node/test_runner/fixtures/03-test-variations.js b/test/js/node/test_runner/fixtures/03-test-variations.js new file mode 100644 index 0000000000..bb60b8d599 --- /dev/null +++ b/test/js/node/test_runner/fixtures/03-test-variations.js @@ -0,0 +1,55 @@ +const test = require("node:test"); + +test(); // test without name or callback + +test("test with name and callback", t => { + t.assert.ok(true); +}); + +test("test with name, options, and callback", { timeout: 5000 }, t => { + t.assert.ok(true); +}); + +test(t => { + t.assert.equal(t.name, ""); +}); + +test(function testWithFunctionName(t) { + t.assert.equal(t.name, "testWithFunctionName"); +}); + +test({ timeout: 5000 }, t => { + t.assert.equal(t.name, ""); +}); + +test.describe("describe with name and callback", () => { + test("nested test", t => { + t.assert.ok(true); + }); +}); + +test.describe("describe with name, options, and callback", { timeout: 5000 }, () => { + test("nested test", t => { + t.assert.ok(true); + }); +}); + +test.skip("skipped test", t => { + t.assert.fail("This test should be skipped"); +}); + +test.skip("skipped test with options", { timeout: 5000 }, t => { + t.assert.fail("This test should be skipped"); +}); + +test.todo("todo test"); + +test.todo("todo test with options", { timeout: 5000 }); + +test.describe.skip("skipped describe", () => { + test("nested test", t => { + t.assert.fail("This test should be skipped"); + }); +}); + +test.describe.todo("todo describe"); diff --git a/test/js/node/test_runner/fixtures/04-async-tests.js b/test/js/node/test_runner/fixtures/04-async-tests.js new file mode 100644 index 0000000000..faaeb5f21e --- /dev/null +++ b/test/js/node/test_runner/fixtures/04-async-tests.js @@ -0,0 +1,54 @@ +const { describe, test, after } = require("node:test"); +const assert = require("node:assert"); + +let callCount = 0; + +function mustCall(fn) { + try { + fn(); + } finally { + callCount++; + } +} + +test( + "test with an async function", + mustCall(async () => { + const result = await Promise.resolve(42); + assert.equal(result, 42); + }), +); + +test( + "test with an async function that delays", + mustCall(async () => { + const start = Date.now(); + await new Promise(resolve => setTimeout(resolve, 100)); + const end = Date.now(); + assert.ok(end - start > 10, "should wait at least 10ms"); + }), +); + +describe("nested tests", () => { + test( + "nested test with an async function", + mustCall(async () => { + const result = await Promise.resolve(42); + assert.equal(result, 42); + }), + ); + + test( + "nested test with an async function that delays", + mustCall(async () => { + const start = Date.now(); + await new Promise(resolve => setTimeout(resolve, 100)); + const end = Date.now(); + assert.ok(end - start > 10, "should wait at least 10ms"); + }), + ); +}); + +after(() => { + assert.equal(callCount, 4); +}); diff --git a/test/js/node/test_runner/node-test.test.ts b/test/js/node/test_runner/node-test.test.ts new file mode 100644 index 0000000000..f8341e223b --- /dev/null +++ b/test/js/node/test_runner/node-test.test.ts @@ -0,0 +1,57 @@ +import { describe, test, expect } from "bun:test"; +import { spawn } from "bun"; +import { join } from "node:path"; +import { bunEnv, bunExe } from "harness"; + +describe("node:test", () => { + test("should run basic tests", async () => { + const { exitCode, stderr } = await runTest("01-harness.js"); + expect({ exitCode, stderr }).toMatchObject({ + exitCode: 0, + stderr: expect.stringContaining("0 fail"), + }); + }); + + test("should run hooks in the right order", async () => { + const { exitCode, stderr } = await runTest("02-hooks.js"); + expect({ exitCode, stderr }).toMatchObject({ + exitCode: 0, + stderr: expect.stringContaining("0 fail"), + }); + }); + + test("should run tests with different variations", async () => { + const { exitCode, stderr } = await runTest("03-test-variations.js"); + expect({ exitCode, stderr }).toMatchObject({ + exitCode: 0, + stderr: expect.stringContaining("0 fail"), + }); + }); + + test("should run async tests", async () => { + const { exitCode, stderr } = await runTest("04-async-tests.js"); + expect({ exitCode, stderr }).toMatchObject({ + exitCode: 0, + stderr: expect.stringContaining("0 fail"), + }); + }); +}); + +async function runTest(filename: string) { + const testPath = join(import.meta.dirname, "fixtures", filename); + const { + exited, + stdout: stdoutStream, + stderr: stderrStream, + } = spawn({ + cmd: [bunExe(), "test", testPath], + env: bunEnv, + stderr: "pipe", + }); + const [exitCode, stdout, stderr] = await Promise.all([ + exited, + new Response(stdoutStream).text(), + new Response(stderrStream).text(), + ]); + return { exitCode, stdout, stderr }; +}