import { describe, expect, test } from "bun:test"; import { once } from "events"; import { bunEnv, bunExe } from "harness"; import path from "path"; import wt from "worker_threads"; describe("web worker", () => { async function waitForWorkerResult(worker: Worker, message: any): Promise { const promise = new Promise((resolve, reject) => { worker.onerror = reject; worker.onmessage = e => resolve(e.data); }); worker.postMessage(message); try { return await promise; } finally { worker.terminate(); } } describe("preload", () => { test("invalid file URL", async () => { expect(() => new Worker("file://:!:!:!!!!", {})).toThrow(/Invalid file URL/); expect( () => new Worker(import.meta.url, { preload: ["file://:!:!:!!!!", "file://:!:!:!!!!2"], }), ).toThrow(/Invalid file URL/); }); test("string", async () => { const worker = new Worker(new URL("worker-fixture-preload-entry.js", import.meta.url).href, { preload: new URL("worker-fixture-preload.js", import.meta.url).href, }); const result = await waitForWorkerResult(worker, "hello world"); expect(result).toEqual("hello world"); }); test("array of 2 strings", async () => { const worker = new Worker(new URL("worker-fixture-preload-entry.js", import.meta.url).href, { preload: [ new URL("worker-fixture-preload.js", import.meta.url).href, new URL("worker-fixture-preload-2.js", import.meta.url).href, ], }); const result = await waitForWorkerResult(worker, "hello world world"); expect(result).toEqual("hello world world"); }); test("array of string", async () => { const worker = new Worker(new URL("worker-fixture-preload-entry.js", import.meta.url).href, { preload: [new URL("worker-fixture-preload.js", import.meta.url).href], }); const result = await waitForWorkerResult(worker, "hello world"); expect(result).toEqual("hello world"); }); test("error in preload doesn't crash parent", async () => { const worker = new Worker(new URL("worker-fixture-preload-entry.js", import.meta.url).href, { preload: [new URL("worker-fixture-preload-bad.js", import.meta.url).href], }); const { resolve, promise } = Promise.withResolvers(); worker.onerror = e => { resolve(e.message); }; const result = await promise; expect(result).toMatch( /THIS IS AN ERROR AND THIS PARTICULAR STRING DOESNT APPEAR IN THE SOURCE CODE SO WE KNOW FOR SURE IT SENT THE ACTUAL MESSAGE AND NOT JUST A DUMP OF THE SOURCE CODE AS IT ORIGINALLY WAS/, ); }); }); test("worker", done => { const worker = new Worker(new URL("worker-fixture.js", import.meta.url).href, { smol: true, }); expect(worker.threadId).toBeGreaterThan(0); worker.postMessage("hello"); worker.onerror = e => { done(e.error); }; worker.onmessage = e => { try { expect(e.data).toEqual("initial message"); } catch (e) { done(e); } finally { worker.terminate(); done(); } worker.terminate(); done(); }; }); test("worker-env", done => { const worker = new Worker(new URL("worker-fixture-env.js", import.meta.url).href, { env: { // Verify that we use putDirectMayBeIndex instead of putDirect [0]: "123", [1]: "234", hello: "world", another_key: 123 as any, }, }); worker.postMessage("hello"); worker.onerror = e => { done(e.error); }; worker.onmessage = e => { try { expect(e.data).toEqual({ env: { [0]: "123", [1]: "234", hello: "world", another_key: "123", }, hello: "world", }); } catch (e) { done(e); } finally { worker.terminate(); done(); } }; }); test("worker-env with a lot of properties", done => { const obj: any = {}; for (let i = 0; i < 1000; i++) { obj["prop " + i] = Math.random().toString(); } const worker = new Worker(new URL("worker-fixture-env.js", import.meta.url).href, { env: obj, }); worker.postMessage("hello"); worker.onerror = e => { done(e.error); }; worker.onmessage = e => { try { expect(e.data).toEqual({ env: obj, hello: undefined, }); } catch (e) { done(e); } finally { worker.terminate(); done(); } }; }); test("argv / execArgv defaults", async () => { const worker = new Worker(new URL("worker-fixture-argv.js", import.meta.url).href, {}); worker.postMessage("hello"); const result = await waitForWorkerResult(worker, "hello"); expect(result.argv).toHaveLength(2); expect(result.execArgv).toEqual(process.execArgv); }); test("argv / execArgv options", async () => { const worker_argv = ["--some-arg=1", "--some-arg=2"]; const worker_execArgv = ["--no-warnings", "--no-deprecation", "--tls-min-v1.2"]; const original_argv = [...process.argv]; const original_execArgv = [...process.execArgv]; const worker = new Worker(new URL("worker-fixture-argv.js", import.meta.url).href, { argv: worker_argv, execArgv: worker_execArgv, }); const result = await waitForWorkerResult(worker, "hello"); expect(result).toEqual({ argv: [original_argv[0], original_argv[1].replace(import.meta.file, "worker-fixture-argv.js"), ...worker_argv], execArgv: worker_execArgv, }); // ensure they didn't change for the main thread expect(process.argv).toEqual(original_argv); expect(process.execArgv).toEqual(original_execArgv); }); test("sending 50 messages should just work", done => { const worker = new Worker(new URL("worker-fixture-many-messages.js", import.meta.url).href, {}); worker.postMessage("initial message"); worker.addEventListener("message", ({ data }) => { if (data.done) { worker.terminate(); done(); } else { worker.postMessage({ i: data.i + 1 }); } }); }); test("worker with event listeners doesn't close event loop", done => { const x = Bun.spawn({ cmd: [bunExe(), path.join(import.meta.dir, "many-messages-event-loop.js"), "worker-fixture-many-messages.js"], env: bunEnv, stdio: ["inherit", "pipe", "inherit"], }); const timer = setTimeout(() => { x.kill(); done(new Error("timeout")); }, 1000); x.exited.then(async code => { clearTimeout(timer); if (code !== 0) { done(new Error("exited with non-zero code")); } else { const text = await new Response(x.stdout).text(); if (!text.includes("done")) { console.log({ text }); done(new Error("event loop killed early")); } else { done(); } } }); }); test("worker with event listeners doesn't close event loop 2", done => { const x = Bun.spawn({ cmd: [bunExe(), path.join(import.meta.dir, "many-messages-event-loop.js"), "worker-fixture-many-messages2.js"], env: bunEnv, stdio: ["inherit", "pipe", "inherit"], }); const timer = setTimeout(() => { x.kill(); done(new Error("timeout")); }, 1000); x.exited.then(async code => { clearTimeout(timer); if (code !== 0) { done(new Error("exited with non-zero code")); } else { const text = await new Response(x.stdout).text(); if (!text.includes("done")) { console.log({ text }); done(new Error("event loop killed early")); } else { done(); } } }); }); test("worker with process.exit", done => { const worker = new Worker(new URL("worker-fixture-process-exit.js", import.meta.url).href, { smol: true, }); worker.addEventListener("close", e => { try { expect(e.code).toBe(2); } catch (e) { done(e); } done(); }); }); describe("worker event", () => { test("is fired with the right object", () => { const { promise, resolve } = Promise.withResolvers(); let worker: Worker | undefined = undefined; let called = false; process.once("worker", eventWorker => { called = true; expect(eventWorker as any).toBe(worker); resolve(); }); worker = new Worker(new URL("data:text/javascript,")); expect(called).toBeFalse(); return promise; }); }); describe("error event", () => { test("is fired with a string of the error", async () => { const worker = new Worker("data:text/javascript,throw 5"); const [err] = await once(worker, "error"); expect(err.type).toBe("error"); expect(err.message).toBe("5"); expect(err.error).toBe(null); }); }); }); // TODO: move to node:worker_threads tests directory describe("worker_threads", () => { test("worker with process.exit", done => { const worker = new wt.Worker(new URL("worker-fixture-process-exit.js", import.meta.url).href, { smol: true, }); worker.on("exit", code => { try { expect(code).toBe(2); } catch (e) { done(e); return; } done(); }); }); test("worker terminate while setting up thread", async () => { // this test is inherently somewhat flaky: if we call terminate() before the worker starts // running any JavaScript the code will be 0 like we expect, but if we terminate while it is // running code the exit code is 1 instead (this happens in Node.js too). this means we can // randomly see an exit code of 1 if the main thread happens to run slower than usual and allows // the worker to run some code. // // to prevent it from polluting the flaky test list, we try 10 times and expect: // - at least 1 time the exit code was 0 // - the exit code is never something other than 0 or 1 const codes: number[] = []; for (let i = 0; i < 10; i++) { const worker = new wt.Worker(new URL("worker-fixture-hang.js", import.meta.url).href, { smol: true, }); worker.on("error", expect.unreachable); const code = await worker.terminate(); expect(code === 0 || code === 1, `unexpected exit code ${code}`).toBeTrue(); codes.push(code); } expect(codes.includes(0)).toBeTrue(); }); test("worker with process.exit (delay) and terminate", async () => { const worker = new wt.Worker(new URL("worker-fixture-process-exit.js", import.meta.url).href, { smol: true, }); await Bun.sleep(200); const code = await worker.terminate(); expect(code).toBe(2); }); test.todo("worker terminating forcefully properly interrupts", async () => { const worker = new wt.Worker(new URL("worker-fixture-while-true.js", import.meta.url).href, {}); await new Promise(done => { worker.on("message", () => done()); }); const code = await worker.terminate(); expect(code).toBe(1); }); test("worker without argv/execArgv", async () => { const worker = new wt.Worker(new URL("worker-fixture-argv.js", import.meta.url), {}); const promise = new Promise(resolve => worker.on("message", resolve)); worker.postMessage("hello"); const result = await promise; expect(result.argv).toHaveLength(process.argv.length); expect(result.execArgv).toHaveLength(process.execArgv.length); }); test("worker with argv/execArgv", async () => { const worker_argv = ["--some-arg=1", "--some-arg=2"]; const worker_execArgv = ["--no-warnings", "--no-deprecation", "--tls-min-v1.2"]; const original_argv = [...process.argv]; const original_execArgv = [...process.execArgv]; const worker = new wt.Worker(new URL("worker-fixture-argv.js", import.meta.url), { argv: worker_argv, execArgv: worker_execArgv, }); const promise = new Promise(resolve => worker.once("message", resolve)); worker.postMessage("hello"); const result = await promise; expect(result).toEqual({ argv: [original_argv[0], original_argv[1].replace(import.meta.file, "worker-fixture-argv.js"), ...worker_argv], execArgv: worker_execArgv, }); // ensure they didn't change for the main thread expect(process.argv).toEqual(original_argv); expect(process.execArgv).toEqual(original_execArgv); }); test("worker with eval = false fails with code", async () => { let has_error = false; const worker = new wt.Worker("console.log('this should not get printed')", { eval: false }); const [err] = await once(worker, "error"); expect(err.constructor.name).toEqual("Error"); expect(err.message).toMatch(/BuildMessage: ModuleNotFound.+/); }); test("worker with eval = true succeeds with valid code", async () => { let message; const worker = new wt.Worker("postMessage('hello')", { eval: true }); worker.on("message", e => { message = e; }); const p = new Promise((resolve, reject) => { worker.on("error", reject); worker.on("exit", resolve); }); await p; expect(message).toEqual("hello"); }); });