diff options
-rw-r--r-- | cli/tests/integration/node_compat_tests.rs | 6 | ||||
-rw-r--r-- | cli/tests/testdata/node/test.js | 383 | ||||
-rw-r--r-- | cli/tests/testdata/node/test.out | 176 | ||||
-rw-r--r-- | ext/node/lib.rs | 1 | ||||
-rw-r--r-- | ext/node/polyfill.rs | 1 | ||||
-rw-r--r-- | ext/node/polyfills/01_require.js | 2 | ||||
-rw-r--r-- | ext/node/polyfills/testing.ts | 237 |
7 files changed, 806 insertions, 0 deletions
diff --git a/cli/tests/integration/node_compat_tests.rs b/cli/tests/integration/node_compat_tests.rs index b5fd7b1b3..29d70708e 100644 --- a/cli/tests/integration/node_compat_tests.rs +++ b/cli/tests/integration/node_compat_tests.rs @@ -17,3 +17,9 @@ fn node_compat_tests() { assert_eq!(Some(0), status.code()); assert!(status.success()); } + +itest!(node_test_module { + args: "test node/test.js", + output: "node/test.out", + exit_code: 1, +}); diff --git a/cli/tests/testdata/node/test.js b/cli/tests/testdata/node/test.js new file mode 100644 index 000000000..9bb5aa885 --- /dev/null +++ b/cli/tests/testdata/node/test.js @@ -0,0 +1,383 @@ +// Copyright Joyent, Inc. and other Node contributors. + +// Ported from https://github.com/nodejs/node/blob/d396a041f71cc055ad60b0abc63ad81c0ee6a574/test/fixtures/test-runner/output/output.js + +// deno-lint-ignore-file + +import assert from "node:assert"; +import test from "node:test"; +import util from "node:util"; +import { setImmediate } from "node:timers"; + +test("sync pass todo", (t) => { + t.todo(); +}); + +test("sync pass todo with message", (t) => { + t.todo("this is a passing todo"); +}); + +test("sync fail todo", (t) => { + t.todo(); + throw new Error("thrown from sync fail todo"); +}); + +test("sync fail todo with message", (t) => { + t.todo("this is a failing todo"); + throw new Error("thrown from sync fail todo with message"); +}); + +test("sync skip pass", (t) => { + t.skip(); +}); + +test("sync skip pass with message", (t) => { + t.skip("this is skipped"); +}); + +test("sync pass", (t) => { + t.diagnostic("this test should pass"); +}); + +test("sync throw fail", () => { + throw new Error("thrown from sync throw fail"); +}); + +test("async skip pass", async (t) => { + t.skip(); +}); + +test("async pass", async () => { +}); + +test("async throw fail", async () => { + throw new Error("thrown from async throw fail"); +}); + +test("async skip fail", async (t) => { + t.skip(); + throw new Error("thrown from async throw fail"); +}); + +test("async assertion fail", async () => { + // Make sure the assert module is handled. + assert.strictEqual(true, false); +}); + +test("resolve pass", () => { + return Promise.resolve(); +}); + +test("reject fail", () => { + return Promise.reject(new Error("rejected from reject fail")); +}); + +test("unhandled rejection - passes but warns", () => { + Promise.reject(new Error("rejected from unhandled rejection fail")); +}); + +test("async unhandled rejection - passes but warns", async () => { + Promise.reject(new Error("rejected from async unhandled rejection fail")); +}); + +test("immediate throw - passes but warns", () => { + setImmediate(() => { + throw new Error("thrown from immediate throw fail"); + }); +}); + +test("immediate reject - passes but warns", () => { + setImmediate(() => { + Promise.reject(new Error("rejected from immediate reject fail")); + }); +}); + +test("immediate resolve pass", () => { + return new Promise((resolve) => { + setImmediate(() => { + resolve(); + }); + }); +}); + +test("subtest sync throw fail", async (t) => { + await t.test("+sync throw fail", (t) => { + t.diagnostic("this subtest should make its parent test fail"); + throw new Error("thrown from subtest sync throw fail"); + }); +}); + +test("sync throw non-error fail", async (t) => { + throw Symbol("thrown symbol from sync throw non-error fail"); +}); + +test("level 0a", { concurrency: 4 }, async (t) => { + t.test("level 1a", async (t) => { + const p1a = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 100); + }); + + return p1a; + }); + + test("level 1b", async (t) => { + const p1b = new Promise((resolve) => { + resolve(); + }); + + return p1b; + }); + + t.test("level 1c", async (t) => { + const p1c = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 200); + }); + + return p1c; + }); + + t.test("level 1d", async (t) => { + const p1c = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 150); + }); + + return p1c; + }); + + const p0a = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 300); + }); + + return p0a; +}); + +test("top level", { concurrency: 2 }, async (t) => { + t.test("+long running", async (t) => { + return new Promise((resolve, reject) => { + setTimeout(resolve, 300).unref(); + }); + }); + + t.test("+short running", async (t) => { + t.test("++short running", async (t) => {}); + }); +}); + +test("invalid subtest - pass but subtest fails", (t) => { + setImmediate(() => { + t.test("invalid subtest fail", () => { + throw new Error("this should not be thrown"); + }); + }); +}); + +test("sync skip option", { skip: true }, (t) => { + throw new Error("this should not be executed"); +}); + +test("sync skip option with message", { skip: "this is skipped" }, (t) => { + throw new Error("this should not be executed"); +}); + +test("sync skip option is false fail", { skip: false }, (t) => { + throw new Error("this should be executed"); +}); + +// A test with no arguments provided. +test(); + +// A test with only a named function provided. +test(function functionOnly() {}); + +// A test with only an anonymous function provided. +test(() => {}); + +// A test with only a name provided. +test("test with only a name provided"); + +// A test with an empty string name. +test(""); + +// A test with only options provided. +test({ skip: true }); + +// A test with only a name and options provided. +test("test with a name and options provided", { skip: true }); + +// A test with only options and a function provided. +test({ skip: true }, function functionAndOptions() {}); + +// A test whose description needs to be escaped. +// test("escaped description \\ # \\#\\ \n \t \f \v \b \r"); + +// A test whose skip message needs to be escaped. +test("escaped skip message", { skip: "#skip" }); + +// A test whose todo message needs to be escaped. +test("escaped todo message", { todo: "#todo" }); + +// A test with a diagnostic message that needs to be escaped. +test("escaped diagnostic", (t) => { + t.diagnostic("#diagnostic"); +}); + +test("callback pass", (t, done) => { + setImmediate(done); +}); + +test("callback fail", (t, done) => { + setImmediate(() => { + done(new Error("callback failure")); + }); +}); + +test("sync t is this in test", function (t) { + assert.strictEqual(this, t); +}); + +test("async t is this in test", async function (t) { + assert.strictEqual(this, t); +}); + +test("callback t is this in test", function (t, done) { + assert.strictEqual(this, t); + done(); +}); + +test("callback also returns a Promise", async (t, done) => { + throw new Error("thrown from callback also returns a Promise"); +}); + +test("callback throw", (t, done) => { + throw new Error("thrown from callback throw"); +}); + +test("callback called twice", (t, done) => { + done(); + done(); +}); + +test("callback called twice in different ticks", (t, done) => { + setImmediate(done); + done(); +}); + +test("callback called twice in future tick", (t, done) => { + setImmediate(() => { + done(); + done(); + }); +}); + +test("callback async throw", (t, done) => { + setImmediate(() => { + throw new Error("thrown from callback async throw"); + }); +}); + +test("callback async throw after done", (t, done) => { + setImmediate(() => { + throw new Error("thrown from callback async throw after done"); + }); + + done(); +}); + +test("custom inspect symbol fail", () => { + const obj = { + [util.inspect.custom]() { + return "customized"; + }, + foo: 1, + }; + + throw obj; +}); + +test("custom inspect symbol that throws fail", () => { + const obj = { + [util.inspect.custom]() { + throw new Error("bad-inspect"); + }, + foo: 1, + }; + + throw obj; +}); + +test("subtest sync throw fails", async (t) => { + await t.test("sync throw fails at first", (t) => { + throw new Error("thrown from subtest sync throw fails at first"); + }); + await t.test("sync throw fails at second", (t) => { + throw new Error("thrown from subtest sync throw fails at second"); + }); +}); + +test("timed out async test", { timeout: 5 }, async (t) => { + return new Promise((resolve) => { + setTimeout(resolve, 100); + }); +}); + +test("timed out callback test", { timeout: 5 }, (t, done) => { + setTimeout(done, 100); +}); + +test("large timeout async test is ok", { timeout: 30_000_000 }, async (t) => { + return new Promise((resolve) => { + setTimeout(resolve, 10); + }); +}); + +test( + "large timeout callback test is ok", + { timeout: 30_000_000 }, + (t, done) => { + setTimeout(done, 10); + }, +); + +test("successful thenable", () => { + let thenCalled = false; + return { + get then() { + if (thenCalled) throw new Error(); + thenCalled = true; + return (successHandler) => successHandler(); + }, + }; +}); + +test("rejected thenable", () => { + let thenCalled = false; + return { + get then() { + if (thenCalled) throw new Error(); + thenCalled = true; + return (_, errorHandler) => errorHandler("custom error"); + }, + }; +}); + +test("unfinished test with uncaughtException", async () => { + await new Promise(() => { + setTimeout(() => { + throw new Error("foo"); + }); + }); +}); + +test("unfinished test with unhandledRejection", async () => { + await new Promise(() => { + setTimeout(() => Promise.reject(new Error("bar"))); + }); +}); diff --git a/cli/tests/testdata/node/test.out b/cli/tests/testdata/node/test.out new file mode 100644 index 000000000..afda0eca3 --- /dev/null +++ b/cli/tests/testdata/node/test.out @@ -0,0 +1,176 @@ +[WILDCARD] +Warning: Not implemented: test.options.concurrency +Warning: Not implemented: test.options.concurrency +Warning: Not implemented: test.options.timeout +Warning: Not implemented: test.options.timeout +Warning: Not implemented: test.options.timeout +Warning: Not implemented: test.options.timeout +running 62 tests from ./node/test.js +sync pass todo ... +------- output ------- +Warning: Not implemented: test.TestContext.todo +----- output end ----- +sync pass todo ... ok [WILDCARD] +sync pass todo with message ... +------- output ------- +Warning: Not implemented: test.TestContext.todo +----- output end ----- +sync pass todo with message ... ok [WILDCARD] +sync fail todo ... +------- output ------- +Warning: Not implemented: test.TestContext.todo +----- output end ----- +sync fail todo ... FAILED [WILDCARD] +sync fail todo with message ... +------- output ------- +Warning: Not implemented: test.TestContext.todo +----- output end ----- +sync fail todo with message ... FAILED [WILDCARD] +sync skip pass ... +------- output ------- +Warning: Not implemented: test.TestContext.skip +----- output end ----- +sync skip pass ... ok [WILDCARD] +sync skip pass with message ... +------- output ------- +Warning: Not implemented: test.TestContext.skip +----- output end ----- +sync skip pass with message ... ok [WILDCARD] +sync pass ... +------- output ------- +DIAGNOSTIC: this test should pass +----- output end ----- +sync pass ... ok [WILDCARD] +sync throw fail ... FAILED [WILDCARD] +async skip pass ... +------- output ------- +Warning: Not implemented: test.TestContext.skip +----- output end ----- +async skip pass ... ok [WILDCARD] +async pass ... ok [WILDCARD] +async throw fail ... FAILED [WILDCARD] +async skip fail ... +------- output ------- +Warning: Not implemented: test.TestContext.skip +----- output end ----- +async skip fail ... FAILED [WILDCARD] +async assertion fail ... FAILED [WILDCARD] +resolve pass ... ok [WILDCARD] +reject fail ... FAILED [WILDCARD] +unhandled rejection - passes but warns ... +Uncaught error from ./node/test.js FAILED +unhandled rejection - passes but warns ... cancelled ([WILDCARD]) +async unhandled rejection - passes but warns ... cancelled ([WILDCARD]) +immediate throw - passes but warns ... cancelled ([WILDCARD]) +immediate reject - passes but warns ... cancelled ([WILDCARD]) +immediate resolve pass ... cancelled ([WILDCARD]) +subtest sync throw fail ... cancelled ([WILDCARD]) +sync throw non-error fail ... cancelled ([WILDCARD]) +level 0a ... cancelled ([WILDCARD]) +top level ... cancelled ([WILDCARD]) +invalid subtest - pass but subtest fails ... cancelled ([WILDCARD]) +sync skip option ... ignored ([WILDCARD]) +sync skip option with message ... cancelled ([WILDCARD]) +sync skip option is false fail ... cancelled ([WILDCARD]) +noop ... cancelled ([WILDCARD]) +functionOnly ... cancelled ([WILDCARD]) +<anonymous> ... cancelled ([WILDCARD]) +test with only a name provided ... cancelled ([WILDCARD]) +noop ... cancelled ([WILDCARD]) +noop ... ignored ([WILDCARD]) +test with a name and options provided ... ignored ([WILDCARD]) +functionAndOptions ... ignored ([WILDCARD]) +escaped skip message ... cancelled ([WILDCARD]) +escaped todo message ... cancelled ([WILDCARD]) +escaped diagnostic ... cancelled ([WILDCARD]) +callback pass ... cancelled ([WILDCARD]) +callback fail ... cancelled ([WILDCARD]) +sync t is this in test ... cancelled ([WILDCARD]) +async t is this in test ... cancelled ([WILDCARD]) +callback t is this in test ... cancelled ([WILDCARD]) +callback also returns a Promise ... cancelled ([WILDCARD]) +callback throw ... cancelled ([WILDCARD]) +callback called twice ... cancelled ([WILDCARD]) +callback called twice in different ticks ... cancelled ([WILDCARD]) +callback called twice in future tick ... cancelled ([WILDCARD]) +callback async throw ... cancelled ([WILDCARD]) +callback async throw after done ... cancelled ([WILDCARD]) +custom inspect symbol fail ... cancelled ([WILDCARD]) +custom inspect symbol that throws fail ... cancelled ([WILDCARD]) +subtest sync throw fails ... cancelled ([WILDCARD]) +timed out async test ... cancelled ([WILDCARD]) +timed out callback test ... cancelled ([WILDCARD]) +large timeout async test is ok ... cancelled ([WILDCARD]) +large timeout callback test is ok ... cancelled ([WILDCARD]) +successful thenable ... cancelled ([WILDCARD]) +rejected thenable ... cancelled ([WILDCARD]) +unfinished test with uncaughtException ... cancelled ([WILDCARD]) +unfinished test with unhandledRejection ... cancelled ([WILDCARD]) + + ERRORS + +sync fail todo => node:test:135:10 +error: Error: thrown from sync fail todo + throw new Error("thrown from sync fail todo"); +[WILDCARD] + +sync fail todo with message => node:test:135:10 +error: Error: thrown from sync fail todo with message + throw new Error("thrown from sync fail todo with message"); +[WILDCARD] + +sync throw fail => node:test:135:10 +error: Error: thrown from sync throw fail + throw new Error("thrown from sync throw fail"); +[WILDCARD] + +async throw fail => node:test:135:10 +error: Error: thrown from async throw fail + throw new Error("thrown from async throw fail"); +[WILDCARD] + +async skip fail => node:test:135:10 +error: Error: thrown from async throw fail + throw new Error("thrown from async throw fail"); +[WILDCARD] + +async assertion fail => node:test:135:10 +error: AssertionError: Values are not strictly equal: + + + [Diff] Actual / Expected + + +- true ++ false + + at [WILDCARD] + +reject fail => node:test:135:10 +error: Error: rejected from reject fail + return Promise.reject(new Error("rejected from reject fail")); + ^ + at [WILDCARD] + +./node/test.js (uncaught error) +error: Error: rejected from unhandled rejection fail + Promise.reject(new Error("rejected from unhandled rejection fail")); + ^ + at [WILDCARD] +This error was not caught from a test and caused the test runner to fail on the referenced module. +It most likely originated from a dangling promise, event/timeout handler or top-level code. + + FAILURES + +sync fail todo => node:test:135:10 +sync fail todo with message => node:test:135:10 +sync throw fail => node:test:135:10 +async throw fail => node:test:135:10 +async skip fail => node:test:135:10 +async assertion fail => node:test:135:10 +reject fail => node:test:135:10 +./node/test.js (uncaught error) + +FAILED | 8 passed | 51 failed | 4 ignored [WILDCARD] + +error: Test failed diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 318de77e1..ca79c3dce 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -491,6 +491,7 @@ deno_core::extension!(deno_node, "stream/web.ts" with_specifier "node:stream/web", "string_decoder.ts" with_specifier "node:string_decoder", "sys.ts" with_specifier "node:sys", + "testing.ts" with_specifier "node:test", "timers.ts" with_specifier "node:timers", "timers/promises.ts" with_specifier "node:timers/promises", "tls.ts" with_specifier "node:tls", diff --git a/ext/node/polyfill.rs b/ext/node/polyfill.rs index 16ffe185d..fede915a2 100644 --- a/ext/node/polyfill.rs +++ b/ext/node/polyfill.rs @@ -62,6 +62,7 @@ generate_builtin_node_module_lists! { "stream/web", "string_decoder", "sys", + "test", "timers", "timers/promises", "tls", diff --git a/ext/node/polyfills/01_require.js b/ext/node/polyfills/01_require.js index c58dad9a4..acdf8402f 100644 --- a/ext/node/polyfills/01_require.js +++ b/ext/node/polyfills/01_require.js @@ -117,6 +117,7 @@ import streamPromises from "node:stream/promises"; import streamWeb from "node:stream/web"; import stringDecoder from "node:string_decoder"; import sys from "node:sys"; +import test from "node:test"; import timers from "node:timers"; import timersPromises from "node:timers/promises"; import tls from "node:tls"; @@ -219,6 +220,7 @@ function setupBuiltinModules() { "stream/web": streamWeb, string_decoder: stringDecoder, sys, + test, timers, "timers/promises": timersPromises, tls, diff --git a/ext/node/polyfills/testing.ts b/ext/node/polyfills/testing.ts new file mode 100644 index 000000000..2c112d9fb --- /dev/null +++ b/ext/node/polyfills/testing.ts @@ -0,0 +1,237 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +// TODO(petamoriken): enable prefer-primordials for node polyfills +// deno-lint-ignore-file prefer-primordials + +import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; + +export function deferred() { + let methods; + const promise = new Promise((resolve, reject) => { + methods = { + async resolve(value) { + await value; + resolve(value); + }, + // deno-lint-ignore no-explicit-any + reject(reason?: any) { + reject(reason); + }, + }; + }); + return Object.assign(promise, methods); +} + +export function run() { + notImplemented("test.run"); +} + +function noop() {} + +class NodeTestContext { + #denoContext: Deno.TestContext; + + constructor(t: Deno.TestContext) { + this.#denoContext = t; + } + + get signal() { + notImplemented("test.TestContext.signal"); + return null; + } + + get name() { + notImplemented("test.TestContext.name"); + return null; + } + + diagnostic(message) { + console.log("DIAGNOSTIC:", message); + } + + get mock() { + notImplemented("test.TestContext.mock"); + return null; + } + + runOnly() { + notImplemented("test.TestContext.runOnly"); + return null; + } + + skip() { + warnNotImplemented("test.TestContext.skip"); + return null; + } + + todo() { + warnNotImplemented("test.TestContext.todo"); + return null; + } + + test(name, options, fn) { + const prepared = prepareOptions(name, options, fn, {}); + return this.#denoContext.step({ + name: prepared.name, + fn: prepared.fn, + ignore: prepared.options.todo || prepared.options.skip, + }).then(() => undefined); + } + + before(_fn, _options) { + notImplemented("test.TestContext.before"); + } + + after(_fn, _options) { + notImplemented("test.TestContext.after"); + } + + beforeEach(_fn, _options) { + notImplemented("test.TestContext.beforeEach"); + } + + afterEach(_fn, _options) { + notImplemented("test.TestContext.afterEach"); + } +} + +function prepareOptions(name, options, fn, overrides) { + if (typeof name === "function") { + fn = name; + } else if (name !== null && typeof name === "object") { + fn = options; + options = name; + } else if (typeof options === "function") { + fn = options; + } + + if (options === null || typeof options !== "object") { + options = {}; + } + + const finalOptions = { ...options, ...overrides }; + const { concurrency, timeout, signal } = finalOptions; + + if (typeof concurrency !== "undefined") { + warnNotImplemented("test.options.concurrency"); + } + if (typeof timeout !== "undefined") { + warnNotImplemented("test.options.timeout"); + } + if (typeof signal !== "undefined") { + warnNotImplemented("test.options.signal"); + } + + if (typeof fn !== "function") { + fn = noop; + } + + if (typeof name !== "string" || name === "") { + name = fn.name || "<anonymous>"; + } + + return { fn, options: finalOptions, name }; +} + +function wrapTestFn(fn, promise) { + return async function (t) { + const nodeTestContext = new NodeTestContext(t); + try { + await fn(nodeTestContext); + } finally { + promise.resolve(undefined); + } + }; +} + +function prepareDenoTest(name, options, fn, overrides) { + const prepared = prepareOptions(name, options, fn, overrides); + + const promise = deferred(); + + const denoTestOptions = { + name: prepared.name, + fn: wrapTestFn(prepared.fn, promise), + only: prepared.options.only, + ignore: prepared.options.todo || prepared.options.skip, + }; + Deno.test(denoTestOptions); + return promise; +} + +export function test(name, options, fn) { + return prepareDenoTest(name, options, fn, {}); +} + +test.skip = function skip(name, options, fn) { + return prepareDenoTest(name, options, fn, { skip: true }); +}; + +test.todo = function todo(name, options, fn) { + return prepareDenoTest(name, options, fn, { todo: true }); +}; + +test.only = function only(name, options, fn) { + return prepareDenoTest(name, options, fn, { only: true }); +}; + +export function describe() { + notImplemented("test.describe"); +} + +export function it() { + notImplemented("test.it"); +} + +export function before() { + notImplemented("test.before"); +} + +export function after() { + notImplemented("test.after"); +} + +export function beforeEach() { + notImplemented("test.beforeEach"); +} + +export function afterEach() { + notImplemented("test.afterEach"); +} + +export const mock = { + fn: () => { + notImplemented("test.mock.fn"); + }, + getter: () => { + notImplemented("test.mock.getter"); + }, + method: () => { + notImplemented("test.mock.method"); + }, + reset: () => { + notImplemented("test.mock.reset"); + }, + restoreAll: () => { + notImplemented("test.mock.restoreAll"); + }, + setter: () => { + notImplemented("test.mock.setter"); + }, + timers: { + enable: () => { + notImplemented("test.mock.timers.enable"); + }, + reset: () => { + notImplemented("test.mock.timers.reset"); + }, + tick: () => { + notImplemented("test.mock.timers.tick"); + }, + runAll: () => { + notImplemented("test.mock.timers.runAll"); + }, + }, +}; + +export default test; |