From 7115b595ffeabd9ca724472f8e5e4ac819688aa5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 31 Oct 2026 11:38:30 +0000 Subject: [PATCH] test(aot): compile less of Bun's library in workers-from-texts-that-the-build-knows.test.ts and state what each command prints The programs of this file hand console.log one text. A value of unknown type made six builds compile the code that formats any value: 436 functions of Bun's library, not 66. The three ways to start a Worker from what the program makes at run time are in one program. The first argument of a run selects the way. That program is built once. A fourth way starts a Worker from the text that the build knows, to show that the executable holds that text. Each run of plain Bun and of an executable states its stdout, an empty stderr, the exit code and the signal. A build states its warnings and errors by code and place. A run that must stop at the interpreter names the Worker that it stops at. --- ...rs-from-texts-that-the-build-knows.test.ts | 339 ++++++++++++------ 1 file changed, 222 insertions(+), 117 deletions(-) diff --git a/test/aot/workers-from-texts-that-the-build-knows.test.ts b/test/aot/workers-from-texts-that-the-build-knows.test.ts index a08c7f8449f3..f7751d2f155f 100644 --- a/test/aot/workers-from-texts-that-the-build-knows.test.ts +++ b/test/aot/workers-from-texts-that-the-build-knows.test.ts @@ -1,10 +1,11 @@ // A Worker that starts from a text or from the path of a file which is known when the program is built runs code that was compiled ahead of time. // ./parallel/workers has the programs that go through the ways to write such a text or path. They run where the interpreter is forbidden. // node:worker_threads brings the streams of Node.js along, so only the programs that are about it import it. -import { expect } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; -import { readFileSync, symlinkSync } from "node:fs"; -import { join } from "node:path"; +// console.log brings the code that formats any value along where the build cannot tell that it is handed texts, so the other programs hand it one text. +import { afterAll, expect } from "bun:test"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { existsSync, readFileSync, statSync, symlinkSync } from "node:fs"; +import { basename, join } from "node:path"; import { built, slot, spawned, withoutCoreDump, withoutCrashReport, withoutDebugInformation } from "./harness/commands"; import { compileCommand, runtimeEnvironment } from "./harness/flags"; import { describe, test } from "./supported"; @@ -15,21 +16,47 @@ const timeout = 600_000; const compiledOnly = { ...bunEnv, [runtimeEnvironment.forbidsInterpreter]: "1" }; const listen = ` - worker.onmessage = event => (console.log("message", event.data), worker.terminate()); - worker.onerror = event => (console.log("error", /^[A-Za-z]*Error: .*|Cannot find module/m.exec(event.message)?.[0]), event.preventDefault()); + worker.onmessage = event => (console.log("message " + event.data), worker.terminate()); + worker.onerror = event => (console.log("error " + /^[A-Za-z]*Error: .*|Cannot find module/m.exec(event.message)?.[0]), event.preventDefault()); `; -async function build(folder: string, ...entries: string[]) { - const executable = join(folder, "program"); - const report = join(folder, "report.json"); +/** `file`: the executable on disk. */ +async function build(folder: string, entries: string[], name = "program") { + const executable = join(folder, name); + const report = join(folder, `${name}.report.json`); const flags = [`--aot-report=${report}`, ...withoutDebugInformation()]; const command = [bunExe(), ...compileCommand, ...flags, ...entries, "--outfile", executable]; - return { executable, report, ...(await spawned(command, folder, bunEnv)) }; + const file = executable + (isWindows ? ".exe" : ""); + return { executable, file, report, ...(await spawned(command, folder, bunEnv)) }; } /** How many programs the build has compiled, also those that it then threw away. */ const programsCompiled = (report: string): number => JSON.parse(readFileSync(report, "utf8")).statistics.EngineRuns; +/** What a command comes to that printed `stdout`, said nothing else and ended by itself. */ +const ended = (stdout: string) => ({ stdout, stderr: "", exitCode: 0, signal: null }); + +/** The warnings and the errors of a build, each as its kind, its code and the place that it names: a file without its folder, a text whole. */ +const said = (stderr: string) => + Array.from( + stderr.matchAll(/^(warn|error): (TS\d+): .*\r?\n +at (.*?)\r?$/gm), + ([, kind, code, place]) => `${kind} ${code} at ${place.startsWith("data:") ? place : basename(place)}`, + ); + +/** The line and the column at which `part` begins in `text`, as a build writes them. */ +function placeOf(text: string, part: string) { + const at = text.indexOf(part); + if (at === -1) throw new Error(`the text does not hold ${part}`); + const before = text.slice(0, at).split("\n"); + return `${before.length}:${before.at(-1)!.length + 1}`; +} + +/** The build ended well, and `warnings` is all that it said. */ +function builtSaying(compiled: { stderr: string; exitCode: number }, warnings: string[] = []) { + built(compiled); + expect(said(compiled.stderr)).toEqual(warnings); +} + describe.concurrent("compile/aot/workers from texts that the build knows", () => { const speaks = `import { parentPort } from "node:worker_threads";\nparentPort!.postMessage("spoken");\n`; const startsBoth = `import { Worker } from "node:worker_threads"; @@ -49,43 +76,81 @@ new Worker(new URL("./second.ts", import.meta.url)).on("message", message => con (_, files, entries, programsOfExecutable) => slot(async () => { using dir = tempDir("aot-workers-from-texts", { "main.ts": startsBoth, "second.ts": speaks, ...files }); - const compiled = await build(String(dir), ...entries); - built(compiled); + const compiled = await build(String(dir), entries); + builtSaying(compiled); expect(programsCompiled(compiled.report)).toBe(programsOfExecutable); }), timeout, ); - test.each([ - ["a data: URL", `new Worker(made("data:text/javascript,postMessage(1)"))`], - ["an object URL", `new Worker(URL.createObjectURL(new Blob([made("postMessage(2)")])))`], + // What each of these hands to the Worker is made while the program runs, so the build knows none of it. The text that the + // second line of the program starts a Worker from is the only one that the executable holds. + const madeAtRunTime = [ + [ + "a data: URL", + `new Worker(made("data:text/javascript,postMessage(1)"))`, + "message 1\n", + /^crashIfInterpreterIsUsed: Worker data:text\/javascript,postMessage\(1\)$/m, + ], + [ + "an object URL", + `new Worker(URL.createObjectURL(new Blob([made("postMessage(2)")])))`, + "message 2\n", + /^crashIfInterpreterIsUsed: Worker blob:[-0-9a-f]+$/m, + ], [ "an object URL of a text that is compiled, with a type that reads it in another way,", `new Worker(URL.createObjectURL(new Blob(["postMessage(1)"], { type: made("application/typescript") })))`, + "message 1\n", + /^crashIfInterpreterIsUsed: Worker blob:[-0-9a-f]+$/m, ], - ])( + ] as const; + + // One program holds the three, and the first argument of a run says which of them it takes: one build and one new executable + // to start, not three. None of the three tells the build what another hands over. + let folderOfWays: ReturnType | undefined; + let programOfWays: Promise<{ folder: string; executable: string }> | undefined; + afterAll(() => folderOfWays?.[Symbol.dispose]()); + const builtWithEveryWay = () => + (programOfWays ??= slot(async () => { + folderOfWays = tempDir("aot-workers-from-texts", { + "main.js": ` + const made = text => Buffer.from(text).toString(); + new Worker(URL.createObjectURL(new Blob(["postMessage(1)"]))).terminate(); + const ways = [ + ${madeAtRunTime.map(([, start]) => `() => ${start},`).join("\n ")} + () => new Worker(URL.createObjectURL(new Blob(["postMessage(1)"]))), + ]; + const worker = ways[process.argv[2]]();${listen}`, + }); + const folder = String(folderOfWays); + const compiled = await build(folder, ["main.js"]); + builtSaying(compiled); + // The last way is the text of the second line again. It runs where the interpreter is forbidden, so the executable + // holds that text, and a run that stops there has stopped at the Worker of its own way. + const known = await spawned([compiled.executable, String(madeAtRunTime.length)], folder, compiledOnly); + expect(known).toEqual(ended("message 1\n")); + return { folder, executable: compiled.executable }; + })); + + test.each(madeAtRunTime.map(([title, , printed, stopsAt], way) => [title, String(way), printed, stopsAt] as const))( "%s that the program makes at run time is interpreted", - (_, start) => - slot(async () => { - using dir = tempDir("aot-workers-from-texts", { - "main.js": ` - const made = text => Buffer.from(text).toString(); - new Worker(URL.createObjectURL(new Blob(["postMessage(1)"]))).terminate(); - const worker = ${start};${listen}`, - }); - const compiled = await build(String(dir), "main.js"); - built(compiled); - const ran = await spawned([compiled.executable], String(dir), bunEnv); - expect({ stdout: ran.stdout, exitCode: ran.exitCode }).toEqual({ - stdout: expect.stringMatching(/^message [12]\n$/), - exitCode: 0, - }); - const stopped = await spawned(withoutCoreDump([compiled.executable]), String(dir), { + async (_, way, printed, stopsAt) => { + const { folder, executable } = await builtWithEveryWay(); + await slot(async () => { + expect(await spawned([executable, way], folder, bunEnv)).toEqual(ended(printed)); + const stopped = await spawned(withoutCoreDump([executable, way]), folder, { ...compiledOnly, ...withoutCrashReport, }); - expect(stopped.stderr).toContain("crashIfInterpreterIsUsed"); - }), + // It names the Worker whose code is not in the executable, before anything of that code has run. + expect(stopped.stderr).toMatch(stopsAt); + expect({ stdout: stopped.stdout, endedWithZero: stopped.exitCode === 0 }).toEqual({ + stdout: "", + endedWithZero: false, + }); + }); + }, timeout, ); @@ -122,15 +187,24 @@ new Worker(new URL("./second.ts", import.meta.url)).on("message", message => con "project/node_modules/pool/lib/worker.js": `postMessage("the worker of a package");`, }); const sources = join(String(dir), "project", "src"); - const plain = await spawned([bunExe(), "run", "main.js"], sources, bunEnv); - const compiled = await build(sources, "main.js"); - built(compiled); - expect(plain.stdout).toContain("a package: the worker of a package\n./no-such-file.ts: error\n"); - const expected = plain.stdout.replace("a package: the worker of a package", "a package: error"); - for (const folder of [sources, join(String(dir), "elsewhere")]) { - const ran = await spawned([compiled.executable], folder, compiledOnly); - expect({ stdout: ran.stdout, exitCode: ran.exitCode }).toEqual({ stdout: expected, exitCode: 0 }); - } + const printed = (ofThePackage: string) => + ended( + [ + "./worker.ts: worker.ts", + "./worker: worker.ts", + "./lib/../worker.ts: worker.ts", + `a package: ${ofThePackage}`, + "./no-such-file.ts: error", + "", + ].join("\n"), + ); + expect(await spawned([bunExe(), "run", "main.js"], sources, bunEnv)).toEqual( + printed("the worker of a package"), + ); + const compiled = await build(sources, ["main.js"]); + builtSaying(compiled); + for (const folder of [sources, join(String(dir), "elsewhere")]) + expect(await spawned([compiled.executable], folder, compiledOnly)).toEqual(printed("error")); }), timeout, ); @@ -156,13 +230,11 @@ new Worker(new URL("./second.ts", import.meta.url)).on("message", message => con }); const folder = String(dir); symlinkSync(join(folder, "packages", "pool"), join(folder, "node_modules", "pool"), "junction"); - const compiled = await build(folder, "src/main.js"); - built(compiled); - const ran = await spawned([compiled.executable], folder, compiledOnly); - expect({ stdout: ran.stdout, exitCode: ran.exitCode }).toEqual({ - stdout: "a package of the workspace: its worker\n", - exitCode: 0, - }); + const compiled = await build(folder, ["src/main.js"]); + builtSaying(compiled); + expect(await spawned([compiled.executable], folder, compiledOnly)).toEqual( + ended("a package of the workspace: its worker\n"), + ); }), timeout, ); @@ -176,18 +248,16 @@ new Worker(new URL("./second.ts", import.meta.url)).on("message", message => con "worker.js": `postMessage("worker.js");`, }); const folder = String(dir); - const alone = await build(folder, "main.js"); - built(alone); - const command = [bunExe(), ...compileCommand, ...withoutDebugInformation(), "main.js", "worker.js"]; - built(await spawned([...command, "--outfile", join(folder, "listed")], folder, bunEnv)); - for (const executable of [alone.executable, join(folder, "listed")]) { - const ran = await spawned([executable], folder, compiledOnly); - expect({ stdout: ran.stdout, exitCode: ran.exitCode }).toEqual({ - stdout: "listed: worker.js\n", - exitCode: 0, - }); - } - expect(Bun.file(join(folder, "listed")).size).toBe(Bun.file(alone.executable).size); + const alone = await build(folder, ["main.js"]); + builtSaying(alone); + const listed = await build(folder, ["main.js", "worker.js"], "listed"); + builtSaying(listed); + for (const { executable } of [alone, listed]) + expect(await spawned([executable], folder, compiledOnly)).toEqual(ended("listed: worker.js\n")); + expect({ programs: programsCompiled(listed.report), bytes: statSync(listed.file).size }).toEqual({ + programs: programsCompiled(alone.report), + bytes: statSync(alone.file).size, + }); }), timeout, ); @@ -230,14 +300,31 @@ new Worker(new URL("./second.ts", import.meta.url)).on("message", message => con "project/node_modules/@scope/tool/main.js": `postMessage("@scope/tool/main.js");`, }); const sources = join(String(dir), "project", "src", "app"); - const plain = await spawned([bunExe(), "run", "main.ts"], sources, bunEnv); - expect(plain.stdout.split("\n").filter(line => line.endsWith(": error"))).toEqual(["no-such-name: error"]); - const compiled = await build(sources, "main.ts"); - built(compiled); - for (const folder of [sources, join(String(dir), "elsewhere")]) { - const ran = await spawned([compiled.executable], folder, compiledOnly); - expect({ stdout: ran.stdout, exitCode: ran.exitCode }).toEqual({ stdout: plain.stdout, exitCode: 0 }); - } + // Plain Bun and the executable find the same file for each of them. + const printed = ended( + [ + "worker.ts: worker.ts", + "worker: worker.ts", + "lib/inner.ts: lib/inner.ts", + "pool: pool/lib/index.js", + "pool/lib/job.js: pool/lib/job.js", + "pool/lib/job: pool/lib/job.js", + "@scope/tool: @scope/tool/main.js", + "no-such-name: error", + "../beside.ts: beside.ts and what it imports", + "../../workers/job.ts: workers/job.ts", + "a URL one folder above: beside.ts and what it imports", + "its href: beside.ts and what it imports", + "a joined path one folder above: beside.ts and what it imports", + "a URL from a folder below: beside.ts and what it imports", + "", + ].join("\n"), + ); + expect(await spawned([bunExe(), "run", "main.ts"], sources, bunEnv)).toEqual(printed); + const compiled = await build(sources, ["main.ts"]); + builtSaying(compiled); + for (const folder of [sources, join(String(dir), "elsewhere")]) + expect(await spawned([compiled.executable], folder, compiledOnly)).toEqual(printed); }), timeout, ); @@ -246,56 +333,65 @@ new Worker(new URL("./second.ts", import.meta.url)).on("message", message => con "a file that is named by a path and does not compile is left out, and the build says so", () => slot(async () => { - using dir = tempDir("aot-workers-from-texts", { - "main.js": ` + const start = `new Worker(new URL("./imports.js", import.meta.url))`; + const main = ` ${report} await report("sound", new Worker(new URL("./sound.js", import.meta.url))); - await report("imports what is not there", new Worker(new URL("./imports.js", import.meta.url))); - `, + await report("imports what is not there", ${start}); + `; + using dir = tempDir("aot-workers-from-texts", { + "main.js": main, "sound.js": `postMessage("sound.js");`, "imports.js": `import "./inner.js"; postMessage("imports.js");`, "inner.js": `import "./no-such-file.js";`, }); - const compiled = await build(String(dir), "main.js"); - built(compiled); - expect(compiled.stderr).toContain("warn: TS100150"); - expect(compiled.stderr).toContain("warn: TS100072"); - const ran = await spawned([compiled.executable], String(dir), compiledOnly); - expect({ stdout: ran.stdout, exitCode: ran.exitCode }).toEqual({ - stdout: "sound: sound.js\nimports what is not there: error\n", - exitCode: 0, - }); + const compiled = await build(String(dir), ["main.js"]); + // What is wrong, in the file that is wrong, and then the Worker that starts from what imports it. + builtSaying(compiled, ["warn TS100150 at inner.js:1:1", `warn TS100072 at main.js:${placeOf(main, start)}`]); + expect(await spawned([compiled.executable], String(dir), compiledOnly)).toEqual( + ended("sound: sound.js\nimports what is not there: error\n"), + ); }), timeout, ); test.each([ - ["a syntax error", "postMessage((", "TS1109", "SyntaxError: Unexpected end of file"], - ["words that are no program", "this is no program", "TS1005", "undefined"], - ["an import that is not found", `import "./no-such-file.js"; postMessage(1)`, "TS100150", "Cannot find module"], + ["a syntax error", "postMessage((", "TS1109", "1:14", "SyntaxError: Unexpected end of file"], + ["words that are no program", "this is no program", "TS1005", "1:6", "undefined"], + [ + "an import that is not found", + `import "./no-such-file.js"; postMessage(1)`, + "TS100150", + "1:1", + "Cannot find module", + ], ])( "a text with %s is left out, and the build says so", - (_, text, code, said) => + (_, text, code, place, heard) => slot(async () => { - using dir = tempDir("aot-workers-from-texts", { - "main.js": ` + const url = "data:text/javascript," + text; + const start = `new Worker(${JSON.stringify(url)})`; + const main = ` const sound = new Worker("data:text/javascript,postMessage('sound')"); sound.onmessage = event => { - console.log("message", event.data); + console.log("message " + event.data); sound.terminate(); - const worker = new Worker(${JSON.stringify("data:text/javascript," + text)});${listen} + const worker = ${start};${listen} }; - `, - }); - const compiled = await build(String(dir), "main.js"); + `; + using dir = tempDir("aot-workers-from-texts", { "main.js": main }); + const compiled = await build(String(dir), ["main.js"]); built(compiled); - expect(compiled.stderr).toContain(`warn: ${code}`); - expect(compiled.stderr).toContain("warn: TS100072"); - const ran = await spawned([compiled.executable], String(dir), bunEnv); - expect({ stdout: ran.stdout, exitCode: ran.exitCode }).toEqual({ - stdout: `message sound\nerror ${said}\n`, - exitCode: 0, - }); + // First what is wrong with the text, where it is wrong. Whatever else the build says is about that text too, but for + // one warning: the Worker that starts from it. + const warnings = said(compiled.stderr); + expect(warnings[0]).toBe(`warn ${code} at ${url}:${place}`); + expect(warnings.filter(warning => !warning.includes(` at ${url}:`))).toEqual([ + `warn TS100072 at main.js:${placeOf(main, start)}`, + ]); + expect(await spawned([compiled.executable], String(dir), bunEnv)).toEqual( + ended(`message sound\nerror ${heard}\n`), + ); }), timeout, ); @@ -308,7 +404,7 @@ new Worker(new URL("./second.ts", import.meta.url)).on("message", message => con "main.ts": ` const heard = (worker: Worker) => new Promise(done => { - worker.onmessage = event => (console.log("message", event.data), worker.terminate(), done()); + worker.onmessage = event => (console.log("message " + event.data), worker.terminate(), done()); worker.onerror = event => (console.log("error"), event.preventDefault(), done()); worker.postMessage(20); }); @@ -331,38 +427,47 @@ new Worker(new URL("./second.ts", import.meta.url)).on("message", message => con "listener.ts": ` self.addEventListener("message", (event: MessageEvent) => { postMessage(event.data + 3); }); self.onerror = null; - console.log(typeof self.dispatchEvent, typeof dispatchEvent, onerror); + console.log(typeof self.dispatchEvent + " " + typeof dispatchEvent + " " + onerror); `, }); - const compiled = await build(String(dir), "main.ts"); - built(compiled); + const compiled = await build(String(dir), ["main.ts"]); + builtSaying(compiled); expect(compiled.stderr).not.toContain("warn:"); - const ran = await spawned([compiled.executable], String(dir), compiledOnly); - expect({ stdout: ran.stdout, exitCode: ran.exitCode }).toEqual({ - stdout: "message 21\nmessage 22\nfunction function null\nmessage 23\nmessage 24\nmessage 25\n", - exitCode: 0, - }); + expect(await spawned([compiled.executable], String(dir), compiledOnly)).toEqual( + ended("message 21\nmessage 22\nfunction function null\nmessage 23\nmessage 24\nmessage 25\n"), + ); }), timeout, ); test.each([ - ["a file that is named by a path", `new URL("./worker.ts", import.meta.url)`], - ["a text", JSON.stringify(`data:text/javascript,let count: number = "text"; postMessage(count)`)], + ["a file that is named by a path", `new URL("./worker.ts", import.meta.url)`, /^error TS2322 at worker\.ts:1:5$/], + [ + "a text", + JSON.stringify(`data:text/javascript,let count: number = "text"; postMessage(count)`), + /^error TS2322 at .+:1:5$/, + ], ])( "a type error in %s stops the build, as it does in an entry point", - (_, code) => + (_, code, error) => slot(async () => { using dir = tempDir("aot-workers-from-texts", { "main.ts": `const worker = new Worker(${code});${listen}`, "worker.ts": `let count: number = "text"; postMessage(count);`, }); - const compiled = await build(String(dir), "main.ts"); - expect({ stderr: compiled.stderr, exitCode: compiled.exitCode }).toEqual({ - stderr: expect.stringContaining("error: TS2322"), + const compiled = await build(String(dir), ["main.ts"]); + // The error is all that the build says: it stops, and leaves nothing out. + expect({ + said: said(compiled.stderr), + exitCode: compiled.exitCode, + signal: compiled.signal, + hasWrittenAnExecutable: existsSync(compiled.file), + }).toEqual({ + said: [expect.stringMatching(error)], exitCode: 1, + signal: null, + hasWrittenAnExecutable: false, }); - expect(compiled.stderr).not.toContain("TS100072"); }), timeout, );