diff options
Diffstat (limited to 'tests/unit/worker_test.ts')
-rw-r--r-- | tests/unit/worker_test.ts | 843 |
1 files changed, 843 insertions, 0 deletions
diff --git a/tests/unit/worker_test.ts b/tests/unit/worker_test.ts new file mode 100644 index 000000000..eea0e8106 --- /dev/null +++ b/tests/unit/worker_test.ts @@ -0,0 +1,843 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +// Requires to be run with `--allow-net` flag + +import { + assert, + assertEquals, + assertMatch, + assertThrows, +} from "@test_util/std/assert/mod.ts"; + +function resolveWorker(worker: string): string { + return import.meta.resolve(`../testdata/workers/${worker}`); +} + +Deno.test( + { permissions: { read: true } }, + function utimeSyncFileSuccess() { + const w = new Worker( + resolveWorker("worker_types.ts"), + { type: "module" }, + ); + assert(w); + w.terminate(); + }, +); + +Deno.test({ + name: "worker terminate", + fn: async function () { + const jsWorker = new Worker( + resolveWorker("test_worker.js"), + { type: "module" }, + ); + const tsWorker = new Worker( + resolveWorker("test_worker.ts"), + { type: "module", name: "tsWorker" }, + ); + + const deferred1 = Promise.withResolvers<string>(); + jsWorker.onmessage = (e) => { + deferred1.resolve(e.data); + }; + + const deferred2 = Promise.withResolvers<string>(); + tsWorker.onmessage = (e) => { + deferred2.resolve(e.data); + }; + + jsWorker.postMessage("Hello World"); + assertEquals(await deferred1.promise, "Hello World"); + tsWorker.postMessage("Hello World"); + assertEquals(await deferred2.promise, "Hello World"); + tsWorker.terminate(); + jsWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker from data url", + async fn() { + const tsWorker = new Worker( + "data:application/typescript;base64,aWYgKHNlbGYubmFtZSAhPT0gInRzV29ya2VyIikgewogIHRocm93IEVycm9yKGBJbnZhbGlkIHdvcmtlciBuYW1lOiAke3NlbGYubmFtZX0sIGV4cGVjdGVkIHRzV29ya2VyYCk7Cn0KCm9ubWVzc2FnZSA9IGZ1bmN0aW9uIChlKTogdm9pZCB7CiAgcG9zdE1lc3NhZ2UoZS5kYXRhKTsKICBjbG9zZSgpOwp9Owo=", + { type: "module", name: "tsWorker" }, + ); + + const { promise, resolve } = Promise.withResolvers<string>(); + tsWorker.onmessage = (e) => { + resolve(e.data); + }; + + tsWorker.postMessage("Hello World"); + assertEquals(await promise, "Hello World"); + tsWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker nested", + fn: async function () { + const nestedWorker = new Worker( + resolveWorker("nested_worker.js"), + { type: "module", name: "nested" }, + ); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers<any>(); + nestedWorker.onmessage = (e) => { + resolve(e.data); + }; + + nestedWorker.postMessage("Hello World"); + assertEquals(await promise, { type: "msg", text: "Hello World" }); + nestedWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker throws when executing", + fn: async function () { + const throwingWorker = new Worker( + resolveWorker("throwing_worker.js"), + { type: "module" }, + ); + + const { promise, resolve } = Promise.withResolvers<string>(); + // deno-lint-ignore no-explicit-any + throwingWorker.onerror = (e: any) => { + e.preventDefault(); + resolve(e.message); + }; + + assertMatch( + await promise as string, + /Uncaught \(in promise\) Error: Thrown error/, + ); + throwingWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker globals", + fn: async function () { + const workerOptions: WorkerOptions = { type: "module" }; + const w = new Worker( + resolveWorker("worker_globals.ts"), + workerOptions, + ); + + const { promise, resolve } = Promise.withResolvers<string>(); + w.onmessage = (e) => { + resolve(e.data); + }; + + w.postMessage("Hello, world!"); + assertEquals(await promise, "true, true, true, true"); + w.terminate(); + }, +}); + +Deno.test({ + name: "worker navigator", + fn: async function () { + const workerOptions: WorkerOptions = { type: "module" }; + const w = new Worker( + resolveWorker("worker_navigator.ts"), + workerOptions, + ); + + const { promise, resolve } = Promise.withResolvers<string>(); + w.onmessage = (e) => { + resolve(e.data); + }; + + w.postMessage("Hello, world!"); + assertEquals(await promise, "string, object, string, number"); + w.terminate(); + }, +}); + +Deno.test({ + name: "worker fetch API", + fn: async function () { + const fetchingWorker = new Worker( + resolveWorker("fetching_worker.js"), + { type: "module" }, + ); + + const { promise, resolve, reject } = Promise.withResolvers<string>(); + // deno-lint-ignore no-explicit-any + fetchingWorker.onerror = (e: any) => { + e.preventDefault(); + reject(e.message); + }; + // Defer promise.resolve() to allow worker to shut down + fetchingWorker.onmessage = (e) => { + resolve(e.data); + }; + + assertEquals(await promise, "Done!"); + fetchingWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker terminate busy loop", + fn: async function () { + const { promise, resolve } = Promise.withResolvers<number>(); + + const busyWorker = new Worker( + resolveWorker("busy_worker.js"), + { type: "module" }, + ); + + let testResult = 0; + + busyWorker.onmessage = (e) => { + testResult = e.data; + if (testResult >= 10000) { + busyWorker.terminate(); + busyWorker.onmessage = (_e) => { + throw new Error("unreachable"); + }; + setTimeout(() => { + resolve(testResult); + }, 100); + } + }; + + busyWorker.postMessage("ping"); + assertEquals(await promise, 10000); + }, +}); + +Deno.test({ + name: "worker race condition", + fn: async function () { + // See issue for details + // https://github.com/denoland/deno/issues/4080 + const { promise, resolve } = Promise.withResolvers<void>(); + + const racyWorker = new Worker( + resolveWorker("racy_worker.js"), + { type: "module" }, + ); + + racyWorker.onmessage = (_e) => { + setTimeout(() => { + resolve(); + }, 100); + }; + + racyWorker.postMessage("START"); + await promise; + }, +}); + +Deno.test({ + name: "worker is event listener", + fn: async function () { + let messageHandlersCalled = 0; + let errorHandlersCalled = 0; + + const deferred1 = Promise.withResolvers<void>(); + const deferred2 = Promise.withResolvers<void>(); + + const worker = new Worker( + resolveWorker("event_worker.js"), + { type: "module" }, + ); + + worker.onmessage = (_e: Event) => { + messageHandlersCalled++; + }; + worker.addEventListener("message", (_e: Event) => { + messageHandlersCalled++; + }); + worker.addEventListener("message", (_e: Event) => { + messageHandlersCalled++; + deferred1.resolve(); + }); + + worker.onerror = (e) => { + errorHandlersCalled++; + e.preventDefault(); + }; + worker.addEventListener("error", (_e: Event) => { + errorHandlersCalled++; + }); + worker.addEventListener("error", (_e: Event) => { + errorHandlersCalled++; + deferred2.resolve(); + }); + + worker.postMessage("ping"); + await deferred1.promise; + assertEquals(messageHandlersCalled, 3); + + worker.postMessage("boom"); + await deferred2.promise; + assertEquals(errorHandlersCalled, 3); + worker.terminate(); + }, +}); + +Deno.test({ + name: "worker scope is event listener", + fn: async function () { + const worker = new Worker( + resolveWorker("event_worker_scope.js"), + { type: "module" }, + ); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers<any>(); + worker.onmessage = (e: MessageEvent) => { + resolve(e.data); + }; + worker.onerror = (_e) => { + throw new Error("unreachable"); + }; + + worker.postMessage("boom"); + worker.postMessage("ping"); + assertEquals(await promise, { + messageHandlersCalled: 4, + errorHandlersCalled: 4, + }); + worker.terminate(); + }, +}); + +Deno.test({ + name: "worker with Deno namespace", + fn: async function () { + const denoWorker = new Worker( + resolveWorker("deno_worker.ts"), + { type: "module", deno: { permissions: "inherit" } }, + ); + + const { promise, resolve } = Promise.withResolvers<string>(); + denoWorker.onmessage = (e) => { + denoWorker.terminate(); + resolve(e.data); + }; + + denoWorker.postMessage("Hello World"); + assertEquals(await promise, "Hello World"); + }, +}); + +Deno.test({ + name: "worker with crypto in scope", + fn: async function () { + const w = new Worker( + resolveWorker("worker_crypto.js"), + { type: "module" }, + ); + + const { promise, resolve } = Promise.withResolvers<boolean>(); + w.onmessage = (e) => { + resolve(e.data); + }; + + w.postMessage(null); + assertEquals(await promise, true); + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker event handler order", + fn: async function () { + const { promise, resolve } = Promise.withResolvers<void>(); + const w = new Worker( + resolveWorker("test_worker.ts"), + { type: "module", name: "tsWorker" }, + ); + const arr: number[] = []; + w.addEventListener("message", () => arr.push(1)); + w.onmessage = (_e) => { + arr.push(2); + }; + w.addEventListener("message", () => arr.push(3)); + w.addEventListener("message", () => { + resolve(); + }); + w.postMessage("Hello World"); + await promise; + assertEquals(arr, [1, 2, 3]); + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker immediate close", + fn: async function () { + const { promise, resolve } = Promise.withResolvers<void>(); + const w = new Worker( + resolveWorker("immediately_close_worker.js"), + { type: "module" }, + ); + setTimeout(() => { + resolve(); + }, 1000); + await promise; + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker post undefined", + fn: async function () { + const { promise, resolve } = Promise.withResolvers<void>(); + const worker = new Worker( + resolveWorker("post_undefined.ts"), + { type: "module" }, + ); + + const handleWorkerMessage = (e: MessageEvent) => { + console.log("main <- worker:", e.data); + worker.terminate(); + resolve(); + }; + + worker.addEventListener("messageerror", () => console.log("message error")); + worker.addEventListener("error", () => console.log("error")); + worker.addEventListener("message", handleWorkerMessage); + + console.log("\npost from parent"); + worker.postMessage(undefined); + await promise; + }, +}); + +Deno.test("Worker inherits permissions", async function () { + const worker = new Worker( + resolveWorker("read_check_worker.js"), + { type: "module", deno: { permissions: "inherit" } }, + ); + + const { promise, resolve } = Promise.withResolvers<boolean>(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage(null); + assertEquals(await promise, true); + worker.terminate(); +}); + +Deno.test("Worker limit children permissions", async function () { + const worker = new Worker( + resolveWorker("read_check_worker.js"), + { type: "module", deno: { permissions: { read: false } } }, + ); + + const { promise, resolve } = Promise.withResolvers<boolean>(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage(null); + assertEquals(await promise, false); + worker.terminate(); +}); + +Deno.test("Worker limit children permissions granularly", async function () { + const workerUrl = resolveWorker("read_check_granular_worker.js"); + const worker = new Worker( + workerUrl, + { + type: "module", + deno: { + permissions: { + env: ["foo"], + hrtime: true, + net: ["foo", "bar:8000"], + ffi: [new URL("foo", workerUrl), "bar"], + read: [new URL("foo", workerUrl), "bar"], + run: [new URL("foo", workerUrl), "bar", "./baz"], + write: [new URL("foo", workerUrl), "bar"], + }, + }, + }, + ); + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers<any>(); + worker.onmessage = ({ data }) => resolve(data); + assertEquals(await promise, { + envGlobal: "prompt", + envFoo: "granted", + envAbsent: "prompt", + hrtime: "granted", + netGlobal: "prompt", + netFoo: "granted", + netFoo8000: "granted", + netBar: "prompt", + netBar8000: "granted", + ffiGlobal: "prompt", + ffiFoo: "granted", + ffiBar: "granted", + ffiAbsent: "prompt", + readGlobal: "prompt", + readFoo: "granted", + readBar: "granted", + readAbsent: "prompt", + runGlobal: "prompt", + runFoo: "granted", + runBar: "granted", + runBaz: "granted", + runAbsent: "prompt", + writeGlobal: "prompt", + writeFoo: "granted", + writeBar: "granted", + writeAbsent: "prompt", + }); + worker.terminate(); +}); + +Deno.test("Nested worker limit children permissions", async function () { + /** This worker has permissions but doesn't grant them to its children */ + const worker = new Worker( + resolveWorker("parent_read_check_worker.js"), + { type: "module", deno: { permissions: "inherit" } }, + ); + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers<any>(); + worker.onmessage = ({ data }) => resolve(data); + assertEquals(await promise, { + envGlobal: "prompt", + envFoo: "prompt", + envAbsent: "prompt", + hrtime: "prompt", + netGlobal: "prompt", + netFoo: "prompt", + netFoo8000: "prompt", + netBar: "prompt", + netBar8000: "prompt", + ffiGlobal: "prompt", + ffiFoo: "prompt", + ffiBar: "prompt", + ffiAbsent: "prompt", + readGlobal: "prompt", + readFoo: "prompt", + readBar: "prompt", + readAbsent: "prompt", + runGlobal: "prompt", + runFoo: "prompt", + runBar: "prompt", + runBaz: "prompt", + runAbsent: "prompt", + writeGlobal: "prompt", + writeFoo: "prompt", + writeBar: "prompt", + writeAbsent: "prompt", + }); + worker.terminate(); +}); + +// This test relies on env permissions not being granted on main thread +Deno.test({ + name: + "Worker initialization throws on worker permissions greater than parent thread permissions", + permissions: { env: false }, + fn: function () { + assertThrows( + () => { + const worker = new Worker( + resolveWorker("deno_worker.ts"), + { type: "module", deno: { permissions: { env: true } } }, + ); + worker.terminate(); + }, + Deno.errors.PermissionDenied, + "Can't escalate parent thread permissions", + ); + }, +}); + +Deno.test("Worker with disabled permissions", async function () { + const worker = new Worker( + resolveWorker("no_permissions_worker.js"), + { type: "module", deno: { permissions: "none" } }, + ); + + const { promise, resolve } = Promise.withResolvers<boolean>(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage(null); + assertEquals(await promise, true); + worker.terminate(); +}); + +Deno.test("Worker permissions are not inherited with empty permission object", async function () { + const worker = new Worker( + resolveWorker("permission_echo.js"), + { type: "module", deno: { permissions: {} } }, + ); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers<any>(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage(null); + assertEquals(await promise, { + env: "prompt", + hrtime: "prompt", + net: "prompt", + ffi: "prompt", + read: "prompt", + run: "prompt", + write: "prompt", + }); + worker.terminate(); +}); + +Deno.test("Worker permissions are not inherited with single specified permission", async function () { + const worker = new Worker( + resolveWorker("permission_echo.js"), + { type: "module", deno: { permissions: { net: true } } }, + ); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers<any>(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage(null); + assertEquals(await promise, { + env: "prompt", + hrtime: "prompt", + net: "granted", + ffi: "prompt", + read: "prompt", + run: "prompt", + write: "prompt", + }); + worker.terminate(); +}); + +Deno.test("Worker with invalid permission arg", function () { + assertThrows( + () => + new Worker(`data:,close();`, { + type: "module", + // @ts-expect-error invalid env value + deno: { permissions: { env: "foo" } }, + }), + TypeError, + '(deno.permissions.env) invalid value: string "foo", expected "inherit" or boolean or string[]', + ); +}); + +Deno.test({ + name: "worker location", + fn: async function () { + const { promise, resolve } = Promise.withResolvers<string>(); + const workerModuleHref = resolveWorker("worker_location.ts"); + const w = new Worker(workerModuleHref, { type: "module" }); + w.onmessage = (e) => { + resolve(e.data); + }; + w.postMessage("Hello, world!"); + assertEquals(await promise, `${workerModuleHref}, true`); + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker with top-level-await", + fn: async function () { + const { promise, resolve, reject } = Promise.withResolvers<void>(); + const worker = new Worker( + resolveWorker("worker_with_top_level_await.ts"), + { type: "module" }, + ); + worker.onmessage = (e) => { + if (e.data == "ready") { + worker.postMessage("trigger worker handler"); + } else if (e.data == "triggered worker handler") { + resolve(); + } else { + reject(new Error("Handler didn't run during top-level delay.")); + } + }; + await promise; + worker.terminate(); + }, +}); + +Deno.test({ + name: "Worker with native HTTP", + fn: async function () { + const { promise, resolve } = Promise.withResolvers<void>(); + const worker = new Worker( + resolveWorker("http_worker.js"), + { type: "module", deno: { permissions: "inherit" } }, + ); + worker.onmessage = () => { + resolve(); + }; + await promise; + + assert(worker); + const response = await fetch("http://localhost:4506"); + assert(await response.arrayBuffer()); + worker.terminate(); + }, +}); + +Deno.test({ + name: "structured cloning postMessage", + fn: async function () { + const worker = new Worker( + resolveWorker("worker_structured_cloning.ts"), + { type: "module" }, + ); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers<any>(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage("START"); + const data = await promise; + // self field should reference itself (circular ref) + assert(data === data.self); + // fields a and b refer to the same array + assertEquals(data.a, ["a", true, 432]); + assertEquals(data.b, ["a", true, 432]); + data.b[0] = "b"; + data.a[2] += 5; + assertEquals(data.a, ["b", true, 437]); + assertEquals(data.b, ["b", true, 437]); + // c is a set + const len = data.c.size; + data.c.add(1); // This value is already in the set. + data.c.add(2); + assertEquals(len + 1, data.c.size); + worker.terminate(); + }, +}); + +Deno.test({ + name: "worker with relative specifier", + fn: async function () { + assertEquals(location.href, "http://127.0.0.1:4545/"); + const w = new Worker( + "./workers/test_worker.ts", + { type: "module", name: "tsWorker" }, + ); + const { promise, resolve } = Promise.withResolvers<string>(); + w.onmessage = (e) => { + resolve(e.data); + }; + w.postMessage("Hello, world!"); + assertEquals(await promise, "Hello, world!"); + w.terminate(); + }, +}); + +Deno.test({ + name: "worker SharedArrayBuffer", + fn: async function () { + const { promise, resolve } = Promise.withResolvers<void>(); + const workerOptions: WorkerOptions = { type: "module" }; + const w = new Worker( + resolveWorker("shared_array_buffer.ts"), + workerOptions, + ); + const sab1 = new SharedArrayBuffer(1); + const sab2 = new SharedArrayBuffer(1); + const bytes1 = new Uint8Array(sab1); + const bytes2 = new Uint8Array(sab2); + assertEquals(bytes1[0], 0); + assertEquals(bytes2[0], 0); + w.onmessage = () => { + w.postMessage([sab1, sab2]); + w.onmessage = () => { + resolve(); + }; + }; + await promise; + assertEquals(bytes1[0], 1); + assertEquals(bytes2[0], 2); + w.terminate(); + }, +}); + +Deno.test({ + name: "Send MessagePorts from / to workers", + fn: async function () { + const worker = new Worker( + resolveWorker("message_port.ts"), + { type: "module" }, + ); + const channel = new MessageChannel(); + + // deno-lint-ignore no-explicit-any + const deferred1 = Promise.withResolvers<any>(); + const deferred2 = Promise.withResolvers<boolean>(); + const deferred3 = Promise.withResolvers<boolean>(); + const result = Promise.withResolvers<void>(); + worker.onmessage = (e) => { + deferred1.resolve([e.data, e.ports.length]); + const port1 = e.ports[0]; + port1.onmessage = (e) => { + deferred2.resolve(e.data); + port1.close(); + worker.postMessage("3", [channel.port1]); + }; + port1.postMessage("2"); + }; + channel.port2.onmessage = (e) => { + deferred3.resolve(e.data); + channel.port2.close(); + result.resolve(); + }; + + assertEquals(await deferred1.promise, ["1", 1]); + assertEquals(await deferred2.promise, true); + assertEquals(await deferred3.promise, true); + await result.promise; + worker.terminate(); + }, +}); + +Deno.test({ + name: "worker Deno.memoryUsage", + fn: async function () { + const w = new Worker( + /** + * Source code + * self.onmessage = function() {self.postMessage(Deno.memoryUsage())} + */ + "data:application/typescript;base64,c2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbigpIHtzZWxmLnBvc3RNZXNzYWdlKERlbm8ubWVtb3J5VXNhZ2UoKSl9", + { type: "module", name: "tsWorker" }, + ); + + w.postMessage(null); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers<any>(); + w.onmessage = function (evt) { + resolve(evt.data); + }; + + assertEquals( + Object.keys( + await promise as unknown as Record<string, number>, + ), + ["rss", "heapTotal", "heapUsed", "external"], + ); + w.terminate(); + }, +}); |