From c0a6e24857dad7e8061b93fade3d4b8fb68b4843 Mon Sep 17 00:00:00 2001 From: Michael Herzner <michael.b.herzner@gmail.com> Date: Wed, 10 Apr 2024 23:17:07 +0200 Subject: [PATCH 1/2] test(async): improve test coverage --- async/_util.ts | 9 +++++ async/_util_test.ts | 32 ++++++++++++++++-- async/abortable.ts | 11 ++----- async/delay.ts | 2 +- async/delay_test.ts | 34 +++++++++++++++++++ async/mux_async_iterator_test.ts | 56 +++++++++++++++++++++++++------- async/pool_test.ts | 4 +-- 7 files changed, 122 insertions(+), 26 deletions(-) diff --git a/async/_util.ts b/async/_util.ts index 31de9f0abc3c..235e5ed3c71b 100644 --- a/async/_util.ts +++ b/async/_util.ts @@ -1,6 +1,15 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // This module is browser compatible. +// This `reason` comes from `AbortSignal` thus must be `any`. +// deno-lint-ignore no-explicit-any +export function createAbortError(reason?: any): DOMException { + return new DOMException( + reason ? `Aborted: ${reason}` : "Aborted", + "AbortError", + ); +} + export function exponentialBackoffWithJitter( cap: number, base: number, diff --git a/async/_util_test.ts b/async/_util_test.ts index 7ff460a51c23..6c3fa49366b3 100644 --- a/async/_util_test.ts +++ b/async/_util_test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { exponentialBackoffWithJitter } from "./_util.ts"; -import { assertEquals } from "../assert/mod.ts"; +import { createAbortError, exponentialBackoffWithJitter } from "./_util.ts"; +import { assertEquals, assertInstanceOf } from "../assert/mod.ts"; // test util to ensure deterministic results during testing of backoff function by polyfilling Math.random function prngMulberry32(seed: number) { @@ -50,3 +50,31 @@ Deno.test("exponentialBackoffWithJitter()", () => { assertEquals(results as typeof row, row); } }); + +Deno.test("createAbortError()", () => { + const error = createAbortError(); + assertInstanceOf(error, DOMException); + assertEquals(error.name, "AbortError"); + assertEquals(error.message, "Aborted"); +}); + +Deno.test("createAbortError() handles aborted signal with reason", () => { + const c = new AbortController(); + c.abort("Expected Reason"); + const error = createAbortError(c.signal.reason); + assertInstanceOf(error, DOMException); + assertEquals(error.name, "AbortError"); + assertEquals(error.message, "Aborted: Expected Reason"); +}); + +Deno.test("createAbortError() handles aborted signal without reason", () => { + const c = new AbortController(); + c.abort(); + const error = createAbortError(c.signal.reason); + assertInstanceOf(error, DOMException); + assertEquals(error.name, "AbortError"); + assertEquals( + error.message, + "Aborted: AbortError: The signal has been aborted", + ); +}); diff --git a/async/abortable.ts b/async/abortable.ts index b25e187fc1a2..9549d4e3bd1a 100644 --- a/async/abortable.ts +++ b/async/abortable.ts @@ -1,6 +1,8 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // This module is browser compatible. +import { createAbortError } from "./_util.ts"; + /** * Make {@linkcode Promise} abortable with the given signal. * @@ -145,12 +147,3 @@ export async function* abortableAsyncIterable<T>( yield value; } } - -// This `reason` comes from `AbortSignal` thus must be `any`. -// deno-lint-ignore no-explicit-any -function createAbortError(reason?: any): DOMException { - return new DOMException( - reason ? `Aborted: ${reason}` : "Aborted", - "AbortError", - ); -} diff --git a/async/delay.ts b/async/delay.ts index 2e8449966846..f8023bd292e8 100644 --- a/async/delay.ts +++ b/async/delay.ts @@ -36,7 +36,7 @@ export interface DelayOptions { * ``` */ export function delay(ms: number, options: DelayOptions = {}): Promise<void> { - const { signal, persistent } = options; + const { signal, persistent = true } = options; if (signal?.aborted) return Promise.reject(signal.reason); return new Promise((resolve, reject) => { const abort = () => { diff --git a/async/delay_test.ts b/async/delay_test.ts index bc22b35a7332..f5d7fb6a34ed 100644 --- a/async/delay_test.ts +++ b/async/delay_test.ts @@ -2,10 +2,12 @@ import { delay } from "./delay.ts"; import { assert, + assertEquals, assertInstanceOf, assertRejects, assertStrictEquals, } from "../assert/mod.ts"; +import { assertSpyCalls, stub } from "../testing/mock.ts"; // https://dom.spec.whatwg.org/#interface-AbortSignal function assertIsDefaultAbortReason(reason: unknown) { @@ -103,3 +105,35 @@ Deno.test("delay() handles already aborted signal", async function () { assert(diff < 100); assertIsDefaultAbortReason(cause); }); + +Deno.test("delay() handles persitent option", async function () { + using unrefTimer = stub(Deno, "unrefTimer"); + await delay(100, { persistent: false }); + assertSpyCalls(unrefTimer, 1); +}); + +Deno.test("delay() handles persistent option with reference error", async function () { + using unrefTimer = stub(Deno, "unrefTimer", () => { + throw new ReferenceError(); + }); + await delay(100, { persistent: false }); + assertSpyCalls(unrefTimer, 1); +}); + +Deno.test({ + name: "delay() handles persistent option with error", + fn: async function () { + using unrefTimer = stub(Deno, "unrefTimer", () => { + throw new Error("Error!"); + }); + try { + await delay(100, { persistent: false }); + } catch (e) { + assert(e instanceof Error); + assertEquals(e.message, "Error!"); + assertSpyCalls(unrefTimer, 1); + } + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/async/mux_async_iterator_test.ts b/async/mux_async_iterator_test.ts index 72feb7c32717..7852b028713a 100644 --- a/async/mux_async_iterator_test.ts +++ b/async/mux_async_iterator_test.ts @@ -34,6 +34,26 @@ Deno.test("MuxAsyncIterator()", async function () { assertEquals(results, new Set([1, 2, 3, 4, 5, 6])); }); +Deno.test("MuxAsyncIterator() works with no iterables", async function () { + const mux = new MuxAsyncIterator<number>(); + const results = new Set(await Array.fromAsync(mux)); + assertEquals(results.size, 0); + assertEquals(results, new Set([])); +}); + +Deno.test("MuxAsyncIterator() clears iterables after successful iteration", async function () { + const mux = new MuxAsyncIterator<number>(); + mux.add(gen123()); + mux.add(gen456()); + const results = new Set(await Array.fromAsync(mux)); + assertEquals(results.size, 6); + assertEquals(results, new Set([1, 2, 3, 4, 5, 6])); + mux.add(gen123()); + const results2 = new Set(await Array.fromAsync(mux)); + assertEquals(results2.size, 3); + assertEquals(results2, new Set([1, 2, 3])); +}); + Deno.test("MuxAsyncIterator() takes async iterable as source", async function () { const mux = new MuxAsyncIterator<number>(); mux.add(new CustomAsyncIterable()); @@ -42,16 +62,28 @@ Deno.test("MuxAsyncIterator() takes async iterable as source", async function () assertEquals(results, new Set([1, 2, 3])); }); -Deno.test({ - name: "MuxAsyncIterator() throws when the source throws", - async fn() { - const mux = new MuxAsyncIterator<number>(); - mux.add(gen123()); - mux.add(genThrows()); - await assertRejects( - async () => await Array.fromAsync(mux), - Error, - "something went wrong", - ); - }, +Deno.test("MuxAsyncIterator() throws when the source throws", async function () { + const mux = new MuxAsyncIterator<number>(); + mux.add(gen123()); + mux.add(genThrows()); + await assertRejects( + async () => await Array.fromAsync(mux), + Error, + "something went wrong", + ); +}); + +Deno.test("MuxAsyncIterator() doesn't clear iterables after throwing", async function () { + const mux = new MuxAsyncIterator<number>(); + mux.add(genThrows()); + await assertRejects( + async () => await Array.fromAsync(mux), + Error, + "something went wrong", + ); + await assertRejects( + async () => await Array.fromAsync(mux), + Error, + "something went wrong", + ); }); diff --git a/async/pool_test.ts b/async/pool_test.ts index 3d005ba55b1a..8c2870cd1ce6 100644 --- a/async/pool_test.ts +++ b/async/pool_test.ts @@ -22,7 +22,7 @@ Deno.test("pooledMap()", async function () { assert(diff < 3000); }); -Deno.test("pooledMap() handles errors", async () => { +Deno.test("pooledMap() handles errors", async function () { async function mapNumber(n: number): Promise<number> { if (n <= 2) { throw new Error(`Bad number: ${n}`); @@ -46,7 +46,7 @@ Deno.test("pooledMap() handles errors", async () => { assertEquals(mappedNumbers, [3]); }); -Deno.test("pooledMap() returns ordered items", async () => { +Deno.test("pooledMap() returns ordered items", async function () { function getRandomInt(min: number, max: number): number { min = Math.ceil(min); max = Math.floor(max); From 656a3343695f09f736d5743b234150365b0258b8 Mon Sep 17 00:00:00 2001 From: Michael Herzner <michael.b.herzner@gmail.com> Date: Thu, 11 Apr 2024 22:39:08 +0200 Subject: [PATCH 2/2] chore: implement review remarks --- async/debounce_test.ts | 10 +++++----- async/delay_test.ts | 18 +++++++++--------- async/mux_async_iterator.ts | 1 - async/mux_async_iterator_test.ts | 12 ++++++------ async/pool_test.ts | 8 ++++---- async/retry_test.ts | 14 +++++++------- 6 files changed, 31 insertions(+), 32 deletions(-) diff --git a/async/debounce_test.ts b/async/debounce_test.ts index 8b187aadc750..9c4bb3f23936 100644 --- a/async/debounce_test.ts +++ b/async/debounce_test.ts @@ -3,7 +3,7 @@ import { assertEquals, assertStrictEquals } from "../assert/mod.ts"; import { debounce, type DebouncedFunction } from "./debounce.ts"; import { delay } from "./delay.ts"; -Deno.test("debounce() handles called", async function () { +Deno.test("debounce() handles called", async () => { let called = 0; const d = debounce(() => called++, 100); d(); @@ -16,7 +16,7 @@ Deno.test("debounce() handles called", async function () { assertEquals(d.pending, false); }); -Deno.test("debounce() handles cancelled", async function () { +Deno.test("debounce() handles cancelled", async () => { let called = 0; const d = debounce(() => called++, 100); d(); @@ -30,7 +30,7 @@ Deno.test("debounce() handles cancelled", async function () { assertEquals(d.pending, false); }); -Deno.test("debounce() handles flush", function () { +Deno.test("debounce() handles flush", () => { let called = 0; const d = debounce(() => called++, 100); d(); @@ -43,7 +43,7 @@ Deno.test("debounce() handles flush", function () { assertEquals(d.pending, false); }); -Deno.test("debounce() handles params and context", async function () { +Deno.test("debounce() handles params and context", async () => { const params: Array<string | number> = []; const d: DebouncedFunction<[string, number]> = debounce( function (param1: string, param2: number) { @@ -66,7 +66,7 @@ Deno.test("debounce() handles params and context", async function () { assertEquals(d.pending, false); }); -Deno.test("debounce() handles number and string types", async function () { +Deno.test("debounce() handles number and string types", async () => { const params: Array<string> = []; const fn = (param: string) => params.push(param); const d: DebouncedFunction<[string]> = debounce(fn, 100); diff --git a/async/delay_test.ts b/async/delay_test.ts index f5d7fb6a34ed..2a7f7c38d3c7 100644 --- a/async/delay_test.ts +++ b/async/delay_test.ts @@ -15,7 +15,7 @@ function assertIsDefaultAbortReason(reason: unknown) { assertStrictEquals(reason.name, "AbortError"); } -Deno.test("delay()", async function () { +Deno.test("delay()", async () => { const start = new Date(); const delayedPromise = delay(100); const result = await delayedPromise; @@ -24,7 +24,7 @@ Deno.test("delay()", async function () { assert(diff >= 100); }); -Deno.test("delay() handles abort", async function () { +Deno.test("delay() handles abort", async () => { const start = new Date(); const abort = new AbortController(); const { signal } = abort; @@ -36,7 +36,7 @@ Deno.test("delay() handles abort", async function () { assertIsDefaultAbortReason(cause); }); -Deno.test("delay() checks abort reason", async function (ctx) { +Deno.test("delay() checks abort reason", async (ctx) => { async function assertRejectsReason(reason: unknown) { const start = new Date(); const abort = new AbortController(); @@ -71,7 +71,7 @@ Deno.test("delay() checks abort reason", async function (ctx) { }); }); -Deno.test("delay() handles non-aborted signal", async function () { +Deno.test("delay() handles non-aborted signal", async () => { const start = new Date(); const abort = new AbortController(); const { signal } = abort; @@ -82,7 +82,7 @@ Deno.test("delay() handles non-aborted signal", async function () { assert(diff >= 100); }); -Deno.test("delay() handles aborted signal after delay", async function () { +Deno.test("delay() handles aborted signal after delay", async () => { const start = new Date(); const abort = new AbortController(); const { signal } = abort; @@ -94,7 +94,7 @@ Deno.test("delay() handles aborted signal after delay", async function () { assert(diff >= 100); }); -Deno.test("delay() handles already aborted signal", async function () { +Deno.test("delay() handles already aborted signal", async () => { const start = new Date(); const abort = new AbortController(); abort.abort(); @@ -106,13 +106,13 @@ Deno.test("delay() handles already aborted signal", async function () { assertIsDefaultAbortReason(cause); }); -Deno.test("delay() handles persitent option", async function () { +Deno.test("delay() handles persitent option", async () => { using unrefTimer = stub(Deno, "unrefTimer"); await delay(100, { persistent: false }); assertSpyCalls(unrefTimer, 1); }); -Deno.test("delay() handles persistent option with reference error", async function () { +Deno.test("delay() handles persistent option with reference error", async () => { using unrefTimer = stub(Deno, "unrefTimer", () => { throw new ReferenceError(); }); @@ -122,7 +122,7 @@ Deno.test("delay() handles persistent option with reference error", async functi Deno.test({ name: "delay() handles persistent option with error", - fn: async function () { + async fn() { using unrefTimer = stub(Deno, "unrefTimer", () => { throw new Error("Error!"); }); diff --git a/async/mux_async_iterator.ts b/async/mux_async_iterator.ts index 1bb4802881af..be53d0d8413a 100644 --- a/async/mux_async_iterator.ts +++ b/async/mux_async_iterator.ts @@ -82,7 +82,6 @@ export class MuxAsyncIterator<T> implements AsyncIterable<T> { for (const e of this.#throws) { throw e; } - this.#throws.length = 0; } // Clear the `yields` list and reset the `signal` promise. this.#yields.length = 0; diff --git a/async/mux_async_iterator_test.ts b/async/mux_async_iterator_test.ts index 7852b028713a..3fb883b0f022 100644 --- a/async/mux_async_iterator_test.ts +++ b/async/mux_async_iterator_test.ts @@ -25,7 +25,7 @@ class CustomAsyncIterable { } } -Deno.test("MuxAsyncIterator()", async function () { +Deno.test("MuxAsyncIterator()", async () => { const mux = new MuxAsyncIterator<number>(); mux.add(gen123()); mux.add(gen456()); @@ -34,14 +34,14 @@ Deno.test("MuxAsyncIterator()", async function () { assertEquals(results, new Set([1, 2, 3, 4, 5, 6])); }); -Deno.test("MuxAsyncIterator() works with no iterables", async function () { +Deno.test("MuxAsyncIterator() works with no iterables", async () => { const mux = new MuxAsyncIterator<number>(); const results = new Set(await Array.fromAsync(mux)); assertEquals(results.size, 0); assertEquals(results, new Set([])); }); -Deno.test("MuxAsyncIterator() clears iterables after successful iteration", async function () { +Deno.test("MuxAsyncIterator() clears iterables after successful iteration", async () => { const mux = new MuxAsyncIterator<number>(); mux.add(gen123()); mux.add(gen456()); @@ -54,7 +54,7 @@ Deno.test("MuxAsyncIterator() clears iterables after successful iteration", asyn assertEquals(results2, new Set([1, 2, 3])); }); -Deno.test("MuxAsyncIterator() takes async iterable as source", async function () { +Deno.test("MuxAsyncIterator() takes async iterable as source", async () => { const mux = new MuxAsyncIterator<number>(); mux.add(new CustomAsyncIterable()); const results = new Set(await Array.fromAsync(mux)); @@ -62,7 +62,7 @@ Deno.test("MuxAsyncIterator() takes async iterable as source", async function () assertEquals(results, new Set([1, 2, 3])); }); -Deno.test("MuxAsyncIterator() throws when the source throws", async function () { +Deno.test("MuxAsyncIterator() throws when the source throws", async () => { const mux = new MuxAsyncIterator<number>(); mux.add(gen123()); mux.add(genThrows()); @@ -73,7 +73,7 @@ Deno.test("MuxAsyncIterator() throws when the source throws", async function () ); }); -Deno.test("MuxAsyncIterator() doesn't clear iterables after throwing", async function () { +Deno.test("MuxAsyncIterator() doesn't clear iterables after throwing", async () => { const mux = new MuxAsyncIterator<number>(); mux.add(genThrows()); await assertRejects( diff --git a/async/pool_test.ts b/async/pool_test.ts index 8c2870cd1ce6..e49c38324251 100644 --- a/async/pool_test.ts +++ b/async/pool_test.ts @@ -8,7 +8,7 @@ import { assertStringIncludes, } from "../assert/mod.ts"; -Deno.test("pooledMap()", async function () { +Deno.test("pooledMap()", async () => { const start = new Date(); const results = pooledMap( 2, @@ -22,7 +22,7 @@ Deno.test("pooledMap()", async function () { assert(diff < 3000); }); -Deno.test("pooledMap() handles errors", async function () { +Deno.test("pooledMap() handles errors", async () => { async function mapNumber(n: number): Promise<number> { if (n <= 2) { throw new Error(`Bad number: ${n}`); @@ -46,7 +46,7 @@ Deno.test("pooledMap() handles errors", async function () { assertEquals(mappedNumbers, [3]); }); -Deno.test("pooledMap() returns ordered items", async function () { +Deno.test("pooledMap() returns ordered items", async () => { function getRandomInt(min: number, max: number): number { min = Math.ceil(min); max = Math.floor(max); @@ -66,7 +66,7 @@ Deno.test("pooledMap() returns ordered items", async function () { assertEquals(returned, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); }); -Deno.test("pooledMap() checks browser compat", async function () { +Deno.test("pooledMap() checks browser compat", async () => { // Simulates the environment where Symbol.asyncIterator is not available const asyncIterFunc = ReadableStream.prototype[Symbol.asyncIterator]; // deno-lint-ignore no-explicit-any diff --git a/async/retry_test.ts b/async/retry_test.ts index e348bc3d5593..7bf1a811a046 100644 --- a/async/retry_test.ts +++ b/async/retry_test.ts @@ -15,7 +15,7 @@ function generateErroringFunction(errorsBeforeSucceeds: number) { }; } -Deno.test("retry()", async function () { +Deno.test("retry()", async () => { const threeErrors = generateErroringFunction(3); const result = await retry(threeErrors, { minTimeout: 100, @@ -23,7 +23,7 @@ Deno.test("retry()", async function () { assertEquals(result, 3); }); -Deno.test("retry() fails after max errors is passed", async function () { +Deno.test("retry() fails after max errors is passed", async () => { const fiveErrors = generateErroringFunction(5); await assertRejects(() => retry(fiveErrors, { @@ -32,7 +32,7 @@ Deno.test("retry() fails after max errors is passed", async function () { ); }); -Deno.test("retry() waits four times by default", async function () { +Deno.test("retry() waits four times by default", async () => { let callCount = 0; const onlyErrors = function () { callCount++; @@ -56,7 +56,7 @@ Deno.test("retry() waits four times by default", async function () { Deno.test( "retry() throws if minTimeout is less than maxTimeout", - async function () { + async () => { await assertRejects(() => retry(() => {}, { minTimeout: 1000, @@ -68,7 +68,7 @@ Deno.test( Deno.test( "retry() throws if maxTimeout is less than 0", - async function () { + async () => { await assertRejects(() => retry(() => {}, { maxTimeout: -1, @@ -79,7 +79,7 @@ Deno.test( Deno.test( "retry() throws if jitter is bigger than 1", - async function () { + async () => { await assertRejects(() => retry(() => {}, { jitter: 2, @@ -91,7 +91,7 @@ Deno.test( Deno.test("retry() checks backoff function timings", async (t) => { const originalMathRandom = Math.random; - await t.step("wait fixed times without jitter", async function () { + await t.step("wait fixed times without jitter", async () => { using time = new FakeTime(); let resolved = false; const checkResolved = async () => {