diff options
Diffstat (limited to 'std/node/_util/_util_callbackify_test.ts')
-rw-r--r-- | std/node/_util/_util_callbackify_test.ts | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/std/node/_util/_util_callbackify_test.ts b/std/node/_util/_util_callbackify_test.ts new file mode 100644 index 000000000..b4dcae755 --- /dev/null +++ b/std/node/_util/_util_callbackify_test.ts @@ -0,0 +1,364 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +// +// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +const { test } = Deno; +import { assert, assertStrictEq } from "../../testing/asserts.ts"; +import { callbackify } from "./_util_callbackify.ts"; + +const values = [ + "hello world", + null, + undefined, + false, + 0, + {}, + { key: "value" }, + Symbol("I am a symbol"), + function ok(): void {}, + ["array", "with", 4, "values"], + new Error("boo"), +]; + +class TestQueue { + #waitingPromise: Promise<void>; + #resolve?: () => void; + #reject?: (err: unknown) => void; + #queueSize = 0; + + constructor() { + this.#waitingPromise = new Promise((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; + }); + } + + enqueue(fn: (done: () => void) => void): void { + this.#queueSize++; + try { + fn(() => { + this.#queueSize--; + if (this.#queueSize === 0) { + assert( + this.#resolve, + "Test setup error; async queue is missing #resolve" + ); + this.#resolve(); + } + }); + } catch (err) { + assert(this.#reject, "Test setup error; async queue is missing #reject"); + this.#reject(err); + } + } + + waitForCompletion(): Promise<void> { + return this.#waitingPromise; + } +} + +test("callbackify passes the resolution value as the second argument to the callback", async () => { + const testQueue = new TestQueue(); + + for (const value of values) { + // eslint-disable-next-line require-await + async function asyncFn(): Promise<typeof value> { + return value; + } + const cbAsyncFn = callbackify(asyncFn); + testQueue.enqueue((done) => { + cbAsyncFn((err: unknown, ret: unknown) => { + assertStrictEq(err, null); + assertStrictEq(ret, value); + done(); + }); + }); + + function promiseFn(): Promise<typeof value> { + return Promise.resolve(value); + } + const cbPromiseFn = callbackify(promiseFn); + testQueue.enqueue((done) => { + cbPromiseFn((err: unknown, ret: unknown) => { + assertStrictEq(err, null); + assertStrictEq(ret, value); + done(); + }); + }); + + function thenableFn(): PromiseLike<any> { + return { + then(onfulfilled): PromiseLike<any> { + assert(onfulfilled); + onfulfilled(value); + return this; + }, + }; + } + const cbThenableFn = callbackify(thenableFn); + testQueue.enqueue((done) => { + cbThenableFn((err: unknown, ret: unknown) => { + assertStrictEq(err, null); + assertStrictEq(ret, value); + done(); + }); + }); + } + + await testQueue.waitForCompletion(); +}); + +test("callbackify passes the rejection value as the first argument to the callback", async () => { + const testQueue = new TestQueue(); + + for (const value of values) { + // eslint-disable-next-line require-await + async function asyncFn(): Promise<never> { + return Promise.reject(value); + } + const cbAsyncFn = callbackify(asyncFn); + assertStrictEq(cbAsyncFn.length, 1); + assertStrictEq(cbAsyncFn.name, "asyncFnCallbackified"); + testQueue.enqueue((done) => { + cbAsyncFn((err: unknown, ret: unknown) => { + assertStrictEq(ret, undefined); + if (err instanceof Error) { + if ("reason" in err) { + assert(!value); + assertStrictEq((err as any).code, "ERR_FALSY_VALUE_REJECTION"); + assertStrictEq((err as any).reason, value); + } else { + assertStrictEq(String(value).endsWith(err.message), true); + } + } else { + assertStrictEq(err, value); + } + done(); + }); + }); + + function promiseFn(): Promise<never> { + return Promise.reject(value); + } + const obj = {}; + Object.defineProperty(promiseFn, "name", { + value: obj, + writable: false, + enumerable: false, + configurable: true, + }); + + const cbPromiseFn = callbackify(promiseFn); + assertStrictEq(promiseFn.name, obj); + testQueue.enqueue((done) => { + cbPromiseFn((err: unknown, ret: unknown) => { + assertStrictEq(ret, undefined); + if (err instanceof Error) { + if ("reason" in err) { + assert(!value); + assertStrictEq((err as any).code, "ERR_FALSY_VALUE_REJECTION"); + assertStrictEq((err as any).reason, value); + } else { + assertStrictEq(String(value).endsWith(err.message), true); + } + } else { + assertStrictEq(err, value); + } + done(); + }); + }); + + function thenableFn(): PromiseLike<never> { + return { + then(onfulfilled, onrejected): PromiseLike<never> { + assert(onrejected); + onrejected(value); + return this; + }, + }; + } + + const cbThenableFn = callbackify(thenableFn); + testQueue.enqueue((done) => { + cbThenableFn((err: unknown, ret: unknown) => { + assertStrictEq(ret, undefined); + if (err instanceof Error) { + if ("reason" in err) { + assert(!value); + assertStrictEq((err as any).code, "ERR_FALSY_VALUE_REJECTION"); + assertStrictEq((err as any).reason, value); + } else { + assertStrictEq(String(value).endsWith(err.message), true); + } + } else { + assertStrictEq(err, value); + } + done(); + }); + }); + } + + await testQueue.waitForCompletion(); +}); + +test("callbackify passes arguments to the original", async () => { + const testQueue = new TestQueue(); + + for (const value of values) { + // eslint-disable-next-line require-await + async function asyncFn<T>(arg: T): Promise<T> { + assertStrictEq(arg, value); + return arg; + } + + const cbAsyncFn = callbackify(asyncFn); + assertStrictEq(cbAsyncFn.length, 2); + assert(Object.getPrototypeOf(cbAsyncFn) !== Object.getPrototypeOf(asyncFn)); + assertStrictEq(Object.getPrototypeOf(cbAsyncFn), Function.prototype); + testQueue.enqueue((done) => { + cbAsyncFn(value, (err: unknown, ret: unknown) => { + assertStrictEq(err, null); + assertStrictEq(ret, value); + done(); + }); + }); + + function promiseFn<T>(arg: T): Promise<T> { + assertStrictEq(arg, value); + return Promise.resolve(arg); + } + const obj = {}; + Object.defineProperty(promiseFn, "length", { + value: obj, + writable: false, + enumerable: false, + configurable: true, + }); + + const cbPromiseFn = callbackify(promiseFn); + assertStrictEq(promiseFn.length, obj); + testQueue.enqueue((done) => { + cbPromiseFn(value, (err: unknown, ret: unknown) => { + assertStrictEq(err, null); + assertStrictEq(ret, value); + done(); + }); + }); + } + + await testQueue.waitForCompletion(); +}); + +test("callbackify preserves the `this` binding", async () => { + const testQueue = new TestQueue(); + + for (const value of values) { + const objectWithSyncFunction = { + fn(this: unknown, arg: typeof value): Promise<typeof value> { + assertStrictEq(this, objectWithSyncFunction); + return Promise.resolve(arg); + }, + }; + const cbSyncFunction = callbackify(objectWithSyncFunction.fn); + testQueue.enqueue((done) => { + cbSyncFunction.call(objectWithSyncFunction, value, function ( + this: unknown, + err: unknown, + ret: unknown + ) { + assertStrictEq(err, null); + assertStrictEq(ret, value); + assertStrictEq(this, objectWithSyncFunction); + done(); + }); + }); + + const objectWithAsyncFunction = { + // eslint-disable-next-line require-await + async fn(this: unknown, arg: typeof value): Promise<typeof value> { + assertStrictEq(this, objectWithAsyncFunction); + return arg; + }, + }; + const cbAsyncFunction = callbackify(objectWithAsyncFunction.fn); + testQueue.enqueue((done) => { + cbAsyncFunction.call(objectWithAsyncFunction, value, function ( + this: unknown, + err: unknown, + ret: unknown + ) { + assertStrictEq(err, null); + assertStrictEq(ret, value); + assertStrictEq(this, objectWithAsyncFunction); + done(); + }); + }); + } + + await testQueue.waitForCompletion(); +}); + +test("callbackify throws with non-function inputs", () => { + ["foo", null, undefined, false, 0, {}, Symbol(), []].forEach((value) => { + try { + callbackify(value as any); + throw Error("We should never reach this error"); + } catch (err) { + assert(err instanceof TypeError); + assertStrictEq((err as any).code, "ERR_INVALID_ARG_TYPE"); + assertStrictEq(err.name, "TypeError"); + assertStrictEq( + err.message, + 'The "original" argument must be of type function.' + ); + } + }); +}); + +test("callbackify returns a function that throws if the last argument is not a function", () => { + // eslint-disable-next-line require-await + async function asyncFn(): Promise<number> { + return 42; + } + + const cb = callbackify(asyncFn) as any; + const args: unknown[] = []; + + ["foo", null, undefined, false, 0, {}, Symbol(), []].forEach((value) => { + args.push(value); + + try { + cb(...args); + throw Error("We should never reach this error"); + } catch (err) { + assert(err instanceof TypeError); + assertStrictEq((err as any).code, "ERR_INVALID_ARG_TYPE"); + assertStrictEq(err.name, "TypeError"); + assertStrictEq( + err.message, + "The last argument must be of type function." + ); + } + }); +}); |