diff options
author | Yoshiya Hinosawa <stibium121@gmail.com> | 2023-02-17 23:58:52 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-17 23:58:52 +0900 |
commit | a01af067d79e78ea8e2c21cf0ef92d86d425f8eb (patch) | |
tree | b6345e86f04b61effd2be9b5dfd8c48507de7d65 /cli/tests/node_compat/test | |
parent | 1a7666a6ca63b3dc6872d8f4631a2799af534159 (diff) |
test: add node compat tests (#17805)
Diffstat (limited to 'cli/tests/node_compat/test')
-rw-r--r-- | cli/tests/node_compat/test/common/index.js | 489 | ||||
-rw-r--r-- | cli/tests/node_compat/test/common/package.json | 1 | ||||
-rw-r--r-- | cli/tests/node_compat/test/common/tmpdir.js | 69 | ||||
-rw-r--r-- | cli/tests/node_compat/test/parallel/package.json | 1 | ||||
-rw-r--r-- | cli/tests/node_compat/test/parallel/test-assert-fail.js | 51 | ||||
-rw-r--r-- | cli/tests/node_compat/test/parallel/test-assert-strict-exists.js | 13 | ||||
-rw-r--r-- | cli/tests/node_compat/test/parallel/test-btoa-atob.js | 46 |
7 files changed, 670 insertions, 0 deletions
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 000000000..f93f7b7a6 --- /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 || "<anonymous>", + }; + } else { + context = { + minimum: criteria, + actual: 0, + stack: util.inspect(new Error()), + name: fn.name || "<anonymous>", + }; + } + + // 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 000000000..0967ef424 --- /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 000000000..d3ce98e45 --- /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 000000000..0967ef424 --- /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 000000000..2aad9766d --- /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 000000000..79139f1e5 --- /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 000000000..b17f4d2a6 --- /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, + })); |