diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2021-02-15 14:48:47 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-15 14:48:47 +0100 |
commit | 8c6d147e6ae40a1f92aba1aedc0d95ef437dd4ba (patch) | |
tree | fbb59c9a6b294fd242854480067ae50cfb61fba7 /cli/tests/workers | |
parent | 1afe6b48e0bfee44f37206eb0fc03ea6ae37ad4d (diff) |
chore: Reorganise workers tests (#9493)
Diffstat (limited to 'cli/tests/workers')
-rw-r--r-- | cli/tests/workers/bench_round_robin.ts | 65 | ||||
-rw-r--r-- | cli/tests/workers/bench_startup.ts | 33 | ||||
-rw-r--r-- | cli/tests/workers/error.ts | 5 | ||||
-rw-r--r-- | cli/tests/workers/post_undefined.ts | 5 | ||||
-rw-r--r-- | cli/tests/workers/test.ts | 677 | ||||
-rw-r--r-- | cli/tests/workers/worker_error.ts | 5 | ||||
-rw-r--r-- | cli/tests/workers/worker_error.ts.out | 7 | ||||
-rw-r--r-- | cli/tests/workers/worker_location.ts | 6 | ||||
-rw-r--r-- | cli/tests/workers/worker_nested_error.ts | 5 | ||||
-rw-r--r-- | cli/tests/workers/worker_nested_error.ts.out | 7 | ||||
-rw-r--r-- | cli/tests/workers/worker_types.ts | 4 |
11 files changed, 819 insertions, 0 deletions
diff --git a/cli/tests/workers/bench_round_robin.ts b/cli/tests/workers/bench_round_robin.ts new file mode 100644 index 000000000..7974760af --- /dev/null +++ b/cli/tests/workers/bench_round_robin.ts @@ -0,0 +1,65 @@ +// Benchmark measures time it takes to send a message to a group of workers one +// at a time and wait for a response from all of them. Just a general +// throughput and consistency benchmark. +const data = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n"; +const workerCount = 4; +const cmdsPerWorker = 400; + +import { Deferred, deferred } from "../../../test_util/std/async/deferred.ts"; + +function handleAsyncMsgFromWorker( + promiseTable: Map<number, Deferred<string>>, + msg: { cmdId: number; data: string }, +): void { + const promise = promiseTable.get(msg.cmdId); + if (promise === null) { + throw new Error(`Failed to find promise: cmdId: ${msg.cmdId}, msg: ${msg}`); + } + promise?.resolve(data); +} + +async function main(): Promise<void> { + const workers: Array<[Map<number, Deferred<string>>, Worker]> = []; + for (let i = 1; i <= workerCount; ++i) { + const worker = new Worker( + new URL("bench_worker.ts", import.meta.url).href, + { type: "module" }, + ); + const promise = deferred(); + worker.onmessage = (e): void => { + if (e.data.cmdId === 0) promise.resolve(); + }; + worker.postMessage({ cmdId: 0, action: 2 }); + await promise; + workers.push([new Map(), worker]); + } + // assign callback function + for (const [promiseTable, worker] of workers) { + worker.onmessage = (e): void => { + handleAsyncMsgFromWorker(promiseTable, e.data); + }; + } + for (const cmdId of Array(cmdsPerWorker).keys()) { + const promises: Array<Promise<string>> = []; + for (const [promiseTable, worker] of workers) { + const promise = deferred<string>(); + promiseTable.set(cmdId, promise); + worker.postMessage({ cmdId: cmdId, action: 1, data }); + promises.push(promise); + } + for (const promise of promises) { + await promise; + } + } + for (const [, worker] of workers) { + const promise = deferred(); + worker.onmessage = (e): void => { + if (e.data.cmdId === 3) promise.resolve(); + }; + worker.postMessage({ action: 3 }); + await promise; + } + console.log("Finished!"); +} + +main(); diff --git a/cli/tests/workers/bench_startup.ts b/cli/tests/workers/bench_startup.ts new file mode 100644 index 000000000..5fbf23b45 --- /dev/null +++ b/cli/tests/workers/bench_startup.ts @@ -0,0 +1,33 @@ +// Benchmark measures time it takes to start and stop a number of workers. +const workerCount = 50; + +async function bench(): Promise<void> { + const workers: Worker[] = []; + for (let i = 1; i <= workerCount; ++i) { + const worker = new Worker( + new URL("bench_worker.ts", import.meta.url).href, + { type: "module" }, + ); + const promise = new Promise<void>((resolve): void => { + worker.onmessage = (e): void => { + if (e.data.cmdId === 0) resolve(); + }; + }); + worker.postMessage({ cmdId: 0, action: 2 }); + await promise; + workers.push(worker); + } + console.log("Done creating workers closing workers!"); + for (const worker of workers) { + const promise = new Promise<void>((resolve): void => { + worker.onmessage = (e): void => { + if (e.data.cmdId === 3) resolve(); + }; + }); + worker.postMessage({ action: 3 }); + await promise; + } + console.log("Finished!"); +} + +bench(); diff --git a/cli/tests/workers/error.ts b/cli/tests/workers/error.ts new file mode 100644 index 000000000..495971090 --- /dev/null +++ b/cli/tests/workers/error.ts @@ -0,0 +1,5 @@ +function foo() { + throw new Error("foo"); +} + +foo(); diff --git a/cli/tests/workers/post_undefined.ts b/cli/tests/workers/post_undefined.ts new file mode 100644 index 000000000..1b9b8d6ca --- /dev/null +++ b/cli/tests/workers/post_undefined.ts @@ -0,0 +1,5 @@ +self.onmessage = (ev: MessageEvent) => { + console.log("received in worker", ev.data); + self.postMessage(undefined); + console.log("posted from worker"); +}; diff --git a/cli/tests/workers/test.ts b/cli/tests/workers/test.ts new file mode 100644 index 000000000..0888e01db --- /dev/null +++ b/cli/tests/workers/test.ts @@ -0,0 +1,677 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// Requires to be run with `--allow-net` flag + +import { + assert, + assertEquals, + assertThrows, +} from "../../../test_util/std/testing/asserts.ts"; +import { deferred } from "../../../test_util/std/async/deferred.ts"; + +Deno.test({ + name: "worker terminate", + fn: async function (): Promise<void> { + const promise = deferred(); + + const jsWorker = new Worker( + new URL("test_worker.js", import.meta.url).href, + { type: "module" }, + ); + const tsWorker = new Worker( + new URL("test_worker.ts", import.meta.url).href, + { type: "module", name: "tsWorker" }, + ); + + tsWorker.onmessage = (e): void => { + assertEquals(e.data, "Hello World"); + promise.resolve(); + }; + + jsWorker.onmessage = (e): void => { + assertEquals(e.data, "Hello World"); + tsWorker.postMessage("Hello World"); + }; + + jsWorker.onerror = (e: Event): void => { + e.preventDefault(); + jsWorker.postMessage("Hello World"); + }; + + jsWorker.postMessage("Hello World"); + await promise; + tsWorker.terminate(); + jsWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker from data url", + async fn() { + const promise = deferred(); + const tsWorker = new Worker( + "data:application/typescript;base64,aWYgKHNlbGYubmFtZSAhPT0gInRzV29ya2VyIikgewogIHRocm93IEVycm9yKGBJbnZhbGlkIHdvcmtlciBuYW1lOiAke3NlbGYubmFtZX0sIGV4cGVjdGVkIHRzV29ya2VyYCk7Cn0KCm9ubWVzc2FnZSA9IGZ1bmN0aW9uIChlKTogdm9pZCB7CiAgcG9zdE1lc3NhZ2UoZS5kYXRhKTsKICBjbG9zZSgpOwp9Owo=", + { type: "module", name: "tsWorker" }, + ); + + tsWorker.onmessage = (e): void => { + assertEquals(e.data, "Hello World"); + promise.resolve(); + }; + + tsWorker.postMessage("Hello World"); + + await promise; + tsWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker nested", + fn: async function (): Promise<void> { + const promise = deferred(); + + const nestedWorker = new Worker( + new URL("nested_worker.js", import.meta.url).href, + { type: "module", name: "nested" }, + ); + + nestedWorker.onmessage = (e): void => { + assert(e.data.type !== "error"); + promise.resolve(); + }; + + nestedWorker.postMessage("Hello World"); + await promise; + nestedWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker throws when executing", + fn: async function (): Promise<void> { + const promise = deferred(); + const throwingWorker = new Worker( + new URL("throwing_worker.js", import.meta.url).href, + { type: "module" }, + ); + + // deno-lint-ignore no-explicit-any + throwingWorker.onerror = (e: any): void => { + e.preventDefault(); + assert(/Uncaught Error: Thrown error/.test(e.message)); + promise.resolve(); + }; + + await promise; + throwingWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker globals", + fn: async function (): Promise<void> { + const promise = deferred(); + const workerOptions: WorkerOptions = { type: "module" }; + const w = new Worker( + new URL("worker_globals.ts", import.meta.url).href, + workerOptions, + ); + w.onmessage = (e): void => { + assertEquals(e.data, "true, true, true"); + promise.resolve(); + }; + w.postMessage("Hello, world!"); + await promise; + w.terminate(); + }, +}); + +Deno.test({ + name: "worker fetch API", + fn: async function (): Promise<void> { + const promise = deferred(); + + const fetchingWorker = new Worker( + new URL("fetching_worker.js", import.meta.url).href, + { type: "module" }, + ); + + // deno-lint-ignore no-explicit-any + fetchingWorker.onerror = (e: any): void => { + e.preventDefault(); + promise.reject(e.message); + }; + + // Defer promise.resolve() to allow worker to shut down + fetchingWorker.onmessage = (e): void => { + assert(e.data === "Done!"); + promise.resolve(); + }; + + await promise; + fetchingWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker terminate busy loop", + fn: async function (): Promise<void> { + const promise = deferred(); + + const busyWorker = new Worker( + new URL("busy_worker.js", import.meta.url).href, + { type: "module" }, + ); + + let testResult = 0; + + busyWorker.onmessage = (e): void => { + testResult = e.data; + if (testResult >= 10000) { + busyWorker.terminate(); + busyWorker.onmessage = (_e): void => { + throw new Error("unreachable"); + }; + setTimeout(() => { + assertEquals(testResult, 10000); + promise.resolve(); + }, 100); + } + }; + + busyWorker.postMessage("ping"); + await promise; + }, +}); + +Deno.test({ + name: "worker race condition", + fn: async function (): Promise<void> { + // See issue for details + // https://github.com/denoland/deno/issues/4080 + const promise = deferred(); + + const racyWorker = new Worker( + new URL("racy_worker.js", import.meta.url).href, + { type: "module" }, + ); + + racyWorker.onmessage = (e): void => { + assertEquals(e.data.buf.length, 999999); + racyWorker.onmessage = (_e): void => { + throw new Error("unreachable"); + }; + setTimeout(() => { + promise.resolve(); + }, 100); + }; + + await promise; + }, +}); + +Deno.test({ + name: "worker is event listener", + fn: async function (): Promise<void> { + let messageHandlersCalled = 0; + let errorHandlersCalled = 0; + + const promise1 = deferred(); + const promise2 = deferred(); + + const worker = new Worker( + new URL("event_worker.js", import.meta.url).href, + { type: "module" }, + ); + + worker.onmessage = (_e: Event): void => { + messageHandlersCalled++; + }; + worker.addEventListener("message", (_e: Event) => { + messageHandlersCalled++; + }); + worker.addEventListener("message", (_e: Event) => { + messageHandlersCalled++; + promise1.resolve(); + }); + + worker.onerror = (e): void => { + errorHandlersCalled++; + e.preventDefault(); + }; + worker.addEventListener("error", (_e: Event) => { + errorHandlersCalled++; + }); + worker.addEventListener("error", (_e: Event) => { + errorHandlersCalled++; + promise2.resolve(); + }); + + worker.postMessage("ping"); + await promise1; + assertEquals(messageHandlersCalled, 3); + + worker.postMessage("boom"); + await promise2; + assertEquals(errorHandlersCalled, 3); + worker.terminate(); + }, +}); + +Deno.test({ + name: "worker scope is event listener", + fn: async function (): Promise<void> { + const promise1 = deferred(); + + const worker = new Worker( + new URL("event_worker_scope.js", import.meta.url).href, + { type: "module" }, + ); + + worker.onmessage = (e: MessageEvent): void => { + const { messageHandlersCalled, errorHandlersCalled } = e.data; + assertEquals(messageHandlersCalled, 4); + assertEquals(errorHandlersCalled, 4); + promise1.resolve(); + }; + + worker.onerror = (_e): void => { + throw new Error("unreachable"); + }; + + worker.postMessage("boom"); + worker.postMessage("ping"); + await promise1; + worker.terminate(); + }, +}); + +Deno.test({ + name: "worker with Deno namespace", + fn: async function (): Promise<void> { + const promise = deferred(); + const promise2 = deferred(); + + const regularWorker = new Worker( + new URL("non_deno_worker.js", import.meta.url).href, + { type: "module" }, + ); + const denoWorker = new Worker( + new URL("deno_worker.ts", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: "inherit", + }, + }, + ); + + regularWorker.onmessage = (e): void => { + assertEquals(e.data, "Hello World"); + regularWorker.terminate(); + promise.resolve(); + }; + + denoWorker.onmessage = (e): void => { + assertEquals(e.data, "Hello World"); + denoWorker.terminate(); + promise2.resolve(); + }; + + regularWorker.postMessage("Hello World"); + await promise; + denoWorker.postMessage("Hello World"); + await promise2; + }, +}); + +Deno.test({ + name: "worker with crypto in scope", + fn: async function (): Promise<void> { + const promise = deferred(); + const w = new Worker( + new URL("worker_crypto.js", import.meta.url).href, + { type: "module" }, + ); + w.onmessage = (e): void => { + assertEquals(e.data, true); + promise.resolve(); + }; + w.postMessage(null); + await promise; + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker event handler order", + fn: async function (): Promise<void> { + const promise = deferred(); + const w = new Worker( + new URL("test_worker.ts", import.meta.url).href, + { type: "module", name: "tsWorker" }, + ); + const arr: number[] = []; + w.addEventListener("message", () => arr.push(1)); + w.onmessage = (e): void => { + arr.push(2); + }; + w.addEventListener("message", () => arr.push(3)); + w.addEventListener("message", () => { + assertEquals(arr, [1, 2, 3]); + promise.resolve(); + }); + w.postMessage("Hello World"); + await promise; + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker immediate close", + fn: async function (): Promise<void> { + const promise = deferred(); + const w = new Worker( + new URL("./immediately_close_worker.js", import.meta.url).href, + { type: "module" }, + ); + setTimeout(() => { + promise.resolve(); + }, 1000); + await promise; + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker post undefined", + fn: async function (): Promise<void> { + const promise = deferred(); + const worker = new Worker( + new URL("./post_undefined.ts", import.meta.url).href, + { type: "module" }, + ); + + const handleWorkerMessage = (e: MessageEvent): void => { + console.log("main <- worker:", e.data); + worker.terminate(); + promise.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 promise = deferred(); + const worker = new Worker( + new URL("./read_check_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: "inherit", + }, + }, + ); + + worker.onmessage = ({ data: hasPermission }) => { + assert(hasPermission); + promise.resolve(); + }; + + worker.postMessage(null); + + await promise; + worker.terminate(); +}); + +Deno.test("Worker limit children permissions", async function () { + const promise = deferred(); + const worker = new Worker( + new URL("./read_check_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: { + read: false, + }, + }, + }, + ); + + worker.onmessage = ({ data: hasPermission }) => { + assert(!hasPermission); + promise.resolve(); + }; + + worker.postMessage(null); + + await promise; + worker.terminate(); +}); + +Deno.test("Worker limit children permissions granularly", async function () { + const promise = deferred(); + const worker = new Worker( + new URL("./read_check_granular_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: { + read: [ + new URL("./read_check_worker.js", import.meta.url), + ], + }, + }, + }, + ); + + //Routes are relative to the spawned worker location + const routes = [ + { permission: false, route: "read_check_granular_worker.js" }, + { permission: true, route: "read_check_worker.js" }, + ]; + + let checked = 0; + worker.onmessage = ({ data }) => { + checked++; + assertEquals(data.hasPermission, routes[data.index].permission); + routes.shift(); + if (checked === routes.length) { + promise.resolve(); + } + }; + + routes.forEach(({ route }, index) => + worker.postMessage({ + index, + route, + }) + ); + + await promise; + worker.terminate(); +}); + +Deno.test("Nested worker limit children permissions", async function () { + const promise = deferred(); + + /** This worker has read permissions but doesn't grant them to its children */ + const worker = new Worker( + new URL("./parent_read_check_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: "inherit", + }, + }, + ); + + worker.onmessage = ({ data }) => { + assert(data.parentHasPermission); + assert(!data.childHasPermission); + promise.resolve(); + }; + + worker.postMessage(null); + + await promise; + worker.terminate(); +}); + +Deno.test("Nested worker limit children permissions granularly", async function () { + const promise = deferred(); + + /** This worker has read permissions but doesn't grant them to its children */ + const worker = new Worker( + new URL("./parent_read_check_granular_worker.js", import.meta.url) + .href, + { + type: "module", + deno: { + namespace: true, + permissions: { + read: [ + new URL("./read_check_granular_worker.js", import.meta.url), + ], + }, + }, + }, + ); + + //Routes are relative to the spawned worker location + const routes = [ + { + childHasPermission: false, + parentHasPermission: true, + route: "read_check_granular_worker.js", + }, + { + childHasPermission: false, + parentHasPermission: false, + route: "read_check_worker.js", + }, + ]; + + let checked = 0; + worker.onmessage = ({ data }) => { + checked++; + assertEquals( + data.childHasPermission, + routes[data.index].childHasPermission, + ); + assertEquals( + data.parentHasPermission, + routes[data.index].parentHasPermission, + ); + if (checked === routes.length) { + promise.resolve(); + } + }; + + // Index needed cause requests will be handled asynchronously + routes.forEach(({ route }, index) => + worker.postMessage({ + index, + route, + }) + ); + + await promise; + worker.terminate(); +}); + +// This test relies on env permissions not being granted on main thread +Deno.test("Worker initialization throws on worker permissions greater than parent thread permissions", function () { + assertThrows( + () => { + const worker = new Worker( + new URL("./deno_worker.ts", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: { + env: true, + }, + }, + }, + ); + worker.terminate(); + }, + Deno.errors.PermissionDenied, + "Can't escalate parent thread permissions", + ); +}); + +Deno.test("Worker with disabled permissions", async function () { + const promise = deferred(); + + const worker = new Worker( + new URL("./no_permissions_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: "none", + }, + }, + ); + + worker.onmessage = ({ data: sandboxed }) => { + assert(sandboxed); + promise.resolve(); + }; + + worker.postMessage(null); + await promise; + worker.terminate(); +}); + +Deno.test({ + name: "worker location", + fn: async function (): Promise<void> { + const promise = deferred(); + const workerModuleHref = + new URL("worker_location.ts", import.meta.url).href; + const w = new Worker(workerModuleHref, { type: "module" }); + w.onmessage = (e): void => { + assertEquals(e.data, `${workerModuleHref}, true`); + promise.resolve(); + }; + w.postMessage("Hello, world!"); + await promise; + w.terminate(); + }, +}); + +Deno.test({ + name: "worker with relative specifier", + fn: async function (): Promise<void> { + assertEquals(location.href, "http://127.0.0.1:4545/cli/tests/"); + const promise = deferred(); + const w = new Worker( + "./workers/test_worker.ts", + { type: "module", name: "tsWorker" }, + ); + w.onmessage = (e): void => { + assertEquals(e.data, "Hello, world!"); + promise.resolve(); + }; + w.postMessage("Hello, world!"); + await promise; + w.terminate(); + }, +}); diff --git a/cli/tests/workers/worker_error.ts b/cli/tests/workers/worker_error.ts new file mode 100644 index 000000000..696680de8 --- /dev/null +++ b/cli/tests/workers/worker_error.ts @@ -0,0 +1,5 @@ +const worker = new Worker( + new URL("error.ts", import.meta.url).href, + { type: "module", name: "bar" }, +); +setTimeout(() => worker.terminate(), 30000); diff --git a/cli/tests/workers/worker_error.ts.out b/cli/tests/workers/worker_error.ts.out new file mode 100644 index 000000000..e46478979 --- /dev/null +++ b/cli/tests/workers/worker_error.ts.out @@ -0,0 +1,7 @@ +[WILDCARD]error: Uncaught (in worker "bar") Error: foo[WILDCARD] + at foo ([WILDCARD]) + at [WILDCARD] +error: Uncaught (in promise) Error: Unhandled error event reached main worker. + throw new Error("Unhandled error event reached main worker."); + ^ + at Worker.#poll ([WILDCARD]) diff --git a/cli/tests/workers/worker_location.ts b/cli/tests/workers/worker_location.ts new file mode 100644 index 000000000..89fa83036 --- /dev/null +++ b/cli/tests/workers/worker_location.ts @@ -0,0 +1,6 @@ +onmessage = function (): void { + postMessage( + `${location.href}, ${location instanceof WorkerLocation}`, + ); + close(); +}; diff --git a/cli/tests/workers/worker_nested_error.ts b/cli/tests/workers/worker_nested_error.ts new file mode 100644 index 000000000..aba2011be --- /dev/null +++ b/cli/tests/workers/worker_nested_error.ts @@ -0,0 +1,5 @@ +const worker = new Worker( + new URL("worker_error.ts", import.meta.url).href, + { type: "module", name: "baz" }, +); +setTimeout(() => worker.terminate(), 30000); diff --git a/cli/tests/workers/worker_nested_error.ts.out b/cli/tests/workers/worker_nested_error.ts.out new file mode 100644 index 000000000..e46478979 --- /dev/null +++ b/cli/tests/workers/worker_nested_error.ts.out @@ -0,0 +1,7 @@ +[WILDCARD]error: Uncaught (in worker "bar") Error: foo[WILDCARD] + at foo ([WILDCARD]) + at [WILDCARD] +error: Uncaught (in promise) Error: Unhandled error event reached main worker. + throw new Error("Unhandled error event reached main worker."); + ^ + at Worker.#poll ([WILDCARD]) diff --git a/cli/tests/workers/worker_types.ts b/cli/tests/workers/worker_types.ts new file mode 100644 index 000000000..b67a3b782 --- /dev/null +++ b/cli/tests/workers/worker_types.ts @@ -0,0 +1,4 @@ +// deno-lint-ignore require-await +self.onmessage = async (_msg: MessageEvent) => { + self.postMessage("hello"); +}; |