Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 123 additions & 45 deletions test/aot/layers-of-a-test-run-share-one-realm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,59 +10,65 @@ import { test } from "./supported";
// Every project here prints what it sees; a run in layers has to print what plain Bun prints.
const part = (name: string) =>
readFileSync(join(import.meta.dir, "layers-of-a-test-run-share-one-realm", name + ".txt"), "utf8");
// A checkout may end the lines of a fixture with \r\n.
const listed = (name: string) =>
part(name)
.split(/\r?\n/)
.filter(line => line);

async function seen(directory: string, flags: string[], words: string) {
const { stdout, stderr } = await run(
const { stdout, stderr, exitCode } = await run(
[bunExe(), "test", ...flags, "--isolate", "--parallel=4", "--timeout=60000"],
directory,
{
...bunEnv,
BUN_DEBUG_TEST_AOT_WORDS: words,
},
);
const all = stdout + "\n" + stderr;
return {
lines: all
.split("\n")
.filter(line => line.startsWith("SEEN"))
.sort(),
deadWorkers: all.split("\n").filter(line => line.includes("worker crashed")),
};
const all = (stdout + "\n" + stderr).split("\n");
return [
{
lines: all.filter(line => line.startsWith("SEEN")).sort(),
ended: {
// The last lines of the run, without the time that it took.
summary: stderr.match(/^ \d+ pass\n(?:.*\n)*?Ran \d+ tests across \d+ files\./m)?.[0],
deadWorkers: all.filter(line => line.includes("worker crashed")),
exitCode,
},
},
// A run that is told how many test files go to a program says how it is built.
stderr.match(/^AOT test run \| .*\b(test files \d+ \| to a program \d+)/m)?.[1],
] as const;
}

async function expectLayersToSeeWhatPlainSees(
name: string,
files: Record<string, string>,
filesOfPiece: number,
fewestLines: number,
) {
/** What a project prints and how its run ends, under plain Bun and in layers with `filesOfPiece` test files to a program. */
async function seenPlainAndInLayers(name: string, files: Record<string, string>, filesOfPiece: number) {
using dir = tempDir(name, { "package.json": `{ "name": "p", "type": "module" }`, "tsconfig.json": `{}`, ...files });
const plain = await seen(String(dir), [], "");
const layers = await seen(
String(dir),
["--aot"],
`TestRunLayers:2,TestRunPartFiles:${filesOfPiece},TestRunPartsGoOnAfterCrash`,
);
expect(plain.lines.length).toBeGreaterThanOrEqual(fewestLines);
expect(layers.deadWorkers).toEqual([]);
expect(layers.lines).toEqual(plain.lines);
const [[plain], [layers, programs]] = await Promise.all([
seen(String(dir), [], ""),
seen(String(dir), ["--aot"], `TestRunLayers:2,TestRunPartFiles:${filesOfPiece}`),
]);
const testFiles = Object.keys(files).filter(path => path.endsWith(".test.js")).length;
expect(programs).toBe(`test files ${testFiles} | to a program ${filesOfPiece}`);
return { plain, layers };
}

const expressions = part("made-by-the-library")
.split("\n")
.filter(line => line);
const expressions = listed("made-by-the-library");
const number = (index: number) => String(index).padStart(3, "0");
// Node's console asks its streams for more than `write`, so the statement with this one throws, under plain Bun too.
const throwsInPlainBun = (expression: string) => expression.includes("new console.Console({ stdout: {");
// One statement starts a program named `true`, and throws where the system has none. The first two tests state what comes through.
const lacksTrue = !Bun.which("true");
if (lacksTrue)
console.error(
"SKIPPED: test/aot/layers-of-a-test-run-share-one-realm.test.ts: the first two tests need a program named `true` in PATH",
);

test("a value that the library or native code makes, used by one statement of a test file", async () => {
const files: Record<string, string> = {};
expressions.forEach((expression, index) => {
files[`t${number(index)}.test.js`] =
`import { test } from "bun:test";\ntest("f", () => { ${expression}; console.log("SEEN", "${number(index)}", "came through"); });\n`;
});
await expectLayersToSeeWhatPlainSees("aot-layers-made-by-the-library-bare", files, 4, expressions.length - 3);
}, 600000);

test("the same values, in a test file and in a file of the project", async () => {
// Most of a run in layers is the build of its lowest layer. For the first two tests that layer is the same program, Bun's own library alone, so their test files are one project, which runs once in each way.
// The third test keeps a project of its own: its lowest layer holds a package too, and a build of these two takes every module for one that a test may mock, because one expression reads `Bun.jest`.
let seenOnce: ReturnType<typeof seeExpressions> | undefined;
const expressionsSeen = () => (seenOnce ??= seeExpressions());
async function seeExpressions() {
const withRequire = `import { createRequire } from "node:module"; const require = createRequire(import.meta.url);\n`;
const files: Record<string, string> = {
"src/all.js":
Expand All @@ -71,23 +77,87 @@ test("the same values, in a test file and in a file of the project", async () =>
expressions.map(expression => ` async () => (${expression}),\n`).join("") +
"];\n",
};
// A program above the layers holds four test files that follow one another by name. A name begins with the number of its four, so that no program but the last holds files of both tests.
const count = { statement: 0, value: 0 };
const name = (kind: keyof typeof count, index: number) =>
`${number(Math.floor(count[kind]++ / 4))}-${kind}-${number(index)}.test.js`;
expressions.forEach((expression, index) => {
// Compiled code words one TypeError of the library otherwise.
if (expression.includes("new console.Console({ stdout: {")) return;
files[`t${number(index)}.test.js`] =
`import { test } from "bun:test"; import { all } from "./src/all.js"; ${withRequire}
files[name("statement", index)] =
`import { test } from "bun:test";\ntest("f", () => { ${expression}; console.log("SEEN", "${number(index)}", "came through"); });\n`;
// Compiled code words that TypeError of the library otherwise.
if (throwsInPlainBun(expression)) return;
files[name("value", index)] = `import { test } from "bun:test"; import { all } from "./src/all.js"; ${withRequire}
const note = (...parts) => console.log("SEEN", "${number(index)}", ...parts); const clean = error => "threw " + error.name + " " + String(error.message).slice(0, 90);
test("in the test file", async () => { try { note("own", String(await (${expression}))); } catch (error) { note("own", clean(error)); } });
test("in the file of the project", async () => { try { note("theirs", String(await all[${index}]())); } catch (error) { note("theirs", clean(error)); } });
`;
});
await expectLayersToSeeWhatPlainSees("aot-layers-made-by-the-library", files, 4, 2 * expressions.length - 2);
}, 600000);
const { plain, layers } = await seenPlainAndInLayers("aot-layers-made-by-the-library", files, 4);
const isOfStatement = (line: string) => line.endsWith(" came through");
const byTest = ({ lines, ended }: typeof plain) => ({
statements: lines.filter(isOfStatement),
values: lines.filter(line => !isOfStatement(line)),
ended,
});
return { plain: byTest(plain), layers: byTest(layers) };
}
// How both runs of that project end: the statement that throws fails its test.
const endOfExpressions = {
summary: " 351 pass\n 1 fail\nRan 352 tests across 235 files.",
deadWorkers: [],
exitCode: 1,
};

test.skipIf(lacksTrue)(
"a value that the library or native code makes, used by one statement of a test file",
async () => {
const { plain, layers } = await expressionsSeen();
const stated = {
statements: expressions.flatMap((expression, index) =>
throwsInPlainBun(expression) ? [] : [`SEEN ${number(index)} came through`],
),
ended: endOfExpressions,
};
expect({ statements: plain.statements, ended: plain.ended }).toEqual(stated);
expect({ statements: layers.statements, ended: layers.ended }).toEqual(stated);
},
600000,
);

test.skipIf(lacksTrue)(
"the same values, in a test file and in a file of the project",
async () => {
const { plain, layers } = await expressionsSeen();
// `[number, value]` of each expression, as the test file or the file of the project has it under plain Bun.
const valuesIn = (place: string) =>
plain.values.flatMap(line => {
const found = line.match(new RegExp(`^SEEN (\\d+) ${place} (.*)$`));
return found ? [[found[1], found[2]]] : [];
});
const [own, theirs] = [valuesIn("own"), valuesIn("theirs")];
const asked = expressions.flatMap((expression, index) => (throwsInPlainBun(expression) ? [] : [number(index)]));
expect(own.map(([index]) => index)).toEqual(asked);
expect(theirs.map(([index]) => index)).toEqual(asked);
expect([...own, ...theirs].filter(([, value]) => value.startsWith("threw "))).toEqual([]);
// The file of the project answers second, in the realm of the test file: it counts that file's listener and its mark too, and it is not the main module.
const differ = own.flatMap(([index, value], at) =>
value === theirs[at][1] ? [] : [[index, value, theirs[at][1]]],
);
expect(differ).toEqual([
["005", "1", "2"],
["046", "number,number,a,number,1", "number,number,a,number,2"],
["084", "function,string,function,string,true", "function,string,function,string,false"],
]);
expect(plain.ended).toEqual(endOfExpressions);
expect({ values: layers.values, ended: layers.ended }).toEqual({ values: plain.values, ended: endOfExpressions });
},
600000,
);

test("one of each thing, one queue of each kind, and what one layer sets the others see", async () => {
const shared = part("shared");
const testFile = shared.replaceAll(/^export const /gm, "const ") + part("asks");
await expectLayersToSeeWhatPlainSees(
const { plain, layers } = await seenPlainAndInLayers(
"aot-layers-one-realm",
{
"node_modules/pkg/package.json": `{ "name": "pkg", "version": "1.0.0", "type": "module", "main": "index.js" }`,
Expand All @@ -97,6 +167,14 @@ test("one of each thing, one queue of each kind, and what one layer sets the oth
"two.test.js": testFile,
},
1,
100,
);
const stated = {
// Each of the two test files gives every answer once.
lines: listed("answers")
.flatMap(answer => [answer, answer])
.sort(),
Comment thread
robobun marked this conversation as resolved.
Resolved
ended: { summary: " 12 pass\n 0 fail\nRan 12 tests across 2 files.", deadWorkers: [], exitCode: 0 },
};
expect(plain).toEqual(stated);
expect(layers).toEqual(stated);
}, 600000);
57 changes: 57 additions & 0 deletions test/aot/layers-of-a-test-run-share-one-realm/answers.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
SEEN custom helper shown my way { inner: shown my way }
SEEN custom own shown my way { inner: shown my way }
SEEN custom pkg shown my way { inner: shown my way }
SEEN depth helper 0 { a: [Object] }
SEEN depth own 0 { a: [Object] }
SEEN depth pkg 0 { a: [Object] }
SEEN emitted helper true true own 1,helper 1,pkg 1,own 2,helper 2,pkg 2 3
SEEN emitted own true true own 1,helper 1,pkg 1,own 2,helper 2,pkg 2 3
SEEN emitted pkg true true own 1,helper 1,pkg 1,own 2,helper 2,pkg 2 3
SEEN iterated helper a
SEEN iterated own a
SEEN iterated pkg a
SEEN listeners helper 1 true true 8192
SEEN listeners own 1 true true 8192
SEEN listeners pkg 1 true true 8192
SEEN made by helper judged by helper all
SEEN made by helper judged by own all
SEEN made by helper judged by pkg all
SEEN made by own judged by helper all
SEEN made by own judged by own all
SEEN made by own judged by pkg all
SEEN made by pkg judged by helper all
SEEN made by pkg judged by own all
SEEN made by pkg judged by pkg all
SEEN order tick own,tick helper,tick pkg,tick own,tick pkg,tick helper,then own,then helper,then pkg,then own,then pkg,then helper,micro own,micro helper,micro pkg,micro own,micro pkg,micro helper,immediate own,immediate helper,immediate pkg,immediate own,immediate pkg,immediate helper,timeout own,timeout helper,timeout pkg,timeout own,timeout pkg,timeout helper
SEEN piped helper a
SEEN piped own a
SEEN piped pkg a
SEEN prepared helper prepared n
SEEN prepared own prepared n
SEEN prepared pkg prepared n
SEEN prototype helper function function function function
SEEN prototype own function function function function
SEEN prototype pkg function function function function
SEEN set by helper read by helper 21 21 21 helper helper string helper
SEEN set by helper read by own 21 21 21 helper helper string helper
SEEN set by helper read by pkg 21 21 21 helper helper string helper
SEEN set by own read by helper 20 20 20 own own string own
SEEN set by own read by own 20 20 20 own own string own
SEEN set by own read by pkg 20 20 20 own own string own
SEEN set by pkg read by helper 22 22 22 pkg pkg string pkg
SEEN set by pkg read by own 22 22 22 pkg pkg string pkg
SEEN set by pkg read by pkg 22 22 22 pkg pkg string pkg
SEEN stack limit helper 1 2
SEEN stack limit own 1 2
SEEN stack limit pkg 1 2
SEEN store helper helper helper's later 5
SEEN store helper own helper's later 5
SEEN store helper pkg helper's later 5
SEEN store own helper own's later 5
SEEN store own own own's later 5
SEEN store own pkg own's later 5
SEEN store pkg helper pkg's later 5
SEEN store pkg own pkg's later 5
SEEN store pkg pkg pkg's later 5
SEEN things helper all the same
SEEN things pkg all the same
2 changes: 1 addition & 1 deletion test/aot/layers-of-a-test-run-share-one-realm/asks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ test("what one place sets the others see", () => {
places[setter].state.setMaxListeners(number); places[setter].state.setEnv("SET_BY", setter); places[setter].state.setGlobal("setBy", setter);
for (const reader of Object.keys(places)) note("set by", setter, "read by", reader, places[reader].state.maxListeners(), places[reader].state.env("SET_BY"), places[reader].state.global("setBy"));
}
Error.stackTraceLimit = 3; for (const reader of Object.keys(places)) note("stack limit", reader, places[reader].state.stackLimit(), places[reader].state.framesOfNew()); Error.stackTraceLimit = 10;
Error.stackTraceLimit = 1; for (const reader of Object.keys(places)) note("stack limit", reader, places[reader].state.stackLimit(), places[reader].state.framesOfNew()); Error.stackTraceLimit = 10;
Error.prepareStackTrace = (error, frames) => "prepared " + frames.length; for (const reader of Object.keys(places)) note("prepared", reader, places[reader].state.prepared().replace(/\d+/, "n")); Error.prepareStackTrace = undefined;
util.inspect.defaultOptions.depth = 0; for (const reader of Object.keys(places)) note("depth", reader, places[reader].state.inspectDepth()); util.inspect.defaultOptions.depth = 2;
class Shown { [util.inspect.custom]() { return "shown my way"; } } for (const reader of Object.keys(places)) note("custom", reader, places[reader].state.inspected(new Shown()), places[reader].state.inspected({ inner: new Shown() }));
Expand Down
2 changes: 1 addition & 1 deletion test/aot/layers-of-a-test-run-share-one-realm/shared.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const state = {
stored: () => storage.getStore(), storedOf: other => other.getStore(), runIn: (other, value, body) => other.run(value, body),
tick: (order, name) => process.nextTick(() => order.push("tick " + name)), micro: (order, name) => queueMicrotask(() => order.push("micro " + name)), resolved: (order, name) => Promise.resolve().then(() => order.push("then " + name)), immediate: (order, name) => setImmediate(() => order.push("immediate " + name)), timeout: (order, name) => setTimeout(() => order.push("timeout " + name), 0),
maxListeners: () => EventEmitter.defaultMaxListeners + " " + events.defaultMaxListeners + " " + new EventEmitter().getMaxListeners(), setMaxListeners: count => { EventEmitter.defaultMaxListeners = count; },
stackLimit: () => Error.stackTraceLimit, framesOfNew: () => new Error("x").stack.split("\n").length, prepared: () => { const stack = new Error("x").stack; return typeof stack === "string" ? "text" : String(stack); },
stackLimit: () => Error.stackTraceLimit, framesOfNew: () => new Error("x").stack.split("\n").length, prepared: () => String(new Error("x").stack).split("\n")[0],
inspectDepth: () => util.inspect.defaultOptions.depth + " " + util.inspect({ a: { b: { c: { d: 1 } } } }), inspected: value => util.inspect(value), env: name => process.env[name] + " " + Bun.env[name], setEnv: (name, value) => { process.env[name] = value; },
global: name => typeof globalThis[name] + " " + String(globalThis[name]), setGlobal: (name, value) => { globalThis[name] = value; }, poolSize: () => Buffer.poolSize, listeners: name => process.listenerCount(name), cwd: () => process.cwd() === path.resolve("."), symbol: description => Symbol.for(description),
prototypeHas: name => typeof [][name] + " " + typeof ({})[name] + " " + typeof (() => {})[name] + " " + typeof ""[name], captureRejections: () => events.captureRejections, title: () => typeof process.title,
Expand Down
Loading