diff --git a/cli/tests/integration/mod.rs b/cli/tests/integration/mod.rs index 7ed1d6e0df..d102b486d6 100644 --- a/cli/tests/integration/mod.rs +++ b/cli/tests/integration/mod.rs @@ -70,6 +70,8 @@ mod js_unit_tests; mod lint; #[path = "lsp_tests.rs"] mod lsp; +#[path = "node_compat_tests.rs"] +mod node_compat_tests; #[path = "node_unit_tests.rs"] mod node_unit_tests; #[path = "npm_tests.rs"] diff --git a/cli/tests/integration/node_compat_tests.rs b/cli/tests/integration/node_compat_tests.rs new file mode 100644 index 0000000000..7617b70379 --- /dev/null +++ b/cli/tests/integration/node_compat_tests.rs @@ -0,0 +1,25 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use test_util as util; + +#[test] +fn node_compat_tests() { + let mut deno = util::deno_cmd() + .current_dir(util::root_path()) + .arg("test") + .arg("--unstable") + .arg("--import-map") + .arg( + util::tests_path() + .join("node_compat") + .join("import_map.json"), + ) + .arg("-A") + .arg(util::tests_path().join("node_compat")) + .spawn() + .expect("failed to spawn script"); + + let status = deno.wait().expect("failed to wait for the child process"); + assert_eq!(Some(0), status.code()); + assert!(status.success()); +} diff --git a/cli/tests/node_compat/common.ts b/cli/tests/node_compat/common.ts new file mode 100644 index 0000000000..72f44e5101 --- /dev/null +++ b/cli/tests/node_compat/common.ts @@ -0,0 +1,54 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { join } from "std/path/mod.ts"; + +/** + * The test suite matches the folders inside the `test` folder inside the + * node repo + * + * Each test suite contains a list of files (which can be paths + * or a regex to match) that will be pulled from the node repo + */ +type TestSuites = Record; + +interface Config { + nodeVersion: string; + /** Ignored files won't regenerated by the update script */ + ignore: TestSuites; + /** + * The files that will be run by the test suite + * + * The files to be generated with the update script must be listed here as well, + * but they won't be regenerated if they are listed in the `ignore` configuration + */ + tests: TestSuites; + windowsIgnore: TestSuites; + darwinIgnore: TestSuites; +} + +export const config: Config = JSON.parse( + await Deno.readTextFile(new URL("./config.json", import.meta.url)), +); + +export const ignoreList = Object.entries(config.ignore).reduce( + (total: RegExp[], [suite, paths]) => { + paths.forEach((path) => total.push(new RegExp(join(suite, path)))); + return total; + }, + [], +); + +export function getPathsFromTestSuites(suites: TestSuites): string[] { + const testPaths: string[] = []; + for (const [dir, paths] of Object.entries(suites)) { + if ( + ["parallel", "internet", "pummel", "sequential", "pseudo-tty"].includes( + dir, + ) + ) { + for (const path of paths) { + testPaths.push(join(dir, path)); + } + } + } + return testPaths; +} diff --git a/cli/tests/node_compat/config.json b/cli/tests/node_compat/config.json new file mode 100644 index 0000000000..e032c699db --- /dev/null +++ b/cli/tests/node_compat/config.json @@ -0,0 +1,13 @@ +{ + "nodeVersion": "18.12.1", + "ignore": {}, + "tests": { + "parallel": [ + "test-assert-fail.js", + "test-assert-strict-exists.js", + "test-btoa-atob.js" + ] + }, + "windowsIgnore": {}, + "darwinIgnore": {} +} diff --git a/cli/tests/node_compat/import_map.json b/cli/tests/node_compat/import_map.json new file mode 100644 index 0000000000..ccdef1e007 --- /dev/null +++ b/cli/tests/node_compat/import_map.json @@ -0,0 +1,5 @@ +{ + "imports": { + "std/": "../../../test_util/std/" + } +} diff --git a/cli/tests/node_compat/runner.ts b/cli/tests/node_compat/runner.ts new file mode 100644 index 0000000000..f12cc69b02 --- /dev/null +++ b/cli/tests/node_compat/runner.ts @@ -0,0 +1,7 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { createRequire } from "node:module"; +const file = Deno.args[0]; +if (!file) { + throw new Error("No file provided"); +} +createRequire(import.meta.url)(file); diff --git a/cli/tests/node_compat/test.ts b/cli/tests/node_compat/test.ts new file mode 100644 index 0000000000..3af1fb6935 --- /dev/null +++ b/cli/tests/node_compat/test.ts @@ -0,0 +1,115 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { magenta } from "std/fmt/colors.ts"; +import { dirname, fromFileUrl, join } from "std/path/mod.ts"; +import { fail } from "std/testing/asserts.ts"; +import { config, getPathsFromTestSuites } from "./common.ts"; + +// If the test case is invoked like +// deno test -A cli/tests/node_compat/test.ts -- +// Use the test-names as filters +const filters = Deno.args; + +/** + * This script will run the test files specified in the configuration file + * + * Each test file will be run independently and wait until completion, if an abnormal + * code for the test is reported, the test suite will fail immediately + */ + +const toolsPath = dirname(fromFileUrl(import.meta.url)); +const stdRootUrl = new URL("../../", import.meta.url).href; +const testPaths = getPathsFromTestSuites(config.tests); +const cwd = new URL(".", import.meta.url); +const importMap = "import_map.json"; +const windowsIgnorePaths = new Set( + getPathsFromTestSuites(config.windowsIgnore), +); +const darwinIgnorePaths = new Set( + getPathsFromTestSuites(config.darwinIgnore), +); + +const decoder = new TextDecoder(); + +for await (const path of testPaths) { + // If filter patterns are given and any pattern doesn't match + // to the file path, then skip the case + if ( + filters.length > 0 && + filters.every((pattern) => !path.includes(pattern)) + ) { + continue; + } + const ignore = + (Deno.build.os === "windows" && windowsIgnorePaths.has(path)) || + (Deno.build.os === "darwin" && darwinIgnorePaths.has(path)); + Deno.test({ + name: `Node.js compatibility "${path}"`, + ignore, + fn: async () => { + const testCase = join(toolsPath, "test", path); + + const v8Flags = ["--stack-size=4000"]; + const testSource = await Deno.readTextFile(testCase); + // TODO(kt3k): Parse `Flags` directive correctly + if (testSource.includes("Flags: --expose_externalize_string")) { + v8Flags.push("--expose-externalize-string"); + } + + const args = [ + "run", + "-A", + "--quiet", + "--unstable", + "--unsafely-ignore-certificate-errors", + "--v8-flags=" + v8Flags.join(), + testCase.endsWith(".mjs") ? "--import-map=" + importMap : "runner.ts", + testCase, + ]; + + // Pipe stdout in order to output each test result as Deno.test output + // That way the tests will respect the `--quiet` option when provided + const command = new Deno.Command(Deno.execPath(), { + args, + env: { + DENO_NODE_COMPAT_URL: stdRootUrl, + }, + cwd, + }); + const { code, stdout, stderr } = await command.output(); + + if (stdout.length) console.log(decoder.decode(stdout)); + + if (code !== 0) { + console.log(`Error: "${path}" failed`); + console.log( + "You can repeat only this test with the command:", + magenta( + `./target/debug/deno test -A --import-map cli/tests/node_compat/import_map.json cli/tests/node_compat/test.ts -- ${path}`, + ), + ); + fail(decoder.decode(stderr)); + } + }, + }); +} + +function checkConfigTestFilesOrder(testFileLists: Array) { + for (const testFileList of testFileLists) { + const sortedTestList = JSON.parse(JSON.stringify(testFileList)); + sortedTestList.sort(); + if (JSON.stringify(testFileList) !== JSON.stringify(sortedTestList)) { + throw new Error( + `File names in \`config.json\` are not correct order.`, + ); + } + } +} + +if (filters.length === 0) { + Deno.test("checkConfigTestFilesOrder", function () { + checkConfigTestFilesOrder([ + ...Object.keys(config.ignore).map((suite) => config.ignore[suite]), + ...Object.keys(config.tests).map((suite) => config.tests[suite]), + ]); + }); +} diff --git a/cli/tests/node_compat/test/common/index.js b/cli/tests/node_compat/test/common/index.js new file mode 100644 index 0000000000..f93f7b7a6b --- /dev/null +++ b/cli/tests/node_compat/test/common/index.js @@ -0,0 +1,489 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +/** + * This file is meant as a replacement for the original common/index.js + * + * That file has a lot of node functionality not currently supported, so this is a lite + * version of that file, which most tests should be able to use + */ +'use strict'; +const assert = require("assert"); +const path = require("path"); +const util = require("util"); +const tmpdir = require("./tmpdir"); + +function platformTimeout(ms) { + return ms; +} + +let localhostIPv4 = null; + +let knownGlobals = [ + AbortSignal, + addEventListener, + alert, + atob, + btoa, + Buffer, + caches, + clearImmediate, + close, + closed, + confirm, + console, + crypto, + Deno, + dispatchEvent, + fetch, + getParent, + global, + global.clearInterval, + global.clearTimeout, + global.setInterval, + global.setTimeout, + localStorage, + location, + navigator, + onload, + onunload, + process, + prompt, + queueMicrotask, + removeEventListener, + reportError, + self, + sessionStorage, + setImmediate, + window, +]; + +if (global.AbortController) + knownGlobals.push(global.AbortController); + +if (global.gc) { + knownGlobals.push(global.gc); +} + +if (global.performance) { + knownGlobals.push(global.performance); +} +if (global.PerformanceMark) { + knownGlobals.push(global.PerformanceMark); +} +if (global.PerformanceMeasure) { + knownGlobals.push(global.PerformanceMeasure); +} + +if (global.structuredClone) { + knownGlobals.push(global.structuredClone); +} + +function allowGlobals(...allowlist) { + knownGlobals = knownGlobals.concat(allowlist); +} + +if (process.env.NODE_TEST_KNOWN_GLOBALS !== '0') { + if (process.env.NODE_TEST_KNOWN_GLOBALS) { + const knownFromEnv = process.env.NODE_TEST_KNOWN_GLOBALS.split(','); + allowGlobals(...knownFromEnv); + } + + function leakedGlobals() { + const leaked = []; + + for (const val in global) { + if (!knownGlobals.includes(global[val])) { + leaked.push(val); + } + } + + return leaked; + } + + process.on('exit', function() { + const leaked = leakedGlobals(); + if (leaked.length > 0) { + assert.fail(`Unexpected global(s) found: ${leaked.join(', ')}`); + } + }); +} + +function _expectWarning(name, expected, code) { + if (typeof expected === 'string') { + expected = [[expected, code]]; + } else if (!Array.isArray(expected)) { + expected = Object.entries(expected).map(([a, b]) => [b, a]); + } else if (!(Array.isArray(expected[0]))) { + expected = [[expected[0], expected[1]]]; + } + // Deprecation codes are mandatory, everything else is not. + if (name === 'DeprecationWarning') { + expected.forEach(([_, code]) => assert(code, expected)); + } + return mustCall((warning) => { + const [ message, code ] = expected.shift(); + assert.strictEqual(warning.name, name); + if (typeof message === 'string') { + assert.strictEqual(warning.message, message); + } else { + assert.match(warning.message, message); + } + assert.strictEqual(warning.code, code); + }, expected.length); +} + +let catchWarning; + +// Accepts a warning name and description or array of descriptions or a map of +// warning names to description(s) ensures a warning is generated for each +// name/description pair. +// The expected messages have to be unique per `expectWarning()` call. +function expectWarning(nameOrMap, expected, code) { + if (catchWarning === undefined) { + catchWarning = {}; + process.on('warning', (warning) => { + if (!catchWarning[warning.name]) { + throw new TypeError( + `"${warning.name}" was triggered without being expected.\n` + + util.inspect(warning) + ); + } + catchWarning[warning.name](warning); + }); + } + if (typeof nameOrMap === 'string') { + catchWarning[nameOrMap] = _expectWarning(nameOrMap, expected, code); + } else { + Object.keys(nameOrMap).forEach((name) => { + catchWarning[name] = _expectWarning(name, nameOrMap[name]); + }); + } +} + +/** + * Useful for testing expected internal/error objects + * + * @param {Error} error + */ +function expectsError(validator, exact) { + /** + * @param {Error} error + */ + return mustCall((...args) => { + if (args.length !== 1) { + // Do not use `assert.strictEqual()` to prevent `inspect` from + // always being called. + assert.fail(`Expected one argument, got ${util.inspect(args)}`); + } + const error = args.pop(); + const descriptor = Object.getOwnPropertyDescriptor(error, 'message'); + // The error message should be non-enumerable + assert.strictEqual(descriptor.enumerable, false); + + assert.throws(() => { throw error; }, validator); + return true; + }, exact); +} + +const noop = () => {}; + +/** + * @param {Function} fn + * @param {number} exact + */ +function mustCall(fn, exact) { + return _mustCallInner(fn, exact, "exact"); +} + +function mustCallAtLeast(fn, minimum) { + return _mustCallInner(fn, minimum, 'minimum'); +} + +function mustSucceed(fn, exact) { + return mustCall(function(err, ...args) { + assert.ifError(err); + if (typeof fn === 'function') + return fn.apply(this, args); + }, exact); +} + +const mustCallChecks = []; +/** + * @param {number} exitCode + */ +function runCallChecks(exitCode) { + if (exitCode !== 0) return; + + const failed = mustCallChecks.filter(function (context) { + if ("minimum" in context) { + context.messageSegment = `at least ${context.minimum}`; + return context.actual < context.minimum; + } + context.messageSegment = `exactly ${context.exact}`; + return context.actual !== context.exact; + }); + + 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")); + }); + + if (failed.length) process.exit(1); +} + +/** + * @param {Function} fn + * @param {"exact" | "minimum"} field + */ +function _mustCallInner(fn, criteria = 1, field) { + // @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 context; + if (field === "exact") { + context = { + exact: criteria, + actual: 0, + stack: util.inspect(new Error()), + name: fn.name || "", + }; + } else { + context = { + minimum: criteria, + actual: 0, + stack: util.inspect(new Error()), + name: fn.name || "", + }; + } + + // Add the exit listener only once to avoid listener leak warnings + if (mustCallChecks.length === 0) process.on("exit", runCallChecks); + + mustCallChecks.push(context); + + return function () { + context.actual++; + return fn.apply(this, arguments); + }; +} + +/** + * @param {string=} msg + */ +function mustNotCall(msg) { + /** + * @param {any[]} args + */ + return function mustNotCall(...args) { + const argsInfo = args.length > 0 + ? `\ncalled with arguments: ${args.map(util.inspect).join(", ")}` + : ""; + assert.fail( + `${msg || "function should not have been called"} at unknown` + + argsInfo, + ); + }; +} + +const _mustNotMutateObjectDeepProxies = new WeakMap(); + +function mustNotMutateObjectDeep(original) { + // Return primitives and functions directly. Primitives are immutable, and + // proxied functions are impossible to compare against originals, e.g. with + // `assert.deepEqual()`. + if (original === null || typeof original !== 'object') { + return original; + } + + const cachedProxy = _mustNotMutateObjectDeepProxies.get(original); + if (cachedProxy) { + return cachedProxy; + } + + const _mustNotMutateObjectDeepHandler = { + __proto__: null, + defineProperty(target, property, descriptor) { + assert.fail(`Expected no side effects, got ${inspect(property)} ` + + 'defined'); + }, + deleteProperty(target, property) { + assert.fail(`Expected no side effects, got ${inspect(property)} ` + + 'deleted'); + }, + get(target, prop, receiver) { + return mustNotMutateObjectDeep(Reflect.get(target, prop, receiver)); + }, + preventExtensions(target) { + assert.fail('Expected no side effects, got extensions prevented on ' + + inspect(target)); + }, + set(target, property, value, receiver) { + assert.fail(`Expected no side effects, got ${inspect(value)} ` + + `assigned to ${inspect(property)}`); + }, + setPrototypeOf(target, prototype) { + assert.fail(`Expected no side effects, got set prototype to ${prototype}`); + } + }; + + const proxy = new Proxy(original, _mustNotMutateObjectDeepHandler); + _mustNotMutateObjectDeepProxies.set(original, proxy); + return proxy; +} + +// A helper function to simplify checking for ERR_INVALID_ARG_TYPE output. +function invalidArgTypeHelper(input) { + if (input == null) { + return ` Received ${input}`; + } + if (typeof input === "function" && input.name) { + return ` Received function ${input.name}`; + } + if (typeof input === "object") { + if (input.constructor && input.constructor.name) { + return ` Received an instance of ${input.constructor.name}`; + } + return ` Received ${util.inspect(input, { depth: -1 })}`; + } + let inspected = util.inspect(input, { colors: false }); + if (inspected.length > 25) { + inspected = `${inspected.slice(0, 25)}...`; + } + return ` Received type ${typeof input} (${inspected})`; +} + +const isWindows = process.platform === 'win32'; +const isAIX = process.platform === 'aix'; +const isSunOS = process.platform === 'sunos'; +const isFreeBSD = process.platform === 'freebsd'; +const isOpenBSD = process.platform === 'openbsd'; +const isLinux = process.platform === 'linux'; +const isOSX = process.platform === 'darwin'; + +const isDumbTerminal = process.env.TERM === 'dumb'; + +function skipIfDumbTerminal() { + if (isDumbTerminal) { + skip('skipping - dumb terminal'); + } +} + +function printSkipMessage(msg) { + console.log(`1..0 # Skipped: ${msg}`); +} + +function skip(msg) { + printSkipMessage(msg); + process.exit(0); +} + +const PIPE = (() => { + const localRelative = path.relative(process.cwd(), `${tmpdir.path}/`); + const pipePrefix = isWindows ? "\\\\.\\pipe\\" : localRelative; + const pipeName = `node-test.${process.pid}.sock`; + return path.join(pipePrefix, pipeName); +})(); + +function getArrayBufferViews(buf) { + const { buffer, byteOffset, byteLength } = buf; + + const out = []; + + const arrayBufferViews = [ + Int8Array, + Uint8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + DataView, + ]; + + for (const type of arrayBufferViews) { + const { BYTES_PER_ELEMENT = 1 } = type; + if (byteLength % BYTES_PER_ELEMENT === 0) { + out.push(new type(buffer, byteOffset, byteLength / BYTES_PER_ELEMENT)); + } + } + return out; +} + +function getBufferSources(buf) { + return [...getArrayBufferViews(buf), new Uint8Array(buf).buffer]; +} + +const pwdCommand = isWindows ? + ['cmd.exe', ['/d', '/c', 'cd']] : + ['pwd', []]; + +module.exports = { + allowGlobals, + expectsError, + expectWarning, + getArrayBufferViews, + getBufferSources, + hasCrypto: true, + invalidArgTypeHelper, + mustCall, + mustCallAtLeast, + mustNotCall, + mustNotMutateObjectDeep, + mustSucceed, + PIPE, + platformTimeout, + printSkipMessage, + pwdCommand, + skipIfDumbTerminal, + isDumbTerminal, + isWindows, + isAIX, + isSunOS, + isFreeBSD, + isOpenBSD, + isLinux, + isOSX, + isMainThread: true, // TODO(f3n67u): replace with `worker_thread.isMainThread` when `worker_thread` implemented + skip, + get hasIPv6() { + const iFaces = require('os').networkInterfaces(); + const re = isWindows ? /Loopback Pseudo-Interface/ : /lo/; + return Object.keys(iFaces).some((name) => { + return re.test(name) && + iFaces[name].some(({ family }) => family === 'IPv6'); + }); + }, + + get localhostIPv4() { + if (localhostIPv4 !== null) return localhostIPv4; + if (localhostIPv4 === null) localhostIPv4 = '127.0.0.1'; + + return localhostIPv4; + }, + + get PORT() { + return 12346; + }, +}; diff --git a/cli/tests/node_compat/test/common/package.json b/cli/tests/node_compat/test/common/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/cli/tests/node_compat/test/common/package.json @@ -0,0 +1 @@ +{} diff --git a/cli/tests/node_compat/test/common/tmpdir.js b/cli/tests/node_compat/test/common/tmpdir.js new file mode 100644 index 0000000000..d3ce98e45b --- /dev/null +++ b/cli/tests/node_compat/test/common/tmpdir.js @@ -0,0 +1,69 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 16.13.0 +// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +// const { isMainThread } = require('worker_threads'); + +function rmSync(pathname) { + fs.rmSync(pathname, { maxRetries: 3, recursive: true, force: true }); +} + +const testRoot = process.env.NODE_TEST_DIR ? + fs.realpathSync(process.env.NODE_TEST_DIR) : path.resolve(__dirname, '..'); + +// Using a `.` prefixed name, which is the convention for "hidden" on POSIX, +// gets tools to ignore it by default or by simple rules, especially eslint. +const tmpdirName = '.tmp.' + + (process.env.TEST_SERIAL_ID || process.env.TEST_THREAD_ID || '0'); +const tmpPath = path.join(testRoot, tmpdirName); + +let firstRefresh = true; +function refresh() { + rmSync(this.path); + fs.mkdirSync(this.path); + + if (firstRefresh) { + firstRefresh = false; + // Clean only when a test uses refresh. This allows for child processes to + // use the tmpdir and only the parent will clean on exit. + process.on('exit', onexit); + } +} + +function onexit() { + // Change directory to avoid possible EBUSY + // TODO(f3n67u): uncomment when `worker_thread.isMainThread` implemented + // if (isMainThread) + // process.chdir(testRoot); + + try { + rmSync(tmpPath); + } catch (e) { + console.error('Can\'t clean tmpdir:', tmpPath); + + const files = fs.readdirSync(tmpPath); + console.error('Files blocking:', files); + + if (files.some((f) => f.startsWith('.nfs'))) { + // Warn about NFS "silly rename" + console.error('Note: ".nfs*" might be files that were open and ' + + 'unlinked but not closed.'); + console.error('See http://nfs.sourceforge.net/#faq_d2 for details.'); + } + + console.error(); + throw e; + } +} + +module.exports = { + path: tmpPath, + refresh +}; diff --git a/cli/tests/node_compat/test/parallel/package.json b/cli/tests/node_compat/test/parallel/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/cli/tests/node_compat/test/parallel/package.json @@ -0,0 +1 @@ +{} diff --git a/cli/tests/node_compat/test/parallel/test-assert-fail.js b/cli/tests/node_compat/test/parallel/test-assert-fail.js new file mode 100644 index 0000000000..2aad9766d6 --- /dev/null +++ b/cli/tests/node_compat/test/parallel/test-assert-fail.js @@ -0,0 +1,51 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 18.12.1 +// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +// No args +assert.throws( + () => { assert.fail(); }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Failed', + operator: 'fail', + actual: undefined, + expected: undefined, + generatedMessage: true, + stack: /Failed/ + } +); + +// One arg = message +assert.throws(() => { + assert.fail('custom message'); +}, { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'custom message', + operator: 'fail', + actual: undefined, + expected: undefined, + generatedMessage: false +}); + +// One arg = Error +assert.throws(() => { + assert.fail(new TypeError('custom message')); +}, { + name: 'TypeError', + message: 'custom message' +}); + +Object.prototype.get = common.mustNotCall(); +assert.throws(() => assert.fail(''), { code: 'ERR_ASSERTION' }); +delete Object.prototype.get; diff --git a/cli/tests/node_compat/test/parallel/test-assert-strict-exists.js b/cli/tests/node_compat/test/parallel/test-assert-strict-exists.js new file mode 100644 index 0000000000..79139f1e5a --- /dev/null +++ b/cli/tests/node_compat/test/parallel/test-assert-strict-exists.js @@ -0,0 +1,13 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 18.12.1 +// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually + +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.strictEqual(require('assert/strict'), assert.strict); diff --git a/cli/tests/node_compat/test/parallel/test-btoa-atob.js b/cli/tests/node_compat/test/parallel/test-btoa-atob.js new file mode 100644 index 0000000000..b17f4d2a66 --- /dev/null +++ b/cli/tests/node_compat/test/parallel/test-btoa-atob.js @@ -0,0 +1,46 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 18.12.1 +// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually + +'use strict'; + +require('../common'); + +const { strictEqual, throws } = require('assert'); +const buffer = require('buffer'); + +// Exported on the global object +strictEqual(globalThis.atob, buffer.atob); +strictEqual(globalThis.btoa, buffer.btoa); + +// Throws type error on no argument passed +throws(() => buffer.atob(), /TypeError/); +throws(() => buffer.btoa(), /TypeError/); + +strictEqual(atob(' '), ''); +strictEqual(atob(' Y\fW\tJ\njZ A=\r= '), 'abcd'); + +strictEqual(atob(null), '\x9Eée'); +strictEqual(atob(NaN), '5£'); +strictEqual(atob(Infinity), '"wâ\x9E+r'); +strictEqual(atob(true), '¶»\x9E'); +strictEqual(atob(1234), '×mø'); +strictEqual(atob([]), ''); +strictEqual(atob({ toString: () => '' }), ''); +strictEqual(atob({ [Symbol.toPrimitive]: () => '' }), ''); + +throws(() => atob(Symbol()), /TypeError/); +[ + undefined, false, () => {}, {}, [1], + 0, 1, 0n, 1n, -Infinity, + 'a', 'a\n\n\n', '\ra\r\r', ' a ', '\t\t\ta', 'a\f\f\f', '\ta\r \n\f', +].forEach((value) => + // See #2 - https://html.spec.whatwg.org/multipage/webappapis.html#dom-atob + throws(() => atob(value), { + constructor: DOMException, + name: 'InvalidCharacterError', + code: 5, + })); diff --git a/ext/node/polyfills/internal/buffer.mjs b/ext/node/polyfills/internal/buffer.mjs index 506f905079..1600462cfe 100644 --- a/ext/node/polyfills/internal/buffer.mjs +++ b/ext/node/polyfills/internal/buffer.mjs @@ -20,6 +20,10 @@ import { normalizeEncoding } from "internal:deno_node/polyfills/internal/util.mj import { validateBuffer } from "internal:deno_node/polyfills/internal/validators.mjs"; import { isUint8Array } from "internal:deno_node/polyfills/internal/util/types.ts"; import { forgivingBase64Encode, forgivingBase64UrlEncode } from "internal:deno_web/00_infra.js"; +import { atob, btoa } from "internal:deno_web/05_base64.js"; +import { Blob } from "internal:deno_web/09_file.js"; + +export { atob, btoa, Blob }; const utf8Encoder = new TextEncoder(); @@ -1802,10 +1806,6 @@ function BufferBigIntNotDefined() { throw new Error("BigInt not supported"); } -export const atob = globalThis.atob; -export const Blob = globalThis.Blob; -export const btoa = globalThis.btoa; - export function readUInt48LE(buf, offset = 0) { validateNumber(offset, "offset"); const first = buf[offset]; diff --git a/tools/copyright_checker.js b/tools/copyright_checker.js index 8adab7a433..6017e25c4d 100644 --- a/tools/copyright_checker.js +++ b/tools/copyright_checker.js @@ -30,6 +30,7 @@ export async function checkCopyright() { ":!:test_util/wpt/**", ":!:cli/tools/init/templates/**", ":!:cli/tests/unit_node/testdata/**", + ":!:cli/tests/node_compat/test/**", // rust "*.rs",