From ab02ab25b1ea37f2861099f91e3783591e08efdd Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Wed, 8 Mar 2023 11:38:09 -0800 Subject: [PATCH] Improve test harness --- .prettierignore | 1 + test/bun.lockb | Bin 31909 -> 35688 bytes test/js/deno/.gitignore | 1 + test/js/deno/abort/abort-controller.test.ts | 97 ++++----- test/js/deno/fetch/body.test.ts | 90 ++++++++ test/js/deno/harness.ts | 230 +------------------- test/js/deno/harness/assert.ts | 207 ++++++++++++++++++ test/js/deno/harness/fixture.ts | 26 +++ test/js/deno/harness/global.ts | 45 ++++ test/js/deno/harness/util.ts | 5 + test/js/deno/html/blob.test.ts | 197 +++++++++-------- test/js/deno/resources/tests.json | 7 +- test/js/deno/scripts/postinstall.ts | 96 +++++++- test/package.json | 1 + 14 files changed, 618 insertions(+), 385 deletions(-) create mode 100644 test/js/deno/.gitignore create mode 100644 test/js/deno/fetch/body.test.ts create mode 100644 test/js/deno/harness/assert.ts create mode 100644 test/js/deno/harness/fixture.ts create mode 100644 test/js/deno/harness/global.ts create mode 100644 test/js/deno/harness/util.ts diff --git a/.prettierignore b/.prettierignore index 464309eabd..21683c3e86 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,4 @@ src/test/fixtures src/react-refresh.js test/snapshots test/snapshots-no-hmr +test/js/deno diff --git a/test/bun.lockb b/test/bun.lockb index f724750a9510e851a521267c387dfe19766f99ad..b05324ca6521f4cb60ee996d73f5daf3169e5c67 100755 GIT binary patch delta 9429 zcmeHNd0bOh_J1$3MRpBhK)_{Kv=Z47sYG1MU_ek%aDf03QrRS+#$^)gGE{2CUUvms zMe8;ywT|L4YPD4d9qL#em#S@*R;rF$$4<2B?|a`uqM-fE=Qq}W=6*c)+2QD!O0Z9qB&1oBYU|?|t{x!qv6!Y%Tax)V;33TKidRS7ePpo_cN`!?m{; zj`WfId&I&ioeec0I53P+H%XIUs$rNA%%zy;vpmiOrhG;s(2H`^x;%ynfLvS9KL?%i zHB*X9G&&tKNnN6+K4ex5V-I?%zE~5e)iDj2Q~3gIfhI5~i#fvcdl+@4>e4cuMrQ+p z69gW%5dzW_=1@adh511e%lb%bFr#LV6D{j6oiEI0ljW?x;kGcFsH1ue^FI91(Cz?J z%U6LNfj@u&Y9mk03(moW(l3Bdmwq00Ff{%Z%5;86!1bV0I|Z83yrLW(vkh}s%$ET} z#9%BdEY=nyhovC6fRSBPG(n4b9xq@_0d5bv7lNY!l&B{MW)?g-+WZ`*D~zhKdxVLMF)+p^3@GCaM_`Pf0aJnP0)3T0 zp9|ap^b%n{8rU84K>|GlnCf{8dvVTO!XUpsoWl5Hv?mmS zNMe{X=2+&;p>2Qsbmf76eEt2}6$J?q4)5qx<^E0eCug-+&5Ql}mBXt>uh)BdtZv%8 zx3hPOL(?2h+SDP{f4ZFiFf(P{`5()dcKcw%$&FEqi<9N|R^N?F7`w?qIcwU@+>_<) zZgj=PNEnLK-|e{f5#QU^Oz}K<5ols_Wr74k&XC8|EHfsNd}Gs*jV5(AO17Ao&~GPE zwo3L6(WCbzb?D282|eg`N>)nr=m(HG^m<}Ke~Ls&m8>h#OO;X|T$dm$m?g2&NH&p} zz?n~?+A7)eM324$sY9gtzh~7!bZXRL!xhkataF_av_9RvI!4vRvM34 z80VF))j;ZGO74XPmm_F~ai#D?Wc+Zd z{K*{~JJ&Qk9!9mwseK_w+KFL?wW^Z_&bU@NyTD0p!P(wc>Vk(-O9l-s8=Mg>R48W` zILWPYq}~iOv{gH~>x(bdVPY7n;G%vXKSjF3`s< zLoNbB{RI;@jU0A`kXWl9oH!K0pI|0%@X5TNN)ruAI`IxmtiZ& zL;+|7g9UktX_P~_-vfTYu|R0X)?}=Y56Sm&;i0Pw7ph8-u*B3<0)Q?I+2R_-bdJ1o zjbbX7!cRF&mogGSahgDX3FiEBf_NY@nS#R0FmJd+e~XA-8~|PM5&&KC3IJX4N&p`0%sK#V|0z!SH3e$)f0qDB z_}?XXWtZf2Ujq3HFToD^@A5kvBwgI5$_9Pb@Kh2|arDaur>a!PX@5A@*2X!_L4C8! zhvO;(zq)_Ne$?I#`xg9S{6e*-*NNPz4xie+*(qY$$_F2Rd>p5T#jQ00X8^=z^&siW zjztX-4V9CIF7MDmqyK9C30KceT7!T3kA3|fyY~vUt?~V2S*@Y{+KOEL>BR%8KGf7q z47;?Y>Cr9!X_>kIqK2qp`ooh+CqlF8Svkk}=e)jhXFJo@qt~KJ&bbe+HrD19rGMY& z%x-=DtV2$za~~|p`Pq2c=Z#wxRzK|s7`}7T1BL(0Xz%$OF5RC3JJc?H(BM%kZV#S3 z-!3}t|Fi3t@$RD+2g?FhkDs~vq+|4i`rZ@XC|k9+e!!vt+a$t1)oo`s?pN=dw#!fI zWq-Ry|HT!B-_6Yrr`nC-4C8eUI13;)tK&6eSKS|RzNztdVPiVipG)bvp=^P1g}bSL z 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 L`cVm_t-Ja^k3REK diff --git a/test/js/deno/.gitignore b/test/js/deno/.gitignore new file mode 100644 index 0000000000..2fb5beae2e --- /dev/null +++ b/test/js/deno/.gitignore @@ -0,0 +1 @@ +*.deno.* diff --git a/test/js/deno/abort/abort-controller.test.ts b/test/js/deno/abort/abort-controller.test.ts index 3aae2b623c..651cadf319 100644 --- a/test/js/deno/abort/abort-controller.test.ts +++ b/test/js/deno/abort/abort-controller.test.ts @@ -1,66 +1,59 @@ -// Updated: Wed, 08 Mar 2023 00:55:15 GMT -// URL: https://raw.githubusercontent.com/denoland/deno/main/cli/tests/unit/abort_controller_test.ts -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2018+ the Deno authors. All rights reserved. MIT license. +// https://raw.githubusercontent.com/denoland/deno/main/cli/tests/unit/abort_controller_test.ts import { assert, assertEquals } from "deno:harness"; - Deno.test(function basicAbortController() { - const controller = new AbortController(); - assert(controller); - const { signal } = controller; - assert(signal); - assertEquals(signal.aborted, false); - controller.abort(); - assertEquals(signal.aborted, true); + const controller = new AbortController(); + assert(controller); + const { signal } = controller; + assert(signal); + assertEquals(signal.aborted, false); + controller.abort(); + assertEquals(signal.aborted, true); }); - Deno.test(function signalCallsOnabort() { - const controller = new AbortController(); - const { signal } = controller; - let called = false; - signal.onabort = evt => { - assert(evt); - assertEquals(evt.type, "abort"); - called = true; - }; - controller.abort(); - assert(called); + const controller = new AbortController(); + const { signal } = controller; + let called = false; + signal.onabort = (evt)=>{ + assert(evt); + assertEquals(evt.type, "abort"); + called = true; + }; + controller.abort(); + assert(called); }); - Deno.test(function signalEventListener() { - const controller = new AbortController(); - const { signal } = controller; - let called = false; - signal.addEventListener("abort", function (ev) { - assert(this === signal); - assertEquals(ev.type, "abort"); - called = true; - }); - controller.abort(); - assert(called); + const controller = new AbortController(); + const { signal } = controller; + let called = false; + signal.addEventListener("abort", function(ev) { + assert(this === signal); + assertEquals(ev.type, "abort"); + called = true; + }); + controller.abort(); + assert(called); }); - Deno.test(function onlyAbortsOnce() { - const controller = new AbortController(); - const { signal } = controller; - let called = 0; - signal.addEventListener("abort", () => called++); - signal.onabort = () => { - called++; - }; - controller.abort(); - assertEquals(called, 2); - controller.abort(); - assertEquals(called, 2); + const controller = new AbortController(); + const { signal } = controller; + let called = 0; + signal.addEventListener("abort", ()=>called++); + signal.onabort = ()=>{ + called++; + }; + controller.abort(); + assertEquals(called, 2); + controller.abort(); + assertEquals(called, 2); }); - Deno.test(function controllerHasProperToString() { - const actual = Object.prototype.toString.call(new AbortController()); - assertEquals(actual, "[object AbortController]"); + const actual = Object.prototype.toString.call(new AbortController()); + assertEquals(actual, "[object AbortController]"); }); - Deno.test(function abortReason() { - const signal = AbortSignal.abort("hey!"); - assertEquals(signal.aborted, true); - assertEquals(signal.reason, "hey!"); + const signal = AbortSignal.abort("hey!"); + assertEquals(signal.aborted, true); + assertEquals(signal.reason, "hey!"); }); diff --git a/test/js/deno/fetch/body.test.ts b/test/js/deno/fetch/body.test.ts new file mode 100644 index 0000000000..92ad70c0aa --- /dev/null +++ b/test/js/deno/fetch/body.test.ts @@ -0,0 +1,90 @@ +// Copyright 2018+ the Deno authors. All rights reserved. MIT license. +// https://raw.githubusercontent.com/denoland/deno/main/cli/tests/unit/body_test.ts + +import { assert, assertEquals } from "deno:harness"; +function buildBody(body: any, headers?: Headers): Body { + const stub = new Request("http://foo/", { + body: body, + headers, + method: "POST" + }); + return stub as Body; +} +const intArrays = [ + Int8Array, + Int16Array, + Int32Array, + Uint8Array, + Uint16Array, + Uint32Array, + Uint8ClampedArray, + Float32Array, + Float64Array +]; +Deno.test(async function arrayBufferFromByteArrays() { + const buffer = new TextEncoder().encode("ahoyhoy8").buffer; + for (const type of intArrays){ + const body = buildBody(new type(buffer)); + const text = new TextDecoder("utf-8").decode(await body.arrayBuffer()); + assertEquals(text, "ahoyhoy8"); + } +}); +Deno.test({ + permissions: { + net: true + } +}, async function bodyMultipartFormData() { + const response = await fetch("http://localhost:4545/multipart_form_data.txt"); + assert(response.body instanceof ReadableStream); + const text = await response.text(); + const body = buildBody(text, response.headers); + const formData = await body.formData(); + assert(formData.has("field_1")); + assertEquals(formData.get("field_1")!.toString(), "value_1 \r\n"); + assert(formData.has("field_2")); +}); +Deno.test({ + permissions: { + net: true + } +}, async function bodyURLEncodedFormData() { + const response = await fetch("http://localhost:4545/subdir/form_urlencoded.txt"); + assert(response.body instanceof ReadableStream); + const text = await response.text(); + const body = buildBody(text, response.headers); + const formData = await body.formData(); + assert(formData.has("field_1")); + assertEquals(formData.get("field_1")!.toString(), "Hi"); + assert(formData.has("field_2")); + assertEquals(formData.get("field_2")!.toString(), ""); +}); +Deno.test({ + permissions: {} +}, async function bodyURLSearchParams() { + const body = buildBody(new URLSearchParams({ + hello: "world" + })); + const text = await body.text(); + assertEquals(text, "hello=world"); +}); +Deno.test(async function bodyArrayBufferMultipleParts() { + const parts: Uint8Array[] = []; + let size = 0; + for(let i = 0; i <= 150000; i++){ + const part = new Uint8Array([ + 1 + ]); + parts.push(part); + size += part.length; + } + let offset = 0; + const stream = new ReadableStream({ + pull (controller) { + const chunk = parts[offset++]; + if (!chunk) return controller.close(); + controller.enqueue(chunk); + } + }); + const body = buildBody(stream); + assertEquals((await body.arrayBuffer()).byteLength, size); +}); diff --git a/test/js/deno/harness.ts b/test/js/deno/harness.ts index 167860d1f3..8634d52616 100644 --- a/test/js/deno/harness.ts +++ b/test/js/deno/harness.ts @@ -1,226 +1,4 @@ -// Deno's test utilities implemented using expect(). -// https://github.com/denoland/deno/blob/main/cli/tests/unit/test_util.ts - -import { concatArrayBuffers } from "bun"; -import { it, expect } from "bun:test"; - -export function test(fn: () => void): void { - it(fn.name, fn); -} - -export function assert(condition: unknown, message?: string): asserts condition is true { - if (message) { - it(message, () => assert(condition)); - } else { - expect(condition).toBeTruthy(); - } -} - -export function assertFalse(condition: unknown, message?: string): asserts condition is false { - if (message) { - it(message, () => assertFalse(condition)); - } else { - expect(condition).toBeFalsy(); - } -} - -export function assertEquals(actual: unknown, expected: unknown, message?: string): void { - if (message) { - it(message, () => assertEquals(actual, expected)); - } else { - expect(actual).toEqual(expected); - } -} - -export function assertExists(value: unknown, message?: string): void { - if (message) { - it(message, () => assertExists(value)); - } else { - expect(value).toBeDefined(); - } -} - -export function assertNotEquals(actual: unknown, expected: unknown, message?: string): void { - if (message) { - it(message, () => assertNotEquals(actual, expected)); - } else { - expect(actual).not.toEqual(expected); - } -} - -export function assertStrictEquals(actual: unknown, expected: unknown, message?: string): void { - if (message) { - it(message, () => assertStrictEquals(actual, expected)); - } else { - expect(actual).toStrictEqual(expected); - } -} - -export function assertNotStrictEquals(actual: unknown, expected: unknown, message?: string): void { - if (message) { - it(message, () => assertNotStrictEquals(actual, expected)); - } else { - expect(actual).not.toStrictEqual(expected); - } -} - -export function assertAlmostEquals(actual: unknown, expected: number, epsilon: number = 1e-7, message?: string): void { - if (message) { - it(message, () => assertAlmostEquals(actual, expected)); - } else if (typeof actual === "number") { - // TODO: toBeCloseTo() - expect(Math.abs(actual - expected)).toBeLessThanOrEqual(epsilon); - } else { - expect(typeof actual).toBe("number"); - } -} - -export function assertInstanceOf(actual: unknown, expected: unknown, message?: string): void { - if (message) { - it(message, () => assertInstanceOf(actual, expected)); - } else if (typeof actual === "object") { - if (actual !== null) { - expect(actual).toHaveProperty("constructor", expected); - } else { - expect(actual).not.toBeNull(); - } - } else { - expect(typeof actual).toBe("object"); - } -} - -export function assertNotInstanceOf(actual: unknown, expected: unknown, message?: string): void { - if (message) { - it(message, () => assertNotInstanceOf(actual, expected)); - } else if (typeof actual === "object") { - if (actual !== null) { - expect(actual).not.toHaveProperty("constructor", expected); - } else { - expect(actual).not.toBeNull(); - } - } else { - expect(typeof actual).toBe("object"); - } -} - -export function assertStringIncludes(actual: unknown, expected: string, message?: string): void { - if (message) { - it(message, () => assertStringIncludes(actual, expected)); - } else if (typeof actual === "string") { - expect(actual).toContain(expected); - } else { - expect(typeof actual).toBe("string"); - } -} - -export function assertArrayIncludes(actual: unknown, expected: unknown[], message?: string): void { - if (message) { - it(message, () => assertArrayIncludes(actual, expected)); - } else if (Array.isArray(actual)) { - for (const value of expected) { - expect(actual).toContain(value); - } - } else { - expect(Array.isArray(actual)).toBe(true); - } -} - -export function assertMatch(actual: unknown, expected: RegExp, message?: string): void { - if (message) { - it(message, () => assertMatch(actual, expected)); - } else if (typeof actual === "string") { - expect(expected.test(actual)).toBe(true); - } else { - expect(typeof actual).toBe("string"); - } -} - -export function assertNotMatch(actual: unknown, expected: RegExp, message?: string): void { - if (message) { - it(message, () => assertNotMatch(actual, expected)); - } else if (typeof actual === "string") { - expect(expected.test(actual)).toBe(false); - } else { - expect(typeof actual).toBe("string"); - } -} - -export function assertObjectMatch(actual: unknown, expected: Record, message?: string): void { - if (message) { - it(message, () => assertObjectMatch(actual, expected)); - } else if (typeof actual === "object") { - // TODO: toMatchObject() - if (actual !== null) { - const expectedKeys = Object.keys(expected); - for (const key of Object.keys(actual)) { - if (!expectedKeys.includes(key)) { - // @ts-ignore - delete actual[key]; - } - } - expect(actual).toEqual(expected); - } else { - expect(actual).not.toBeNull(); - } - } else { - expect(typeof actual).toBe("object"); - } -} - -export function assertThrows(fn: () => void, message?: string): void { - if (message) { - it(message, () => assertThrows(fn)); - } else { - try { - fn(); - } catch (error) { - expect(error).toBeDefined(); - return; - } - throw new Error("Expected an error to be thrown"); - } -} - -export async function assertRejects(fn: () => Promise, message?: string): Promise { - if (message) { - it(message, () => assertRejects(fn)); - } else { - try { - await fn(); - } catch (error) { - expect(error).toBeDefined(); - return; - } - throw new Error("Expected an error to be thrown"); - } -} - -export function equal(a: unknown, b: unknown): boolean { - return Bun.deepEquals(a, b); -} - -export function fail(message: string): never { - throw new Error(message); -} - -export function unimplemented(message: string): never { - throw new Error(`Unimplemented: ${message}`); -} - -export function unreachable(): never { - throw new Error("Unreachable"); -} - -export function concat(...buffers: Uint8Array[]): Uint8Array { - return new Uint8Array(concatArrayBuffers(buffers)); -} - -export function inspect(...args: unknown[]): string { - return Bun.inspect(...args); -} - -// @ts-expect-error -globalThis["Deno"] = { - test, - inspect, -}; +export * from "./harness/global.js"; +export * from "./harness/util.js"; +export * from "./harness/assert.js"; +export * from "./harness/fixture.js"; diff --git a/test/js/deno/harness/assert.ts b/test/js/deno/harness/assert.ts new file mode 100644 index 0000000000..ba35701153 --- /dev/null +++ b/test/js/deno/harness/assert.ts @@ -0,0 +1,207 @@ +// Deno's test assertions implemented using expect(). +// https://github.com/denoland/deno/blob/main/cli/tests/unit/test_util.ts + +import { test, expect } from "bun:test"; + +export function assert(condition: unknown, message?: string): asserts condition is true { + if (message) { + test(message, () => assert(condition)); + } else { + expect(condition).toBeTruthy(); + } +} + +export function assertFalse(condition: unknown, message?: string): asserts condition is false { + if (message) { + test(message, () => assertFalse(condition)); + } else { + expect(condition).toBeFalsy(); + } +} + +export function assertEquals(actual: unknown, expected: unknown, message?: string): void { + if (message) { + test(message, () => assertEquals(actual, expected)); + } else { + expect(actual).toEqual(expected); + } +} + +export function assertExists(value: unknown, message?: string): void { + if (message) { + test(message, () => assertExists(value)); + } else { + expect(value).toBeDefined(); + } +} + +export function assertNotEquals(actual: unknown, expected: unknown, message?: string): void { + if (message) { + test(message, () => assertNotEquals(actual, expected)); + } else { + expect(actual).not.toEqual(expected); + } +} + +export function assertStrictEquals(actual: unknown, expected: unknown, message?: string): void { + if (message) { + test(message, () => assertStrictEquals(actual, expected)); + } else { + expect(actual).toStrictEqual(expected); + } +} + +export function assertNotStrictEquals(actual: unknown, expected: unknown, message?: string): void { + if (message) { + test(message, () => assertNotStrictEquals(actual, expected)); + } else { + expect(actual).not.toStrictEqual(expected); + } +} + +export function assertAlmostEquals(actual: unknown, expected: number, epsilon: number = 1e-7, message?: string): void { + if (message) { + test(message, () => assertAlmostEquals(actual, expected)); + } else if (typeof actual === "number") { + // TODO: toBeCloseTo() + expect(Math.abs(actual - expected)).toBeLessThanOrEqual(epsilon); + } else { + expect(typeof actual).toBe("number"); + } +} + +export function assertInstanceOf(actual: unknown, expected: unknown, message?: string): void { + if (message) { + test(message, () => assertInstanceOf(actual, expected)); + } else if (typeof actual === "object") { + if (actual !== null) { + expect(actual).toHaveProperty("constructor", expected); + } else { + expect(actual).not.toBeNull(); + } + } else { + expect(typeof actual).toBe("object"); + } +} + +export function assertNotInstanceOf(actual: unknown, expected: unknown, message?: string): void { + if (message) { + test(message, () => assertNotInstanceOf(actual, expected)); + } else if (typeof actual === "object") { + if (actual !== null) { + expect(actual).not.toHaveProperty("constructor", expected); + } else { + expect(actual).not.toBeNull(); + } + } else { + expect(typeof actual).toBe("object"); + } +} + +export function assertStringIncludes(actual: unknown, expected: string, message?: string): void { + if (message) { + test(message, () => assertStringIncludes(actual, expected)); + } else if (typeof actual === "string") { + expect(actual).toContain(expected); + } else { + expect(typeof actual).toBe("string"); + } +} + +export function assertArrayIncludes(actual: unknown, expected: unknown[], message?: string): void { + if (message) { + test(message, () => assertArrayIncludes(actual, expected)); + } else if (Array.isArray(actual)) { + for (const value of expected) { + expect(actual).toContain(value); + } + } else { + expect(Array.isArray(actual)).toBe(true); + } +} + +export function assertMatch(actual: unknown, expected: RegExp, message?: string): void { + if (message) { + test(message, () => assertMatch(actual, expected)); + } else if (typeof actual === "string") { + expect(expected.test(actual)).toBe(true); + } else { + expect(typeof actual).toBe("string"); + } +} + +export function assertNotMatch(actual: unknown, expected: RegExp, message?: string): void { + if (message) { + test(message, () => assertNotMatch(actual, expected)); + } else if (typeof actual === "string") { + expect(expected.test(actual)).toBe(false); + } else { + expect(typeof actual).toBe("string"); + } +} + +export function assertObjectMatch(actual: unknown, expected: Record, message?: string): void { + if (message) { + test(message, () => assertObjectMatch(actual, expected)); + } else if (typeof actual === "object") { + // TODO: toMatchObject() + if (actual !== null) { + const expectedKeys = Object.keys(expected); + for (const key of Object.keys(actual)) { + if (!expectedKeys.includes(key)) { + // @ts-ignore + delete actual[key]; + } + } + expect(actual).toEqual(expected); + } else { + expect(actual).not.toBeNull(); + } + } else { + expect(typeof actual).toBe("object"); + } +} + +export function assertThrows(fn: () => void, message?: string): void { + if (message) { + test(message, () => assertThrows(fn)); + } else { + try { + fn(); + } catch (error) { + expect(error).toBeDefined(); + return; + } + throw new Error("Expected an error to be thrown"); + } +} + +export async function assertRejects(fn: () => Promise, message?: string): Promise { + if (message) { + test(message, () => assertRejects(fn)); + } else { + try { + await fn(); + } catch (error) { + expect(error).toBeDefined(); + return; + } + throw new Error("Expected an error to be thrown"); + } +} + +export function equal(a: unknown, b: unknown): boolean { + return Bun.deepEquals(a, b); +} + +export function fail(message: string): never { + throw new Error(message); +} + +export function unimplemented(message: string): never { + throw new Error(`Unimplemented: ${message}`); +} + +export function unreachable(): never { + throw new Error("Unreachable"); +} diff --git a/test/js/deno/harness/fixture.ts b/test/js/deno/harness/fixture.ts new file mode 100644 index 0000000000..13a531e522 --- /dev/null +++ b/test/js/deno/harness/fixture.ts @@ -0,0 +1,26 @@ +import type { Server } from "bun"; +import { serve } from "bun"; +import { afterAll, beforeAll } from "bun:test"; + +let server: Server; + +beforeAll(() => { + server = serve({ + port: 4545, + fetch(request: Request): Response { + const { url } = request; + const { pathname, search } = new URL(url); + const redirect = new URL( + `${pathname}?${search}`, + "https://raw.githubusercontent.com/denoland/deno/main/cli/tests/testdata/", + ); + return Response.redirect(redirect.toString()); + }, + }); +}); + +afterAll(() => { + if (server) { + server.stop(true); + } +}); diff --git a/test/js/deno/harness/global.ts b/test/js/deno/harness/global.ts new file mode 100644 index 0000000000..4992f026fb --- /dev/null +++ b/test/js/deno/harness/global.ts @@ -0,0 +1,45 @@ +import { test as bunTest } from "bun:test"; + +type Fn = () => void | Promise; +type Options = { + permissions?: { + net?: boolean; + }; + ignore?: boolean; +}; + +function test(arg0: Fn | Options, arg1?: Fn): void { + if (typeof arg0 === "function") { + bunTest(arg0.name, arg0); + } else if (typeof arg1 === "function") { + if (arg0?.ignore === true || arg0?.permissions?.net === false) { + bunTest.skip(arg1.name, arg1); + } else { + bunTest(arg1.name, arg1); + } + } else { + throw new Error("Unimplemented"); + } +} + +test.ignore = (arg0: Fn | Options, arg1?: Fn) => { + if (typeof arg0 === "function") { + bunTest.skip(arg0.name, arg0); + } else if (typeof arg1 === "function") { + bunTest.skip(arg1.name, arg1); + } else { + throw new Error("Unimplemented"); + } +}; + +export function inspect(...args: unknown[]): string { + return Bun.inspect(...args); +} + +export const Deno = { + test, + inspect, +}; + +// @ts-expect-error +globalThis["Deno"] = Deno; diff --git a/test/js/deno/harness/util.ts b/test/js/deno/harness/util.ts new file mode 100644 index 0000000000..a7fbf7fce1 --- /dev/null +++ b/test/js/deno/harness/util.ts @@ -0,0 +1,5 @@ +import { concatArrayBuffers } from "bun"; + +export function concat(...buffers: Uint8Array[]): Uint8Array { + return new Uint8Array(concatArrayBuffers(buffers)); +} diff --git a/test/js/deno/html/blob.test.ts b/test/js/deno/html/blob.test.ts index af1e7f6e8d..306d9ca590 100644 --- a/test/js/deno/html/blob.test.ts +++ b/test/js/deno/html/blob.test.ts @@ -1,114 +1,119 @@ -// Updated: Wed, 08 Mar 2023 00:55:15 GMT -// URL: https://raw.githubusercontent.com/denoland/deno/main/cli/tests/unit/blob_test.ts -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2018+ the Deno authors. All rights reserved. MIT license. +// https://raw.githubusercontent.com/denoland/deno/main/cli/tests/unit/blob_test.ts + import { assert, assertEquals, assertStringIncludes } from "deno:harness"; import { concat } from "deno:harness"; - Deno.test(function blobString() { - const b1 = new Blob(["Hello World"]); - const str = "Test"; - const b2 = new Blob([b1, str]); - assertEquals(b2.size, b1.size + str.length); + const b1 = new Blob([ + "Hello World" + ]); + const str = "Test"; + const b2 = new Blob([ + b1, + str + ]); + assertEquals(b2.size, b1.size + str.length); }); - Deno.test(function blobBuffer() { - const buffer = new ArrayBuffer(12); - const u8 = new Uint8Array(buffer); - const f1 = new Float32Array(buffer); - const b1 = new Blob([buffer, u8]); - assertEquals(b1.size, 2 * u8.length); - const b2 = new Blob([b1, f1]); - assertEquals(b2.size, 3 * u8.length); + const buffer = new ArrayBuffer(12); + const u8 = new Uint8Array(buffer); + const f1 = new Float32Array(buffer); + const b1 = new Blob([ + buffer, + u8 + ]); + assertEquals(b1.size, 2 * u8.length); + const b2 = new Blob([ + b1, + f1 + ]); + assertEquals(b2.size, 3 * u8.length); }); - Deno.test(function blobSlice() { - const blob = new Blob(["Deno", "Foo"]); - const b1 = blob.slice(0, 3, "Text/HTML"); - assert(b1 instanceof Blob); - assertEquals(b1.size, 3); - assertEquals(b1.type, "text/html"); - const b2 = blob.slice(-1, 3); - assertEquals(b2.size, 0); - const b3 = blob.slice(100, 3); - assertEquals(b3.size, 0); - const b4 = blob.slice(0, 10); - assertEquals(b4.size, blob.size); + const blob = new Blob([ + "Deno", + "Foo" + ]); + const b1 = blob.slice(0, 3, "Text/HTML"); + assert(b1 instanceof Blob); + assertEquals(b1.size, 3); + assertEquals(b1.type, "text/html"); + const b2 = blob.slice(-1, 3); + assertEquals(b2.size, 0); + const b3 = blob.slice(100, 3); + assertEquals(b3.size, 0); + const b4 = blob.slice(0, 10); + assertEquals(b4.size, blob.size); }); - Deno.test(function blobInvalidType() { - const blob = new Blob(["foo"], { - type: "\u0521", - }); - - assertEquals(blob.type, ""); + const blob = new Blob([ + "foo" + ], { + type: "\u0521" + }); + assertEquals(blob.type, ""); }); - Deno.test(function blobShouldNotThrowError() { - let hasThrown = false; - - try { - // deno-lint-ignore no-explicit-any - const options1: any = { - ending: "utf8", - hasOwnProperty: "hasOwnProperty", - }; - const options2 = Object.create(null); - new Blob(["Hello World"], options1); - new Blob(["Hello World"], options2); - } catch { - hasThrown = true; - } - - assertEquals(hasThrown, false); -}); - -/* TODO https://github.com/denoland/deno/issues/7540 -Deno.test(function nativeEndLine() { - const options = { - ending: "native", - } as const; - const blob = new Blob(["Hello\nWorld"], options); - - assertEquals(blob.size, Deno.build.os === "windows" ? 12 : 11); -}); -*/ - -Deno.test(async function blobText() { - const blob = new Blob(["Hello World"]); - assertEquals(await blob.text(), "Hello World"); -}); - -Deno.test(async function blobStream() { - const blob = new Blob(["Hello World"]); - const stream = blob.stream(); - assert(stream instanceof ReadableStream); - const reader = stream.getReader(); - let bytes = new Uint8Array(); - const read = async (): Promise => { - const { done, value } = await reader.read(); - if (!done && value) { - bytes = concat(bytes, value); - return read(); + let hasThrown = false; + try { + const options1: any = { + ending: "utf8", + hasOwnProperty: "hasOwnProperty" + }; + const options2 = Object.create(null); + new Blob([ + "Hello World" + ], options1); + new Blob([ + "Hello World" + ], options2); + } catch { + hasThrown = true; } - }; - await read(); - const decoder = new TextDecoder(); - assertEquals(decoder.decode(bytes), "Hello World"); + assertEquals(hasThrown, false); +}); +Deno.test(async function blobText() { + const blob = new Blob([ + "Hello World" + ]); + assertEquals(await blob.text(), "Hello World"); +}); +Deno.test(async function blobStream() { + const blob = new Blob([ + "Hello World" + ]); + const stream = blob.stream(); + assert(stream instanceof ReadableStream); + const reader = stream.getReader(); + let bytes = new Uint8Array(); + const read = async (): Promise =>{ + const { done , value } = await reader.read(); + if (!done && value) { + bytes = concat(bytes, value); + return read(); + } + }; + await read(); + const decoder = new TextDecoder(); + assertEquals(decoder.decode(bytes), "Hello World"); }); - Deno.test(async function blobArrayBuffer() { - const uint = new Uint8Array([102, 111, 111]); - const blob = new Blob([uint]); - assertEquals(await blob.arrayBuffer(), uint.buffer); + const uint = new Uint8Array([ + 102, + 111, + 111 + ]); + const blob = new Blob([ + uint + ]); + assertEquals(await blob.arrayBuffer(), uint.buffer); }); - Deno.test(function blobConstructorNameIsBlob() { - const blob = new Blob(); - assertEquals(blob.constructor.name, "Blob"); + const blob = new Blob(); + assertEquals(blob.constructor.name, "Blob"); }); - -Deno.test(function blobCustomInspectFunction() { - const blob = new Blob(); - assertEquals(Deno.inspect(blob), `Blob { size: 0, type: "" }`); - assertStringIncludes(Deno.inspect(Blob.prototype), "Blob"); +Deno.test.ignore(function blobCustomInspectFunction() { + const blob = new Blob(); + assertEquals(Deno.inspect(blob), `Blob { size: 0, type: "" }`); + assertStringIncludes(Deno.inspect(Blob.prototype), "Blob"); }); diff --git a/test/js/deno/resources/tests.json b/test/js/deno/resources/tests.json index 067cb7205b..7de35dd787 100644 --- a/test/js/deno/resources/tests.json +++ b/test/js/deno/resources/tests.json @@ -5,6 +5,11 @@ }, { "path": "html/blob.test.ts", - "remotePath": "unit/blob_test.ts" + "remotePath": "unit/blob_test.ts", + "skip": ["blobCustomInspectFunction"] + }, + { + "path": "fetch/body.test.ts", + "remotePath": "unit/body_test.ts" } ] diff --git a/test/js/deno/scripts/postinstall.ts b/test/js/deno/scripts/postinstall.ts index bca1e65822..65fd5a04e9 100644 --- a/test/js/deno/scripts/postinstall.ts +++ b/test/js/deno/scripts/postinstall.ts @@ -1,26 +1,102 @@ import { mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; +import { parse, print } from "@swc/core"; +import type { ImportDeclaration, ExpressionStatement, CallExpression } from "@swc/core"; import imports from "../resources/imports.json"; import tests from "../resources/tests.json"; +// FIXME: https://github.com/oven-sh/bun/issues/2350 +// import * as harness from "deno:harness"; + for (const test of tests) { const path = join(import.meta.dir, "..", test.path); const url = new URL(test.remotePath, "https://raw.githubusercontent.com/denoland/deno/main/cli/tests/"); const response = await fetch(url); console.log(response.status, url.toString(), "->", test.path); if (!response.ok) { - throw new Error(`Failed to download from GitHub: ${url} [status: ${response.status}]`); + continue; } - let body = await response.text(); - for (const query of imports) { - const pattern = new RegExp(`"(.*${query})"`, "gmi"); - body = body.replace(pattern, '"deno:harness"'); - } - const src = `// Updated: ${response.headers.get("Date")} -// URL: ${url} -${body}`; + const src = await response.text(); + const dst = await visit(src, test); try { mkdirSync(dirname(path)); } catch {} - await Bun.write(path, src); + await Bun.write(path.replace(".test.", ".deno."), src); + await Bun.write(path, dst); +} + +async function visit(src: string, test: any): Promise { + const ast = await parse(src, { + syntax: "typescript", + target: "esnext", + dynamicImport: true, + }); + for (const item of ast.body) { + if (item.type === "ImportDeclaration") { + visitImport(item); + } + if (item.type === "ExpressionStatement") { + visitExpression(item, test); + } + } + const header = `// Copyright 2018+ the Deno authors. All rights reserved. MIT license. +// https://raw.githubusercontent.com/denoland/deno/main/cli/tests/${test.remotePath} +\n`; + const { code } = await print(ast, { + isModule: true, + }); + return header + code; +} + +function visitImport(item: ImportDeclaration): void { + const src = item.source.value; + let match = false; + for (const name of imports) { + if (src.endsWith(name)) { + match = true; + break; + } + } + if (!match) { + console.warn("Unknown import:", src); + return; + } + item.source.raw = '"deno:harness"'; + // FIXME: https://github.com/oven-sh/bun/issues/2350 + /*let missing = []; + for (const specifier of item.specifiers) { + const name = specifier.local.value; + if (!(name in harness)) { + missing.push(name); + } + } + if (missing.length) { + console.warn("Harness does not contain exports:", missing); + }*/ +} + +function visitExpression(item: ExpressionStatement, test: any): void { + if ( + item.expression.type === "CallExpression" && + item.expression.callee.type === "MemberExpression" && + item.expression.callee.object.type === "Identifier" && + item.expression.callee.object.value === "Deno" + ) { + if (item.expression.callee.property.type === "Identifier" && item.expression.callee.property.value === "test") { + visitTest(item.expression); + } + } +} + +function visitTest(item: CallExpression): void { + for (const argument of item.arguments) { + if (argument.expression.type === "FunctionExpression") { + const fn = argument.expression.identifier?.value; + for (const test of tests) { + if (test.skip && test.skip.includes(fn)) { + item.callee.property.value = "test.ignore"; + } + } + } + } } diff --git a/test/package.json b/test/package.json index dfeabcd42a..bffbfee576 100644 --- a/test/package.json +++ b/test/package.json @@ -6,6 +6,7 @@ "bun-types": "canary" }, "dependencies": { + "@swc/core": "^1.3.38", "bktree-fast": "^0.0.7", "body-parser": "^1.20.2", "esbuild": "^0.17.11",