diff --git a/test/aot/built-in-functions-where-tests-replace-members-of-other-classes.test.ts b/test/aot/built-in-functions-where-tests-replace-members-of-other-classes.test.ts index f50e53f4ea2b..035eb301952a 100644 --- a/test/aot/built-in-functions-where-tests-replace-members-of-other-classes.test.ts +++ b/test/aot/built-in-functions-where-tests-replace-members-of-other-classes.test.ts @@ -4,7 +4,7 @@ import { raiseDefaultTimeout } from "./harness/default-timeout"; import { run } from "./harness/run"; import { test } from "./supported"; -raiseDefaultTimeout(240_000); +raiseDefaultTimeout(120_000); // A built-in function has a row for a pristine operand, which leaves out what a lookup on such an operand finds, and a general row. Where tests replace a member that the table locks, // the first is passed by for the classes on whose instances a lookup finds the replaced member: Object.prototype.valueOf on an array, but not Object.prototype.toString, which @@ -74,36 +74,195 @@ const rows = ` return rows; `; -const replacements: Record = { - "object-and-function-tostring": ` +// What a row answers in plain Bun while nothing is replaced, in the order of the rows. +const answers: Record = { + "numbers joined": "1-2-3", + "nested lists joined": "1,2,3,x,4", + "String(list)": "1,2", + "list in a template": "1,2", + "list + text": "1,2", + "list.toString()": "1,2", + "holes joined": ",1,|xx", + "objects joined": "[object Object],[object Object]", + "functions joined": "true", + "numbers sorted": "1,2,3", + "texts sorted": "a,b", + "objects sorted": "3", + "sorted by a function": "3,2,1", + "slice": "2,3", + "map": "2,4,6", + "filter": "2,3", + "concat": "1,2,3,4", + "flat": "1,2,3", + "indexOf includes": "1 true", + "reverse splice": "3,2|4,1", + "push pop shift unshift": "1,2", + "spread of a list": "1,2,3", + "Array.from": "1,2|0,1", + "a list pattern": "1 9", + "for of a list": "6", + "list stringified": '[1,"a",null,{"b":2},[3]]', + "record stringified": '{"a":[1,2],"d":"1970-01-01T00:00:00.000Z","n":{"m":1}}', + "list.toLocaleString()": "1,a", + "list to a number": "5 10 true true", + "bytes joined": "1-2-3", + "String(bytes)": "1,2|3|1.5", + "bytes slice map": "2,3|2,3", + "spread of bytes": "7,8", + "bytes sorted": "-1,2,3", + "a map of pairs": "2 2", + "spread of a map": "1,2|1", + "String(map)": "[object Map][object Set]", + "a set of a list": "2 true", + "spread of a set": "3,4", + "map get set has": "1 true false", + "replace by a pattern": "a+b+c|a_b_", + "match search test": "b 2 true", + "split by a pattern": "a,b,c", + "matchAll": "2", + "String(pattern)": "/a/g/b/", + "String(date)": "Thu Jan 01 1970 00:00:00 GMT..", + "date to a number": "5 5", + "String(error)": "Error: e|TypeError: t", + "String(function)": "true", + "String(record)": "[object Object]|[object Object]|[object Object]", + "Object.prototype.toString.call": "[object Array][object Null][object Date]", + "record to a number": "NaN Object", + "wrappers": "s6true", + "Promise.all": "1,2", + "await": "12", + "Promise.race then": "6 2", + "Object.keys entries assign": 'a,b|a,1|{"c":3}', + "text methods": "xbbcab-ab", +}; + +type Case = { + /** What the test runs between the rows to replace the members. Each replacement lists what undoes it. */ + replaces: string; + /** The rows that call a replaced member in plain Bun: how many calls, and of which members. Every other row makes none. */ + calls: Record; + /** The rows that answer something else while the members are replaced. */ + answers?: Record; +}; + +const cases: Record = { + "object-and-function-tostring": { + replaces: ` const original1 = Object.prototype.toString; Object.prototype.toString = function (...operands) { seen[seen.length] = "Object.toString"; return original1.apply(this, operands); }; undo[undo.length] = () => { Object.prototype.toString = original1; }; const original2 = Function.prototype.toString; Function.prototype.toString = function (...operands) { seen[seen.length] = "Function.toString"; return original2.apply(this, operands); }; undo[undo.length] = () => { Function.prototype.toString = original2; };`, - "object-valueof": ` + calls: { + "objects joined": "2 Object.toString", + "functions joined": "1 Function.toString", + "objects sorted": "2 Object.toString", + "String(map)": "2 Object.toString", + "String(function)": "1 Function.toString", + "String(record)": "3 Object.toString", + "Object.prototype.toString.call": "3 Object.toString", + "record to a number": "1 Object.toString", + }, + }, + "object-valueof": { + replaces: ` const original3 = Object.prototype.valueOf; Object.prototype.valueOf = function (...operands) { seen[seen.length] = "Object.valueOf"; return original3.apply(this, operands); }; undo[undo.length] = () => { Object.prototype.valueOf = original3; };`, - "array-join": ` + calls: { + "list + text": "1 Object.valueOf", + "list to a number": "5 Object.valueOf", + "String(record)": "1 Object.valueOf", + "record to a number": "2 Object.valueOf", + }, + }, + "array-join": { + replaces: ` const original4 = Array.prototype.join; Array.prototype.join = function (...operands) { seen[seen.length] = "Array.join"; return original4.apply(this, operands); }; undo[undo.length] = () => { Array.prototype.join = original4; };`, - "array-tostring": ` + calls: { + "numbers joined": "1 Array.join", + "nested lists joined": "4 Array.join", + "String(list)": "1 Array.join", + "list in a template": "1 Array.join", + "list + text": "1 Array.join", + "list.toString()": "1 Array.join", + "holes joined": "2 Array.join", + "objects joined": "1 Array.join", + "functions joined": "1 Array.join", + "numbers sorted": "1 Array.join", + "texts sorted": "1 Array.join", + "objects sorted": "1 Array.join", + "sorted by a function": "1 Array.join", + "slice": "1 Array.join", + "map": "1 Array.join", + "filter": "1 Array.join", + "concat": "2 Array.join", + "flat": "2 Array.join", + "reverse splice": "2 Array.join", + "push pop shift unshift": "1 Array.join", + "spread of a list": "1 Array.join", + "Array.from": "2 Array.join", + "list to a number": "5 Array.join", + "spread of bytes": "1 Array.join", + "spread of a map": "3 Array.join", + "spread of a set": "1 Array.join", + "split by a pattern": "1 Array.join", + "Promise.all": "1 Array.join", + "Object.keys entries assign": "3 Array.join", + "text methods": "1 Array.join", + }, + }, + "array-tostring": { + replaces: ` const original5 = Array.prototype.toString; Array.prototype.toString = function (...operands) { seen[seen.length] = "Array.toString"; return original5.apply(this, operands); }; undo[undo.length] = () => { Array.prototype.toString = original5; };`, - "names-opened-on-object-prototype": ` + calls: { + "nested lists joined": "3 Array.toString", + "String(list)": "1 Array.toString", + "list in a template": "1 Array.toString", + "list + text": "1 Array.toString", + "list.toString()": "1 Array.toString", + "objects sorted": "1 Array.toString", + "concat": "1 Array.toString", + "flat": "1 Array.toString", + "list to a number": "5 Array.toString", + "spread of a map": "1 Array.toString", + "Object.keys entries assign": "1 Array.toString", + }, + }, + "names-opened-on-object-prototype": { + replaces: ` Object.prototype.format = function () { seen[seen.length] = "format"; return "formatted"; }; undo[undo.length] = () => { delete Object.prototype.format; }; Object.prototype.toJSON = function () { seen[seen.length] = "toJSON"; return "json"; }; undo[undo.length] = () => { delete Object.prototype.toJSON; };`, - "promise-then": ` + calls: { + "list stringified": "1 toJSON", + "record stringified": "1 toJSON", + "Object.keys entries assign": "1 toJSON", + }, + answers: { + "list stringified": '"json"', + "record stringified": '"json"', + "Object.keys entries assign": 'a,b|a,1|"json"', + }, + }, + "promise-then": { + replaces: ` const original16 = Promise.prototype.then; Promise.prototype.then = function (...operands) { seen[seen.length] = "Promise.then"; return original16.apply(this, operands); }; undo[undo.length] = () => { Promise.prototype.then = original16; };`, - "date-and-error-tostring": ` + calls: { + "Promise.all": "2 Promise.then", + "Promise.race then": "2 Promise.then", + }, + }, + "date-and-error-tostring": { + replaces: ` const original26 = Date.prototype.toString; Date.prototype.toString = function (...operands) { seen[seen.length] = "Date.toString"; return original26.apply(this, operands); }; undo[undo.length] = () => { Date.prototype.toString = original26; }; @@ -116,18 +275,43 @@ const replacements: Record = { const original29 = Date.prototype.valueOf; Date.prototype.valueOf = function (...operands) { seen[seen.length] = "Date.valueOf"; return original29.apply(this, operands); }; undo[undo.length] = () => { Date.prototype.valueOf = original29; };`, - "regexp-exec": ` + calls: { + "record stringified": "2 Date.toJSON,Date.valueOf", + "String(date)": "1 Date.toString", + "date to a number": "3 Date.valueOf", + "String(error)": "2 Error.toString", + }, + }, + "regexp-exec": { + replaces: ` const original10 = RegExp.prototype.exec; RegExp.prototype.exec = function (...operands) { seen[seen.length] = "RegExp.exec"; return original10.apply(this, operands); }; undo[undo.length] = () => { RegExp.prototype.exec = original10; };`, - "map-set-and-set-add": ` + calls: { + "replace by a pattern": "6 RegExp.exec", + "match search test": "3 RegExp.exec", + "split by a pattern": "5 RegExp.exec", + "matchAll": "3 RegExp.exec", + }, + }, + "map-set-and-set-add": { + replaces: ` const original11 = Map.prototype.set; Map.prototype.set = function (...operands) { seen[seen.length] = "Map.set"; return original11.apply(this, operands); }; undo[undo.length] = () => { Map.prototype.set = original11; }; const original12 = Set.prototype.add; Set.prototype.add = function (...operands) { seen[seen.length] = "Set.add"; return original12.apply(this, operands); }; undo[undo.length] = () => { Set.prototype.add = original12; };`, - "statics-and-globals": ` + calls: { + "a map of pairs": "4 Map.set", + "spread of a map": "2 Map.set", + "a set of a list": "5 Set.add", + "spread of a set": "2 Set.add", + "map get set has": "1 Map.set", + }, + }, + "statics-and-globals": { + replaces: ` const original21 = Date.now; Date.now = function (...operands) { seen[seen.length] = "Date.now"; return original21.apply(this, operands); }; undo[undo.length] = () => { Date.now = original21; }; @@ -143,16 +327,37 @@ const replacements: Record = { const original25 = globalThis.setTimeout; globalThis.setTimeout = function (...operands) { seen[seen.length] = "setTimeout"; return original25.apply(this, operands); }; undo[undo.length] = () => { globalThis.setTimeout = original25; };`, + calls: { + "Object.keys entries assign": "1 Object.keys", + }, + }, }; -for (const [name, replacement] of Object.entries(replacements)) { +// A word that changes nothing about a run: with it the run says on stderr how it is built. +const saysItsForm = "TestRunSaysItsForm"; + +// What a run prints behind the line that names the version: the rows of the module of the project and those of the test file, before, while and after the members are replaced. +function printedBy({ calls, answers: answersWhileReplaced = {} }: Omit) { + return ["before", "replaced", "restored"].flatMap(when => + ["src", "test file"].flatMap(where => + Object.entries(answers).map(([row, answer]) => + when === "replaced" + ? `replaced ${where} | ${row} | ${answersWhileReplaced[row] ?? answer} | ${calls[row] ?? "0 "}` + : `${when} ${where} | ${row} | ${answer} | 0 `, + ), + ), + ); +} + +for (const [name, { replaces, ...stated }] of Object.entries(cases)) { test.concurrent(`built-in functions on what the program makes, where tests replace ${name}`, async () => { - using dir = tempDir("tests-replace-" + name, { - "package.json": `{ "name": "p", "type": "module" }`, - "tsconfig.json": "{}", - "src/log.js": "export const seen = [];\n", - "src/uses.js": `import { seen } from "./log.js";\nexport async function uses(where) {${rows}}\n`, - "a.test.js": ` + const project = (print: string) => + tempDir("tests-replace-" + name, { + "package.json": `{ "name": "p", "type": "module" }`, + "tsconfig.json": "{}", + "src/log.js": "export const seen = [];\n", + "src/uses.js": `import { seen } from "./log.js";\nexport async function uses(where) {${rows}}\n`, + "a.test.js": ` import { test } from "bun:test"; import { seen } from "./src/log.js"; import { uses } from "./src/uses.js"; @@ -160,28 +365,37 @@ for (const [name, replacement] of Object.entries(replacements)) { const undo = []; test("a", async () => { const lines = [...await uses("before src"), ...await usesHere("before test file")]; - ${replacement} + ${replaces} const replaced = [...await uses("replaced src"), ...await usesHere("replaced test file")]; while (undo.length) undo.pop()(); lines.push(...replaced, ...await uses("restored src"), ...await usesHere("restored test file")); - process.stdout.write(lines.join("\\n") + "\\n"); + ${print} }, 60000);`, - "z.test.js": `import { test } from "bun:test";\ntest("z", () => {});\n`, - }); - const seen = async (env: Record, ...flags: string[]) => { - const { stdout, stderr, exitCode } = await run([bunExe(), "test", "--isolate", ...flags], String(dir), { - ...env, - TZ: "UTC", + "z.test.js": `import { test } from "bun:test";\ntest("z", () => {});\n`, }); + using dir = project(`process.stdout.write(lines.join("\\n") + "\\n");`); + // A program that reads process.stdout is built with the modules of Bun's own code behind that stream, which a lowest layer holds anyway. For the run as one + // program that is most of the build, so that run has a copy of the project which prints through console.log. + using dirForOneProgram = project(`console.log(lines.join("\\n"));`); + const ran = async (cwd: string, words?: string, ...flags: string[]) => { + const env = words ? { ...bunEnv, TZ: "UTC", BUN_DEBUG_TEST_AOT_WORDS: words } : { ...bunEnv, TZ: "UTC" }; + const { stdout, stderr, exitCode } = await run([bunExe(), "test", "--isolate", ...flags], cwd, env); + // A crash prints only there. if (exitCode !== 0) console.error(stderr); - return { exitCode, lines: stdout.split("\n").filter(line => line.includes(" | ")) }; + // run() asks a run over layers for its layers. The other compiled run is one program. + if (words === saysItsForm) expect(stderr).toMatch(/^AOT test run \| one program/m); + return { + printed: stdout.split("\n").slice(1), + results: [...stderr.matchAll(/^\((pass|fail|skip)\) (.*?)(?: \[.*\])?$/gm)].map( + match => `${match[1]} ${match[2]}`, + ), + exitCode, + }; }; - const plain = await seen(bunEnv); - expect(plain.exitCode).toBe(0); - expect(plain.lines.length).toBe(348); - expect(await seen(bunEnv, "--aot")).toEqual(plain); - expect(await seen({ ...bunEnv, BUN_DEBUG_TEST_AOT_WORDS: "TestRunLayers:2,TestRunPartFiles:1" }, "--aot")).toEqual( - plain, - ); + const asPlainBun = { printed: [...printedBy(stated), ""], results: ["pass a", "pass z"], exitCode: 0 }; + expect(await ran(String(dir))).toEqual(asPlainBun); + expect(await ran(String(dirForOneProgram))).toEqual(asPlainBun); + expect(await ran(String(dirForOneProgram), saysItsForm, "--aot")).toEqual(asPlainBun); + expect(await ran(String(dir), "TestRunLayers:2,TestRunPartFiles:1", "--aot")).toEqual(asPlainBun); }); }