diff options
Diffstat (limited to 'test/js')
102 files changed, 3372 insertions, 506 deletions
diff --git a/test/js/bun/eventsource/eventsource.test.ts b/test/js/bun/eventsource/eventsource.test.ts index 2f5b7d755..d4da99aa3 100644 --- a/test/js/bun/eventsource/eventsource.test.ts +++ b/test/js/bun/eventsource/eventsource.test.ts @@ -1,153 +1,153 @@ -function sse(req: Request) { - const signal = req.signal; - return new Response( - new ReadableStream({ - type: "direct", - async pull(controller) { - while (!signal.aborted) { - await controller.write(`data:Hello, World!\n\n`); - await controller.write(`event: bun\ndata: Hello, World!\n\n`); - await controller.write(`event: lines\ndata: Line 1!\ndata: Line 2!\n\n`); - await controller.write(`event: id_test\nid:1\n\n`); - await controller.flush(); - await Bun.sleep(100); - } - controller.close(); - }, - }), - { status: 200, headers: { "Content-Type": "text/event-stream" } }, - ); -} +// function sse(req: Request) { +// const signal = req.signal; +// return new Response( +// new ReadableStream({ +// type: "direct", +// async pull(controller) { +// while (!signal.aborted) { +// await controller.write(`data:Hello, World!\n\n`); +// await controller.write(`event: bun\ndata: Hello, World!\n\n`); +// await controller.write(`event: lines\ndata: Line 1!\ndata: Line 2!\n\n`); +// await controller.write(`event: id_test\nid:1\n\n`); +// await controller.flush(); +// await Bun.sleep(100); +// } +// controller.close(); +// }, +// }), +// { status: 200, headers: { "Content-Type": "text/event-stream" } }, +// ); +// } -function sse_unstable(req: Request) { - const signal = req.signal; - let id = parseInt(req.headers.get("last-event-id") || "0", 10); +// function sse_unstable(req: Request) { +// const signal = req.signal; +// let id = parseInt(req.headers.get("last-event-id") || "0", 10); - return new Response( - new ReadableStream({ - type: "direct", - async pull(controller) { - if (!signal.aborted) { - await controller.write(`id:${++id}\ndata: Hello, World!\nretry:100\n\n`); - await controller.flush(); - } - controller.close(); - }, - }), - { status: 200, headers: { "Content-Type": "text/event-stream" } }, - ); -} +// return new Response( +// new ReadableStream({ +// type: "direct", +// async pull(controller) { +// if (!signal.aborted) { +// await controller.write(`id:${++id}\ndata: Hello, World!\nretry:100\n\n`); +// await controller.flush(); +// } +// controller.close(); +// }, +// }), +// { status: 200, headers: { "Content-Type": "text/event-stream" } }, +// ); +// } -function sseServer( - done: (err?: unknown) => void, - pathname: string, - callback: (evtSource: EventSource, done: (err?: unknown) => void) => void, -) { - const server = Bun.serve({ - port: 0, - fetch(req) { - if (new URL(req.url).pathname === "/stream") { - return sse(req); - } - if (new URL(req.url).pathname === "/unstable") { - return sse_unstable(req); - } - return new Response("Hello, World!"); - }, - }); - let evtSource: EventSource | undefined; - try { - evtSource = new EventSource(`http://localhost:${server.port}${pathname}`); - callback(evtSource, err => { - try { - done(err); - evtSource?.close(); - } catch (err) { - done(err); - } finally { - server.stop(true); - } - }); - } catch (err) { - evtSource?.close(); - server.stop(true); - done(err); - } -} +// function sseServer( +// done: (err?: unknown) => void, +// pathname: string, +// callback: (evtSource: EventSource, done: (err?: unknown) => void) => void, +// ) { +// const server = Bun.serve({ +// port: 0, +// fetch(req) { +// if (new URL(req.url).pathname === "/stream") { +// return sse(req); +// } +// if (new URL(req.url).pathname === "/unstable") { +// return sse_unstable(req); +// } +// return new Response("Hello, World!"); +// }, +// }); +// let evtSource: EventSource | undefined; +// try { +// evtSource = new EventSource(`http://localhost:${server.port}${pathname}`); +// callback(evtSource, err => { +// try { +// done(err); +// evtSource?.close(); +// } catch (err) { +// done(err); +// } finally { +// server.stop(true); +// } +// }); +// } catch (err) { +// evtSource?.close(); +// server.stop(true); +// done(err); +// } +// } -import { describe, expect, it } from "bun:test"; +// import { describe, expect, it } from "bun:test"; -describe("events", () => { - it("should call open", done => { - sseServer(done, "/stream", (evtSource, done) => { - evtSource.onopen = () => { - done(); - }; - evtSource.onerror = err => { - done(err); - }; - }); - }); +// describe("events", () => { +// it("should call open", done => { +// sseServer(done, "/stream", (evtSource, done) => { +// evtSource.onopen = () => { +// done(); +// }; +// evtSource.onerror = err => { +// done(err); +// }; +// }); +// }); - it("should call message", done => { - sseServer(done, "/stream", (evtSource, done) => { - evtSource.onmessage = e => { - expect(e.data).toBe("Hello, World!"); - done(); - }; - }); - }); +// it("should call message", done => { +// sseServer(done, "/stream", (evtSource, done) => { +// evtSource.onmessage = e => { +// expect(e.data).toBe("Hello, World!"); +// done(); +// }; +// }); +// }); - it("should call custom event", done => { - sseServer(done, "/stream", (evtSource, done) => { - evtSource.addEventListener("bun", e => { - expect(e.data).toBe("Hello, World!"); - done(); - }); - }); - }); +// it("should call custom event", done => { +// sseServer(done, "/stream", (evtSource, done) => { +// evtSource.addEventListener("bun", e => { +// expect(e.data).toBe("Hello, World!"); +// done(); +// }); +// }); +// }); - it("should call event with multiple lines", done => { - sseServer(done, "/stream", (evtSource, done) => { - evtSource.addEventListener("lines", e => { - expect(e.data).toBe("Line 1!\nLine 2!"); - done(); - }); - }); - }); +// it("should call event with multiple lines", done => { +// sseServer(done, "/stream", (evtSource, done) => { +// evtSource.addEventListener("lines", e => { +// expect(e.data).toBe("Line 1!\nLine 2!"); +// done(); +// }); +// }); +// }); - it("should receive id", done => { - sseServer(done, "/stream", (evtSource, done) => { - evtSource.addEventListener("id_test", e => { - expect(e.lastEventId).toBe("1"); - done(); - }); - }); - }); +// it("should receive id", done => { +// sseServer(done, "/stream", (evtSource, done) => { +// evtSource.addEventListener("id_test", e => { +// expect(e.lastEventId).toBe("1"); +// done(); +// }); +// }); +// }); - it("should reconnect with id", done => { - sseServer(done, "/unstable", (evtSource, done) => { - const ids: string[] = []; - evtSource.onmessage = e => { - ids.push(e.lastEventId); - if (ids.length === 2) { - for (let i = 0; i < 2; i++) { - expect(ids[i]).toBe((i + 1).toString()); - } - done(); - } - }; - }); - }); +// it("should reconnect with id", done => { +// sseServer(done, "/unstable", (evtSource, done) => { +// const ids: string[] = []; +// evtSource.onmessage = e => { +// ids.push(e.lastEventId); +// if (ids.length === 2) { +// for (let i = 0; i < 2; i++) { +// expect(ids[i]).toBe((i + 1).toString()); +// } +// done(); +// } +// }; +// }); +// }); - it("should call error", done => { - sseServer(done, "/", (evtSource, done) => { - evtSource.onerror = e => { - expect(e.error.message).toBe( - `EventSource's response has a MIME type that is not "text/event-stream". Aborting the connection.`, - ); - done(); - }; - }); - }); -}); +// it("should call error", done => { +// sseServer(done, "/", (evtSource, done) => { +// evtSource.onerror = e => { +// expect(e.error.message).toBe( +// `EventSource's response has a MIME type that is not "text/event-stream". Aborting the connection.`, +// ); +// done(); +// }; +// }); +// }); +// }); diff --git a/test/js/bun/http/error-response.js b/test/js/bun/http/error-response.js new file mode 100644 index 000000000..3284c146b --- /dev/null +++ b/test/js/bun/http/error-response.js @@ -0,0 +1,8 @@ +const s = Bun.serve({ + fetch(req, res) { + s.stop(true); + throw new Error("1"); + }, + port: 0, +}); +fetch(`http://${s.hostname}:${s.port}`).then(res => console.log(res.status)); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index 7182ba68d..bba35c085 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -2,8 +2,10 @@ import { file, gc, Serve, serve, Server } from "bun"; import { afterEach, describe, it, expect, afterAll } from "bun:test"; import { readFileSync, writeFileSync } from "fs"; import { resolve } from "path"; +import { bunExe, bunEnv } from "harness"; import { renderToReadableStream } from "react-dom/server"; import app_jsx from "./app.jsx"; +import { spawn } from "child_process"; type Handler = (req: Request) => Response; afterEach(() => gc(true)); @@ -980,6 +982,19 @@ describe("should support Content-Range with Bun.file()", () => { } }); +it("formats error responses correctly", async () => { + const c = spawn(bunExe(), ["./error-response.js"], { cwd: import.meta.dir, env: bunEnv }); + + var output = ""; + c.stderr.on("data", chunk => { + output += chunk.toString(); + }); + c.stderr.on("end", () => { + expect(output).toContain('throw new Error("1");'); + c.kill(); + }); +}); + it("request body and signal life cycle", async () => { { const headers = { diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index 1da000834..5126067e6 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -105,7 +105,28 @@ it("should reject on connection error, calling both connectError() and rejecting }); it("should not leak memory when connect() fails", async () => { - await expectMaxObjectTypeCount(expect, "TCPSocket", 1, 100); + await (async () => { + var promises = new Array(100); + for (let i = 0; i < 100; i++) { + promises[i] = connect({ + hostname: "localhost", + port: 55555, + socket: { + connectError(socket, error) {}, + data() {}, + drain() {}, + close() {}, + end() {}, + error() {}, + open() {}, + }, + }); + } + await Promise.allSettled(promises); + promises.length = 0; + })(); + + await expectMaxObjectTypeCount(expect, "TCPSocket", 50, 100); }); // this also tests we mark the promise as handled if connectError() is called diff --git a/test/js/bun/resolve/esModule-annotation.test.js b/test/js/bun/resolve/esModule-annotation.test.js new file mode 100644 index 000000000..33c84be5d --- /dev/null +++ b/test/js/bun/resolve/esModule-annotation.test.js @@ -0,0 +1,68 @@ +import { test, expect, describe } from "bun:test"; +import * as WithTypeModuleExportEsModuleAnnotationMissingDefault from "./with-type-module/export-esModule-annotation-empty.cjs"; +import * as WithTypeModuleExportEsModuleAnnotationNoDefault from "./with-type-module/export-esModule-annotation-no-default.cjs"; +import * as WithTypeModuleExportEsModuleAnnotation from "./with-type-module/export-esModule-annotation.cjs"; +import * as WithTypeModuleExportEsModuleNoAnnotation from "./with-type-module/export-esModule-no-annotation.cjs"; +import * as WithoutTypeModuleExportEsModuleAnnotationMissingDefault from "./without-type-module/export-esModule-annotation-empty.cjs"; +import * as WithoutTypeModuleExportEsModuleAnnotationNoDefault from "./without-type-module/export-esModule-annotation-no-default.cjs"; +import * as WithoutTypeModuleExportEsModuleAnnotation from "./without-type-module/export-esModule-annotation.cjs"; +import * as WithoutTypeModuleExportEsModuleNoAnnotation from "./without-type-module/export-esModule-no-annotation.cjs"; + +describe('without type: "module"', () => { + test("module.exports = {}", () => { + expect(WithoutTypeModuleExportEsModuleAnnotationMissingDefault.default).toEqual({}); + expect(WithoutTypeModuleExportEsModuleAnnotationMissingDefault.__esModule).toBeUndefined(); + }); + + test("exports.__esModule = true", () => { + expect(WithoutTypeModuleExportEsModuleAnnotationNoDefault.default).toEqual({ + __esModule: true, + }); + + // The module namespace object will not have the __esModule property. + expect(WithoutTypeModuleExportEsModuleAnnotationNoDefault).not.toHaveProperty("__esModule"); + }); + + test("exports.default = true; exports.__esModule = true;", () => { + expect(WithoutTypeModuleExportEsModuleAnnotation.default).toBeTrue(); + expect(WithoutTypeModuleExportEsModuleAnnotation.__esModule).toBeUndefined(); + }); + + test("exports.default = true;", () => { + expect(WithoutTypeModuleExportEsModuleNoAnnotation.default).toEqual({ + default: true, + }); + expect(WithoutTypeModuleExportEsModuleAnnotation.__esModule).toBeUndefined(); + }); +}); + +describe('with type: "module"', () => { + test("module.exports = {}", () => { + expect(WithTypeModuleExportEsModuleAnnotationMissingDefault.default).toEqual({}); + expect(WithTypeModuleExportEsModuleAnnotationMissingDefault.__esModule).toBeUndefined(); + }); + + test("exports.__esModule = true", () => { + expect(WithTypeModuleExportEsModuleAnnotationNoDefault.default).toEqual({ + __esModule: true, + }); + + // The module namespace object WILL have the __esModule property. + expect(WithTypeModuleExportEsModuleAnnotationNoDefault).toHaveProperty("__esModule"); + }); + + test("exports.default = true; exports.__esModule = true;", () => { + expect(WithTypeModuleExportEsModuleAnnotation.default).toEqual({ + default: true, + __esModule: true, + }); + expect(WithTypeModuleExportEsModuleAnnotation.__esModule).toBeTrue(); + }); + + test("exports.default = true;", () => { + expect(WithTypeModuleExportEsModuleNoAnnotation.default).toEqual({ + default: true, + }); + expect(WithTypeModuleExportEsModuleAnnotation.__esModule).toBeTrue(); + }); +}); diff --git a/test/js/bun/resolve/import-meta.test.js b/test/js/bun/resolve/import-meta.test.js index 5771aeb30..e23b44446 100644 --- a/test/js/bun/resolve/import-meta.test.js +++ b/test/js/bun/resolve/import-meta.test.js @@ -9,7 +9,7 @@ import sync from "./require-json.json"; const { path, dir } = import.meta; it("primordials are not here!", () => { - expect(import.meta.primordials === undefined).toBe(true); + expect(globalThis[Symbol.for("Bun.lazy")]("primordials") === undefined).toBe(true); }); it("import.meta.main", () => { @@ -25,9 +25,14 @@ it("import.meta.main", () => { it("import.meta.resolveSync", () => { expect(import.meta.resolveSync("./" + import.meta.file, import.meta.path)).toBe(path); +}); + +it("Module.createRequire", () => { const require = Module.createRequire(import.meta.path); expect(require.resolve(import.meta.path)).toBe(path); expect(require.resolve("./" + import.meta.file)).toBe(path); + const { resolve } = require; + expect(resolve("./" + import.meta.file)).toBe(path); // check it works with URL objects expect(Module.createRequire(new URL(import.meta.url)).resolve(import.meta.path)).toBe(import.meta.path); @@ -68,8 +73,11 @@ it("import.meta.require (json)", () => { }); it("const f = require;require(json)", () => { + function capture(f) { + return f.length; + } const f = require; - console.log(f); + capture(f); expect(f("./require-json.json").hello).toBe(sync.hello); }); @@ -82,12 +90,6 @@ it("Module.createRequire().resolve", () => { expect(result).toBe(expected); }); -// this is stubbed out -it("Module._nodeModulePaths()", () => { - const expected = Module._nodeModulePaths(); - expect(!!expected).toBe(true); -}); - // this isn't used in bun but exists anyway // we just want it to not be undefined it("Module._cache", () => { @@ -95,9 +97,9 @@ it("Module._cache", () => { expect(!!expected).toBe(true); }); -it("Module._resolveFileName()", () => { +it("Module._resolveFilename()", () => { const expected = Bun.resolveSync(import.meta.path, "/"); - const result = Module._resolveFileName(import.meta.path, "/", true); + const result = Module._resolveFilename(import.meta.path, "/", true); expect(result).toBe(expected); }); @@ -107,7 +109,6 @@ it("Module.createRequire(file://url).resolve(file://url)", () => { const createdRequire = Module.createRequire(import.meta.url); const result1 = createdRequire.resolve("./require-json.json"); const result2 = createdRequire.resolve("file://./require-json.json"); - expect(result1).toBe(expected); expect(result2).toBe(expected); }); diff --git a/test/js/bun/resolve/with-type-module/export-esModule-annotation-empty.cjs b/test/js/bun/resolve/with-type-module/export-esModule-annotation-empty.cjs new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/test/js/bun/resolve/with-type-module/export-esModule-annotation-empty.cjs @@ -0,0 +1 @@ +module.exports = {}; diff --git a/test/js/bun/resolve/with-type-module/export-esModule-annotation-no-default.cjs b/test/js/bun/resolve/with-type-module/export-esModule-annotation-no-default.cjs new file mode 100644 index 000000000..32b83d4a5 --- /dev/null +++ b/test/js/bun/resolve/with-type-module/export-esModule-annotation-no-default.cjs @@ -0,0 +1 @@ +exports.__esModule = true; diff --git a/test/js/bun/resolve/with-type-module/export-esModule-annotation.cjs b/test/js/bun/resolve/with-type-module/export-esModule-annotation.cjs new file mode 100644 index 000000000..bc0625a0c --- /dev/null +++ b/test/js/bun/resolve/with-type-module/export-esModule-annotation.cjs @@ -0,0 +1,2 @@ +exports.default = true; +exports.__esModule = true; diff --git a/test/js/bun/resolve/with-type-module/export-esModule-no-annotation.cjs b/test/js/bun/resolve/with-type-module/export-esModule-no-annotation.cjs new file mode 100644 index 000000000..a4b65815f --- /dev/null +++ b/test/js/bun/resolve/with-type-module/export-esModule-no-annotation.cjs @@ -0,0 +1 @@ +exports.default = true; diff --git a/test/js/bun/resolve/with-type-module/package.json b/test/js/bun/resolve/with-type-module/package.json new file mode 100644 index 000000000..f1863a426 --- /dev/null +++ b/test/js/bun/resolve/with-type-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "with-type-module", + "type": "module" +} diff --git a/test/js/bun/resolve/without-type-module/export-esModule-annotation-empty.cjs b/test/js/bun/resolve/without-type-module/export-esModule-annotation-empty.cjs new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/test/js/bun/resolve/without-type-module/export-esModule-annotation-empty.cjs @@ -0,0 +1 @@ +module.exports = {}; diff --git a/test/js/bun/resolve/without-type-module/export-esModule-annotation-no-default.cjs b/test/js/bun/resolve/without-type-module/export-esModule-annotation-no-default.cjs new file mode 100644 index 000000000..32b83d4a5 --- /dev/null +++ b/test/js/bun/resolve/without-type-module/export-esModule-annotation-no-default.cjs @@ -0,0 +1 @@ +exports.__esModule = true; diff --git a/test/js/bun/resolve/without-type-module/export-esModule-annotation.cjs b/test/js/bun/resolve/without-type-module/export-esModule-annotation.cjs new file mode 100644 index 000000000..bc0625a0c --- /dev/null +++ b/test/js/bun/resolve/without-type-module/export-esModule-annotation.cjs @@ -0,0 +1,2 @@ +exports.default = true; +exports.__esModule = true; diff --git a/test/js/bun/resolve/without-type-module/export-esModule-no-annotation.cjs b/test/js/bun/resolve/without-type-module/export-esModule-no-annotation.cjs new file mode 100644 index 000000000..a4b65815f --- /dev/null +++ b/test/js/bun/resolve/without-type-module/export-esModule-no-annotation.cjs @@ -0,0 +1 @@ +exports.default = true; diff --git a/test/js/bun/resolve/without-type-module/package.json b/test/js/bun/resolve/without-type-module/package.json new file mode 100644 index 000000000..5b290db1c --- /dev/null +++ b/test/js/bun/resolve/without-type-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "without-type-module", + "type": "commonjs" +} diff --git a/test/js/bun/spawn/spawn-streaming-stdin.test.ts b/test/js/bun/spawn/spawn-streaming-stdin.test.ts index 27efa14ec..0c430b680 100644 --- a/test/js/bun/spawn/spawn-streaming-stdin.test.ts +++ b/test/js/bun/spawn/spawn-streaming-stdin.test.ts @@ -2,18 +2,22 @@ import { it, test, expect } from "bun:test"; import { spawn } from "bun"; import { bunExe, bunEnv, gcTick } from "harness"; import { closeSync, openSync } from "fs"; +import { tmpdir } from "node:os"; +import { join } from "path"; +import { unlinkSync } from "node:fs"; const N = 100; test("spawn can write to stdin multiple chunks", async () => { const maxFD = openSync("/dev/null", "w"); for (let i = 0; i < N; i++) { + const tmperr = join(tmpdir(), "stdin-repro-error.log." + i); var exited; await (async function () { const proc = spawn({ cmd: [bunExe(), import.meta.dir + "/stdin-repro.js"], stdout: "pipe", stdin: "pipe", - stderr: Bun.file("/tmp/out.log"), + stderr: Bun.file(tmperr), env: bunEnv, }); exited = proc.exited; @@ -45,6 +49,10 @@ test("spawn can write to stdin multiple chunks", async () => { await Promise.all([prom, prom2]); expect(Buffer.concat(chunks).toString().trim()).toBe("Wrote to stdin!\n".repeat(4).trim()); await proc.exited; + + try { + unlinkSync(tmperr); + } catch (e) {} })(); } @@ -54,4 +62,4 @@ test("spawn can write to stdin multiple chunks", async () => { // assert we didn't leak any file descriptors expect(newMaxFD).toBe(maxFD); -}); +}, 10_000); diff --git a/test/js/bun/sqlite/sqlite.test.js b/test/js/bun/sqlite/sqlite.test.js index faa7d5015..e4725cac2 100644 --- a/test/js/bun/sqlite/sqlite.test.js +++ b/test/js/bun/sqlite/sqlite.test.js @@ -513,6 +513,24 @@ it("latin1 supplement chars", () => { expect(db.query("SELECT * FROM foo WHERE id > 9999").values()).toEqual([]); }); +it("supports FTS5", () => { + const db = new Database(); + db.run("CREATE VIRTUAL TABLE movies USING fts5(title, tokenize='trigram')"); + const insert = db.prepare("INSERT INTO movies VALUES ($title)"); + const insertMovies = db.transaction(movies => { + for (const movie of movies) insert.run(movie); + }); + insertMovies([ + { $title: "The Shawshank Redemption" }, + { $title: "WarGames" }, + { $title: "Interstellar" }, + { $title: "Se7en" }, + { $title: "City of God" }, + { $title: "Spirited Away" }, + ]); + expect(db.query("SELECT * FROM movies('game')").all()).toEqual([{ title: "WarGames" }]); +}); + describe("Database.run", () => { it("should not throw error `not an error` when provided query containing only whitespace", () => { const db = Database.open(":memory:"); diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index f09f7d196..ed94b7e9a 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -6,6 +6,28 @@ var { isBun, test, describe, expect, jest, vi, mock, bunTest, spyOn } = require("./test-interop.js")(); describe("expect()", () => { + test("rejects", async () => { + await expect(Promise.reject(1)).rejects.toBe(1); + + // Different task + await expect( + new Promise((_, reject) => { + setTimeout(() => reject(1), 0); + }), + ).rejects.toBe(1); + }); + + test("resolves", async () => { + await expect(Promise.resolve(1)).resolves.toBe(1); + + // Different task + await expect( + new Promise(resolve => { + setTimeout(() => resolve(1), 0); + }), + ).resolves.toBe(1); + }); + test("can call without an argument", () => { expect().toBe(undefined); }); @@ -1313,6 +1335,62 @@ describe("expect()", () => { expect([1, 2, 3, 4]).not.toEqual([1, 2, 3]); }); + test("toEqual() - private class fields", () => { + class A { + #three = 3; + set three(value) { + this.#three = value; + } + + get three() { + return this.#three; + } + } + + class B { + #three = 3; + set three(value) { + this.#three = value; + } + + get three() { + return this.#three; + } + } + + let a1 = new A(); + let a2 = new A(); + a1.three = 4; + expect(a1).toEqual(a2); + expect(a2).toEqual(a1); + + let a3 = new A(); + let a4 = new A(); + a3.three = 4; + // use indexed properties for slow path + a3[1] = 2; + a4[1] = 2; + expect(a3).toEqual(a4); + expect(a4).toEqual(a3); + + let b1 = new B(); + let a5 = new A(); + expect(b1).toEqual(a5); + expect(a5).toEqual(b1); + + b1.three = 4; + expect(b1).toEqual(a5); + expect(a5).toEqual(b1); + + b1[1] = 2; + expect(b1).not.toEqual(a5); + expect(a5).not.toEqual(b1); + + a5[1] = 2; + expect(b1).toEqual(a5); + expect(a5).toEqual(b1); + }); + test("properties with different circularity are not equal", () => { const a = {}; a.x = { y: a }; @@ -2654,28 +2732,74 @@ describe("expect()", () => { describe("toBeEmpty()", () => { const values = [ - "", - [], - {}, - new Set(), - new Map(), - new String(), - new Array(), - new Uint8Array(), - new Object(), - Buffer.from(""), - ...(isBun ? [Bun.file("/tmp/empty.txt")] : []), - new Headers(), - new URLSearchParams(), - new FormData(), - (function* () {})(), + { + label: `""`, + value: "", + }, + { + label: `[]`, + value: [], + }, + { + label: `{}`, + value: {}, + }, + { + label: `new Set()`, + value: new Set(), + }, + { + label: `new Map()`, + value: new Map(), + }, + { + label: `new String()`, + value: new String(), + }, + { + label: `new Array()`, + value: new Array(), + }, + { + label: `new Uint8Array()`, + value: new Uint8Array(), + }, + { + label: `new Object()`, + value: new Object(), + }, + { + label: `Buffer.from("")`, + value: Buffer.from(""), + }, + { + label: `new Headers()`, + value: new Headers(), + }, + { + label: `new URLSearchParams()`, + value: new URLSearchParams(), + }, + { + label: `new FormData()`, + value: new FormData(), + }, + { + label: `(function* () {})()`, + value: (function* () {})(), + }, ]; - for (const value of values) { - test(label(value), () => { - if (value && typeof value === "object" && value instanceof Blob) { + if (isBun) { + values.push({ + label: `Bun.file()`, + value: Bun.file("/tmp/empty.txt"), + }); + } + for (const { label, value } of values) { + test(label, () => { + if (value instanceof Blob) { require("fs").writeFileSync("/tmp/empty.txt", ""); } - expect(value).toBeEmpty(); }); } @@ -2683,34 +2807,81 @@ describe("expect()", () => { describe("not.toBeEmpty()", () => { const values = [ - " ", - [""], - [undefined], - { "": "" }, - new Set([""]), - new Map([["", ""]]), - new String(" "), - new Array(1), - new Uint8Array(1), - Buffer.from(" "), - ...(isBun ? [Bun.file(__filename)] : []), - new Headers({ - a: "b", - c: "d", - }), - new URL("https://example.com?d=e&f=g").searchParams, - (() => { - var a = new FormData(); - a.append("a", "b"); - a.append("c", "d"); - return a; - })(), - (function* () { - yield "123"; - })(), + { + label: `" "`, + value: " ", + }, + { + label: `[""]`, + value: [""], + }, + { + label: `[undefined]`, + value: [undefined], + }, + { + label: `{ "": "" }`, + value: { "": "" }, + }, + { + label: `new Set([""])`, + value: new Set([""]), + }, + { + label: `new Map([["", ""]])`, + value: new Map([["", ""]]), + }, + { + label: `new String(" ")`, + value: new String(" "), + }, + { + label: `new Array(1)`, + value: new Array(1), + }, + { + label: `new Uint8Array(1)`, + value: new Uint8Array(1), + }, + { + label: `Buffer.from(" ")`, + value: Buffer.from(" "), + }, + { + label: `new Headers({...})`, + value: new Headers({ + a: "b", + c: "d", + }), + }, + { + label: `URL.searchParams`, + value: new URL("https://example.com?d=e&f=g").searchParams, + }, + { + label: `FormData`, + value: (() => { + var a = new FormData(); + a.append("a", "b"); + a.append("c", "d"); + return a; + })(), + }, + { + label: `generator function`, + value: (function* () { + yield "123"; + })(), + }, ]; - for (const value of values) { - test(label(value), () => { + if (isBun) { + values.push({ + label: `Bun.file()`, + value: Bun.file(__filename), + }); + } + for (const { label, value } of values) { + test(label, () => { expect(value).not.toBeEmpty(); }); } @@ -2743,7 +2914,7 @@ describe("expect()", () => { expect([]).toBeArrayOfSize(0); expect(new Array()).toBeArrayOfSize(0); expect([1, 2, 3, "🫓"]).toBeArrayOfSize(4); - expect((new Array() < string) | (number > (1, 2, 3, "🫓"))).toBeArrayOfSize(4); + expect(new Array(1, 2, 3, "🫓")).toBeArrayOfSize(4); expect({}).not.toBeArrayOfSize(1); expect("").not.toBeArrayOfSize(1); expect(0).not.toBeArrayOfSize(1); diff --git a/test/js/bun/test/jest-hooks.test.ts b/test/js/bun/test/jest-hooks.test.ts index c99dc7759..618cdc4c6 100644 --- a/test/js/bun/test/jest-hooks.test.ts +++ b/test/js/bun/test/jest-hooks.test.ts @@ -1,5 +1,36 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +let hooks_run: string[] = []; + +beforeAll(() => hooks_run.push("global beforeAll")); +beforeEach(() => hooks_run.push("global beforeEach")); +afterAll(() => hooks_run.push("global afterAll")); +afterEach(() => hooks_run.push("global afterEach")); + +describe("describe scope", () => { + beforeAll(() => hooks_run.push("describe beforeAll")); + beforeEach(() => hooks_run.push("describe beforeEach")); + afterAll(() => hooks_run.push("describe afterAll")); + afterEach(() => hooks_run.push("describe afterEach")); + + it("should run after beforeAll/beforeEach in the correct order", () => { + expect(hooks_run).toEqual(["global beforeAll", "describe beforeAll", "global beforeEach", "describe beforeEach"]); + }); + + it("should run after afterEach/afterAll in the correct order", () => { + expect(hooks_run).toEqual([ + "global beforeAll", + "describe beforeAll", + "global beforeEach", + "describe beforeEach", + "describe afterEach", + "global afterEach", + "global beforeEach", + "describe beforeEach", + ]); + }); +}); + describe("test jest hooks in bun-test", () => { describe("test beforeAll hook", () => { let animal = "tiger"; diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index eac981fd1..ef3c4b7d3 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -2,7 +2,16 @@ * This file is meant to be runnable in both Jest and Bun. * `bunx jest mock-fn.test.js` */ -var { isBun, test, describe, expect, jest, vi, mock, bunTest, spyOn } = require("./test-interop.js")(); +var { isBun, expect, jest, vi, mock, spyOn } = require("./test-interop.js")(); + +// if you want to test vitest, comment the above and uncomment the below + +// import { expect, describe, test, vi } from "vitest"; +// const isBun = false; +// const jest = { fn: vi.fn, restoreAllMocks: vi.restoreAllMocks }; +// const spyOn = vi.spyOn; +// import * as extended from "jest-extended"; +// expect.extend(extended); async function expectResolves(promise) { expect(promise).toBeInstanceOf(Promise); @@ -434,7 +443,6 @@ describe("mock()", () => { return "3"; }, ); - expect(result).toBe(undefined); expect(fn()).toBe("1"); }); test("withImplementation (async)", async () => { @@ -595,5 +603,19 @@ describe("spyOn", () => { }); } + test("spyOn twice works", () => { + var obj = { + original() { + return 42; + }, + }; + const _original = obj.original; + const fn = spyOn(obj, "original"); + const fn2 = spyOn(obj, "original"); + expect(fn).toBe(obj.original); + expect(fn2).toBe(fn); + expect(fn).not.toBe(_original); + }); + // spyOn does not work with getters/setters yet. }); diff --git a/test/js/bun/test/test-interop.js b/test/js/bun/test/test-interop.js index 5c41082d6..4b2199ae9 100644 --- a/test/js/bun/test/test-interop.js +++ b/test/js/bun/test/test-interop.js @@ -20,6 +20,25 @@ module.exports = () => { vi: bunTest.vi, spyOn: bunTest.spyOn, }; + } else if (process.env.VITEST) { + const vi = require("vitest"); + + return { + isBun: false, + bunTest: null, + test: vi.test, + describe: vi.describe, + it: vi.it, + expect: vi.expect, + beforeEach: vi.beforeEach, + afterEach: vi.afterEach, + beforeAll: vi.beforeAll, + afterAll: vi.afterAll, + jest: { fn: vi.fn }, + mock: null, + vi, + spyOn: vi.spyOn, + }; } else { const globals = require("@jest/globals"); const extended = require("jest-extended"); diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index 7ecfdef11..5f732bb82 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -540,18 +540,18 @@ beforeEach: #2 beforeEach: TEST-FILE beforeEach: one describe scope -- inside one describe scope -- +afterEach: one describe scope +afterEach: TEST-FILE afterEach: #1 afterEach: #2 -afterEach: TEST-FILE -afterEach: one describe scope afterAll: one describe scope beforeEach: #1 beforeEach: #2 beforeEach: TEST-FILE -- the top-level test -- +afterEach: TEST-FILE afterEach: #1 afterEach: #2 -afterEach: TEST-FILE afterAll: TEST-FILE afterAll: #1 afterAll: #2 diff --git a/test/js/bun/test/test-timers.test.ts b/test/js/bun/test/test-timers.test.ts new file mode 100644 index 000000000..963467dee --- /dev/null +++ b/test/js/bun/test/test-timers.test.ts @@ -0,0 +1,28 @@ +test("we can go back in time", () => { + const DateBeforeMocked = Date; + const orig = new Date(); + orig.setHours(0, 0, 0, 0); + jest.useFakeTimers(); + jest.setSystemTime(new Date("1995-12-19T00:00:00.000Z")); + + expect(new Date().toISOString()).toBe("1995-12-19T00:00:00.000Z"); + expect(Date.now()).toBe(819331200000); + + if (typeof Bun !== "undefined") { + // In bun, the Date object remains the same despite being mocked. + // This prevents a whole bunch of subtle bugs in tests. + expect(DateBeforeMocked).toBe(Date); + expect(DateBeforeMocked.now).toBe(Date.now); + + // Jest doesn't property mock new Intl.DateTimeFormat().format() + expect(new Intl.DateTimeFormat().format()).toBe("12/19/1995"); + } else { + expect(DateBeforeMocked).not.toBe(Date); + expect(DateBeforeMocked.now).not.toBe(Date.now); + } + + jest.useRealTimers(); + const now = new Date(); + now.setHours(0, 0, 0, 0); + expect(now.toISOString()).toBe(orig.toISOString()); +}); diff --git a/test/js/bun/util/bun-file-exists.test.js b/test/js/bun/util/bun-file-exists.test.js new file mode 100644 index 000000000..cca28e359 --- /dev/null +++ b/test/js/bun/util/bun-file-exists.test.js @@ -0,0 +1,20 @@ +import { test, expect } from "bun:test"; +import { join } from "path"; +import { tmpdir } from "os"; +import { write } from "bun"; +import { unlinkSync } from "fs"; +test("bun-file-exists", async () => { + expect(await Bun.file(import.meta.path).exists()).toBeTrue(); + expect(await Bun.file(import.meta.path + "boop").exists()).toBeFalse(); + expect(await Bun.file(import.meta.dir).exists()).toBeFalse(); + expect(await Bun.file(import.meta.dir + "/").exists()).toBeFalse(); + const temp = join(tmpdir(), "bun-file-exists.test.js"); + try { + unlinkSync(temp); + } catch (e) {} + expect(await Bun.file(temp).exists()).toBeFalse(); + await write(temp, "boop"); + expect(await Bun.file(temp).exists()).toBeTrue(); + unlinkSync(temp); + expect(await Bun.file(temp).exists()).toBeFalse(); +}); diff --git a/test/js/bun/util/error-gc-test.test.js b/test/js/bun/util/error-gc-test.test.js new file mode 100644 index 000000000..4a45346b6 --- /dev/null +++ b/test/js/bun/util/error-gc-test.test.js @@ -0,0 +1,81 @@ +import { test, expect } from "bun:test"; +import { readFileSync } from "fs"; +// This test checks that printing stack traces increments and decrements +// reference-counted strings +test("error gc test", () => { + for (let i = 0; i < 100; i++) { + var fn = function yo() { + var err = (function innerOne() { + var err = new Error(); + for (let i = 0; i < 1000; i++) { + Bun.inspect(err); + } + Bun.gc(true); + return err; + })(); + err.stack += ""; + }; + + Object.defineProperty(fn, "name", { + value: + "yoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyo" + + i, + }); + + fn(); + Bun.gc(true); + } +}); + +test("error gc test #2", () => { + for (let i = 0; i < 1000; i++) { + new Error().stack; + Bun.gc(); + } +}); + +test("error gc test #3", () => { + for (let i = 0; i < 1000; i++) { + var err = new Error(); + Error.captureStackTrace(err); + Bun.inspect(err); + Bun.gc(); + } +}); + +// This test fails if: +// - it crashes +// - The test failure message gets a non-sensical error +test("error gc test #4", () => { + for (let i = 0; i < 1000; i++) { + let path = + // Use a long-enough string for it to be obvious if we leak memory + "/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/ii/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/ii/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i"; + try { + readFileSync(path); + throw new Error("unreachable"); + } catch (e) { + if (e.message === "unreachable") { + throw e; + } + + const inspected = Bun.inspect(e); + Bun.gc(true); + + // Deliberately avoid using .toContain() directly to avoid + // BunString shenanigins. + // + // Only JSC builtin functions to operate on the string after inspecting it. + // + if (!inspected.includes(path)) { + expect(inspected).toContain(path); + } + + if (!inspected.includes("ENOENT")) { + expect(inspected).toContain("ENOENT"); + } + } finally { + Bun.gc(true); + } + } +}); diff --git a/test/js/bun/websocket/websocket-server.test.ts b/test/js/bun/websocket/websocket-server.test.ts index 7d43c7e65..7a79f9f5f 100644 --- a/test/js/bun/websocket/websocket-server.test.ts +++ b/test/js/bun/websocket/websocket-server.test.ts @@ -3,6 +3,77 @@ import { gcTick } from "harness"; import { serve, ServerWebSocket } from "bun"; describe("websocket server", () => { + it("send & receive empty messages", done => { + const serverReceived: any[] = []; + const clientReceived: any[] = []; + var clientDone = false; + var serverDone = false; + + let server = Bun.serve({ + websocket: { + open(ws) { + ws.send(""); + ws.send(new ArrayBuffer(0)); + }, + message(ws, data) { + serverReceived.push(data); + + if (serverReceived.length === 2) { + if (serverReceived.find(d => d === "") === undefined) { + done(new Error("expected empty string")); + } + + if (!serverReceived.find(d => d.byteLength === 0)) { + done(new Error("expected empty Buffer")); + } + + serverDone = true; + + if (clientDone && serverDone) { + z.close(); + server.stop(true); + done(); + } + } + }, + close() {}, + }, + fetch(req, server) { + if (!server.upgrade(req)) { + return new Response(null, { status: 404 }); + } + }, + port: 0, + }); + + let z = new WebSocket(`ws://${server.hostname}:${server.port}`); + z.onmessage = e => { + clientReceived.push(e.data); + + if (clientReceived.length === 2) { + if (clientReceived.find(d => d === "") === undefined) { + done(new Error("expected empty string")); + } + + if (!clientReceived.find(d => d.byteLength === 0)) { + done(new Error("expected empty Buffer")); + } + + clientDone = true; + if (clientDone && serverDone) { + server.stop(true); + z.close(); + + done(); + } + } + }; + z.addEventListener("open", () => { + z.send(""); + z.send(new Buffer(0)); + }); + }); + it("remoteAddress works", done => { let server = Bun.serve({ websocket: { @@ -849,7 +920,7 @@ describe("websocket server", () => { it("send rope strings", async () => { var ropey = "hello world".repeat(10); var sendQueue: any[] = []; - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 20; i++) { sendQueue.push(ropey + " " + i); } @@ -859,16 +930,13 @@ describe("websocket server", () => { const server = serve({ port: 0, websocket: { - open(ws) { - server.stop(); - }, + open(ws) {}, message(ws, msg) { ws.send(sendQueue[serverCounter++] + " "); - gcTick(); + serverCounter % 10 === 0 && gcTick(); }, }, fetch(req, server) { - server.stop(); if ( server.upgrade(req, { data: { count: 0 }, @@ -879,54 +947,56 @@ describe("websocket server", () => { return new Response("noooooo hello world"); }, }); + try { + await new Promise<void>((resolve, reject) => { + const websocket = new WebSocket(`ws://${server.hostname}:${server.port}`); + websocket.onerror = e => { + reject(e); + }; - await new Promise<void>((resolve, reject) => { - const websocket = new WebSocket(`ws://${server.hostname}:${server.port}`); - websocket.onerror = e => { - reject(e); - }; + websocket.onopen = () => { + server.stop(); + websocket.send("first"); + }; - var counter = 0; - websocket.onopen = () => websocket.send("first"); - websocket.onmessage = e => { - try { - const expected = sendQueue[clientCounter++] + " "; - expect(e.data).toBe(expected); - websocket.send("next"); - if (clientCounter === sendQueue.length) { + websocket.onmessage = e => { + try { + const expected = sendQueue[clientCounter++] + " "; + expect(e.data).toBe(expected); + websocket.send("next"); + if (clientCounter === sendQueue.length) { + websocket.close(); + resolve(); + } + } catch (r) { + reject(r); + console.error(r); websocket.close(); - resolve(); } - } catch (r) { - reject(r); - console.error(r); - websocket.close(); - } - }; - }); - server.stop(true); + }; + }); + } catch (e) { + throw e; + } finally { + server.stop(true); + } }); - // this test sends 100 messages to 10 connected clients via pubsub + // this test sends 50 messages to 10 connected clients via pubsub it("pub/sub", async () => { var ropey = "hello world".repeat(10); var sendQueue: any[] = []; - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 50; i++) { sendQueue.push(ropey + " " + i); - gcTick(); } + var serverCounter = 0; var clientCount = 0; const server = serve({ port: 0, websocket: { - // FIXME: update this test to not rely on publishToSelf: true, - publishToSelf: true, - open(ws) { - server.stop(); ws.subscribe("test"); - gcTick(); if (!ws.isSubscribed("test")) { throw new Error("not subscribed"); } @@ -936,15 +1006,15 @@ describe("websocket server", () => { } ws.subscribe("test"); clientCount++; - if (clientCount === 10) setTimeout(() => ws.publish("test", "hello world"), 1); + if (clientCount === 10) { + setTimeout(() => server.publish("test", "hello world"), 1); + } }, message(ws, msg) { - if (serverCounter < sendQueue.length) ws.publish("test", sendQueue[serverCounter++] + " "); + if (serverCounter < sendQueue.length) server.publish("test", sendQueue[serverCounter++] + " "); }, }, fetch(req) { - gcTick(); - server.stop(); if ( server.upgrade(req, { data: { count: 0 }, @@ -954,90 +1024,90 @@ describe("websocket server", () => { return new Response("noooooo hello world"); }, }); + try { + const connections = new Array(10); + const websockets = new Array(connections.length); + var doneCounter = 0; + await new Promise<void>(done => { + for (var i = 0; i < connections.length; i++) { + var j = i; + var resolve: (_?: unknown) => void, + reject: (_?: unknown) => void, + resolveConnection: (_?: unknown) => void, + rejectConnection: (_?: unknown) => void; + connections[j] = new Promise((res, rej) => { + resolveConnection = res; + rejectConnection = rej; + }); + websockets[j] = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + const websocket = new WebSocket(`ws://${server.hostname}:${server.port}`); + websocket.onerror = e => { + reject(e); + }; + websocket.onclose = () => { + doneCounter++; + if (doneCounter === connections.length) { + done(); + } + }; + var hasOpened = false; + websocket.onopen = () => { + if (!hasOpened) { + hasOpened = true; + resolve(websocket); + } + }; - const connections = new Array(10); - const websockets = new Array(connections.length); - var doneCounter = 0; - await new Promise<void>(done => { - for (var i = 0; i < connections.length; i++) { - var j = i; - var resolve: (_?: unknown) => void, - reject: (_?: unknown) => void, - resolveConnection: (_?: unknown) => void, - rejectConnection: (_?: unknown) => void; - connections[j] = new Promise((res, rej) => { - resolveConnection = res; - rejectConnection = rej; - }); - websockets[j] = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - gcTick(); - const websocket = new WebSocket(`ws://${server.hostname}:${server.port}`); - websocket.onerror = e => { - reject(e); - }; - websocket.onclose = () => { - doneCounter++; - if (doneCounter === connections.length) { - done(); - } - }; - var hasOpened = false; - websocket.onopen = () => { - if (!hasOpened) { - hasOpened = true; - resolve(websocket); - } - }; - - let clientCounter = -1; - var hasSentThisTick = false; - - websocket.onmessage = e => { - gcTick(); - - if (!hasOpened) { - hasOpened = true; - resolve(websocket); - } + let clientCounter = -1; + var hasSentThisTick = false; - if (e.data === "hello world") { - clientCounter = 0; - websocket.send("first"); - return; - } + websocket.onmessage = e => { + if (!hasOpened) { + hasOpened = true; + resolve(websocket); + } - try { - expect(!!sendQueue.find(a => a + " " === e.data)).toBe(true); - - if (!hasSentThisTick) { - websocket.send("second"); - hasSentThisTick = true; - queueMicrotask(() => { - hasSentThisTick = false; - }); + if (e.data === "hello world") { + clientCounter = 0; + websocket.send("first"); + return; } - gcTick(); + try { + expect(!!sendQueue.find(a => a + " " === e.data)).toBe(true); + + if (!hasSentThisTick) { + websocket.send("second"); + hasSentThisTick = true; + queueMicrotask(() => { + hasSentThisTick = false; + }); + } - if (clientCounter++ === sendQueue.length - 1) { + if (clientCounter++ === sendQueue.length - 1) { + websocket.close(); + resolveConnection(); + } + } catch (r) { + console.error(r); websocket.close(); - resolveConnection(); + rejectConnection(r); } - } catch (r) { - console.error(r); - websocket.close(); - rejectConnection(r); - gcTick(); - } - }; - } - }); + }; + } + }); + } catch (e) { + throw e; + } finally { + server.stop(true); + gcTick(); + } + expect(serverCounter).toBe(sendQueue.length); - server.stop(true); - }, 10_000); + }, 30_000); it("can close with reason and code #2631", done => { let timeout: any; let server = Bun.serve({ diff --git a/test/js/node/assert/assert.test.cjs b/test/js/node/assert/assert.test.cjs new file mode 100644 index 000000000..e9d472412 --- /dev/null +++ b/test/js/node/assert/assert.test.cjs @@ -0,0 +1,9 @@ +const assert = require("assert"); + +test("assert from require as a function does not throw", () => assert(true)); +test("assert from require as a function does throw", () => { + try { + assert(false); + expect(false).toBe(true); + } catch (e) {} +}); diff --git a/test/js/node/assert/assert-test.test.ts b/test/js/node/assert/assert.test.ts index 1723b7d47..1723b7d47 100644 --- a/test/js/node/assert/assert-test.test.ts +++ b/test/js/node/assert/assert.test.ts diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 697774e0a..cfd114423 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -1,4 +1,4 @@ -import { Buffer, SlowBuffer } from "buffer"; +import { Buffer, SlowBuffer, isAscii, isUtf8 } from "buffer"; import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { gc } from "harness"; @@ -7,6 +7,28 @@ const BufferModule = await import("buffer"); beforeEach(() => gc()); afterEach(() => gc()); +it("isAscii", () => { + expect(isAscii(new Buffer("abc"))).toBeTrue(); + expect(isAscii(new Buffer(""))).toBeTrue(); + expect(isAscii(new Buffer([32, 32, 128]))).toBeFalse(); + expect(isAscii(new Buffer("What did the 🦊 say?"))).toBeFalse(); + + expect(isAscii(new Buffer("").buffer)).toBeTrue(); + expect(isAscii(new Buffer([32, 32, 128]).buffer)).toBeFalse(); +}); + +it("isUtf8", () => { + expect(isUtf8(new Buffer("abc"))).toBeTrue(); + expect(isAscii(new Buffer(""))).toBeTrue(); + expect(isUtf8(new Buffer("What did the 🦊 say?"))).toBeTrue(); + expect(isUtf8(new Buffer([129, 129, 129]))).toBeFalse(); + + expect(isUtf8(new Buffer("abc").buffer)).toBeTrue(); + expect(isAscii(new Buffer("").buffer)).toBeTrue(); + expect(isUtf8(new Buffer("What did the 🦊 say?").buffer)).toBeTrue(); + expect(isUtf8(new Buffer([129, 129, 129]).buffer)).toBeFalse(); +}); + // https://github.com/oven-sh/bun/issues/2052 it("Buffer global is settable", () => { var prevBuffer = globalThis.Buffer; @@ -2353,6 +2375,85 @@ it("Buffer.byteLength()", () => { } }); +it("Buffer.toString(encoding, start, end)", () => { + const buf = Buffer.from("0123456789", "utf8"); + + expect(buf.toString()).toStrictEqual("0123456789"); + expect(buf.toString("utf8")).toStrictEqual("0123456789"); + expect(buf.toString("utf8", 3)).toStrictEqual("3456789"); + expect(buf.toString("utf8", 3, 4)).toStrictEqual("3"); + + expect(buf.toString("utf8", 3, 100)).toStrictEqual("3456789"); + expect(buf.toString("utf8", 3, 1)).toStrictEqual(""); + expect(buf.toString("utf8", 100, 200)).toStrictEqual(""); + expect(buf.toString("utf8", 100, 1)).toStrictEqual(""); +}); + +it("Buffer.toString(offset, length, encoding)", () => { + const buf = Buffer.from("0123456789", "utf8"); + + expect(buf.toString(3, 6, "utf8")).toStrictEqual("345678"); + expect(buf.toString(3, 100, "utf8")).toStrictEqual("3456789"); + expect(buf.toString(100, 200, "utf8")).toStrictEqual(""); + expect(buf.toString(100, 50, "utf8")).toStrictEqual(""); +}); + +it("Buffer.asciiSlice())", () => { + const buf = Buffer.from("0123456789", "ascii"); + + expect(buf.asciiSlice()).toStrictEqual("0123456789"); + expect(buf.asciiSlice(3)).toStrictEqual("3456789"); + expect(buf.asciiSlice(3, 4)).toStrictEqual("3"); +}); + +it("Buffer.latin1Slice()", () => { + const buf = Buffer.from("âéö", "latin1"); + + expect(buf.latin1Slice()).toStrictEqual("âéö"); + expect(buf.latin1Slice(1)).toStrictEqual("éö"); + expect(buf.latin1Slice(1, 2)).toStrictEqual("é"); +}); + +it("Buffer.utf8Slice()", () => { + const buf = Buffer.from("あいうえお", "utf8"); + + expect(buf.utf8Slice()).toStrictEqual("あいうえお"); + expect(buf.utf8Slice(3)).toStrictEqual("いうえお"); + expect(buf.utf8Slice(3, 6)).toStrictEqual("い"); +}); + +it("Buffer.hexSlice()", () => { + const buf = Buffer.from("0123456789", "utf8"); + + expect(buf.hexSlice()).toStrictEqual("30313233343536373839"); + expect(buf.hexSlice(3)).toStrictEqual("33343536373839"); + expect(buf.hexSlice(3, 4)).toStrictEqual("33"); +}); + +it("Buffer.ucs2Slice()", () => { + const buf = Buffer.from("あいうえお", "ucs2"); + + expect(buf.ucs2Slice()).toStrictEqual("あいうえお"); + expect(buf.ucs2Slice(2)).toStrictEqual("いうえお"); + expect(buf.ucs2Slice(2, 6)).toStrictEqual("いう"); +}); + +it("Buffer.base64Slice()", () => { + const buf = Buffer.from("0123456789", "utf8"); + + expect(buf.base64Slice()).toStrictEqual("MDEyMzQ1Njc4OQ=="); + expect(buf.base64Slice(3)).toStrictEqual("MzQ1Njc4OQ=="); + expect(buf.base64Slice(3, 4)).toStrictEqual("Mw=="); +}); + +it("Buffer.base64urlSlice()", () => { + const buf = Buffer.from("0123456789", "utf8"); + + expect(buf.base64urlSlice()).toStrictEqual("MDEyMzQ1Njc4OQ"); + expect(buf.base64urlSlice(3)).toStrictEqual("MzQ1Njc4OQ"); + expect(buf.base64urlSlice(3, 4)).toStrictEqual("Mw"); +}); + it("should not crash on invalid UTF-8 byte sequence", () => { const buf = Buffer.from([0xc0, 0xfd]); expect(buf.length).toBe(2); @@ -2392,3 +2493,48 @@ it("inspect() should exist", () => { expect(Buffer.prototype.inspect).toBeInstanceOf(Function); expect(new Buffer("123").inspect()).toBe(Bun.inspect(new Buffer("123"))); }); + +it("read alias", () => { + var buf = new Buffer(1024); + var data = new DataView(buf.buffer); + + data.setUint8(0, 200, false); + + expect(buf.readUint8(0)).toBe(buf.readUInt8(0)); + expect(buf.readUintBE(0, 4)).toBe(buf.readUIntBE(0, 4)); + expect(buf.readUintLE(0, 4)).toBe(buf.readUIntLE(0, 4)); + expect(buf.readUint16BE(0)).toBe(buf.readUInt16BE(0)); + expect(buf.readUint16LE(0)).toBe(buf.readUInt16LE(0)); + expect(buf.readUint32BE(0)).toBe(buf.readUInt32BE(0)); + expect(buf.readUint32LE(0)).toBe(buf.readUInt32LE(0)); + expect(buf.readBigUint64BE(0)).toBe(buf.readBigUInt64BE(0)); + expect(buf.readBigUint64LE(0)).toBe(buf.readBigUInt64LE(0)); +}); + +it("write alias", () => { + var buf = new Buffer(1024); + var buf2 = new Buffer(1024); + + function reset() { + new Uint8Array(buf.buffer).fill(0); + new Uint8Array(buf2.buffer).fill(0); + } + + function shouldBeSame(name, name2, ...args) { + buf[name].call(buf, ...args); + buf2[name2].call(buf2, ...args); + + expect(buf).toStrictEqual(buf2); + reset(); + } + + shouldBeSame("writeUint8", "writeUInt8", 10); + shouldBeSame("writeUintBE", "writeUIntBE", 10, 0, 4); + shouldBeSame("writeUintLE", "writeUIntLE", 10, 0, 4); + shouldBeSame("writeUint16BE", "writeUInt16BE", 1000); + shouldBeSame("writeUint16LE", "writeUInt16LE", 1000); + shouldBeSame("writeUint32BE", "writeUInt32BE", 1000); + shouldBeSame("writeUint32LE", "writeUInt32LE", 1000); + shouldBeSame("writeBigUint64BE", "writeBigUInt64BE", BigInt(1000)); + shouldBeSame("writeBigUint64LE", "writeBigUInt64LE", BigInt(1000)); +}); diff --git a/test/js/node/crypto/crypto.test.ts b/test/js/node/crypto/crypto.test.ts index d8bfe5353..b1b8646f3 100644 --- a/test/js/node/crypto/crypto.test.ts +++ b/test/js/node/crypto/crypto.test.ts @@ -1,6 +1,6 @@ import { sha, MD5, MD4, SHA1, SHA224, SHA256, SHA384, SHA512, SHA512_256, gc, CryptoHasher } from "bun"; import { it, expect, describe } from "bun:test"; - +import crypto from "crypto"; const HashClasses = [MD5, MD4, SHA1, SHA224, SHA256, SHA384, SHA512, SHA512_256]; describe("CryptoHasher", () => { @@ -109,6 +109,13 @@ describe("CryptoHasher", () => { } }); +describe("crypto.getCurves", () => { + it("should return an array of strings", () => { + expect(Array.isArray(crypto.getCurves())).toBe(true); + expect(typeof crypto.getCurves()[0]).toBe("string"); + }); +}); + describe("crypto", () => { for (let Hash of HashClasses) { for (let [input, label] of [ diff --git a/test/js/node/crypto/node-crypto.test.js b/test/js/node/crypto/node-crypto.test.js index 9e0e7f396..2489f96c7 100644 --- a/test/js/node/crypto/node-crypto.test.js +++ b/test/js/node/crypto/node-crypto.test.js @@ -8,6 +8,27 @@ it("crypto.randomBytes should return a Buffer", () => { expect(Buffer.isBuffer(crypto.randomBytes(1))).toBe(true); }); +it("crypto.randomInt should return a number", () => { + const result = crypto.randomInt(0, 10); + expect(typeof result).toBe("number"); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(10); +}); + +it("crypto.randomInt with no arguments", () => { + const result = crypto.randomInt(); + expect(typeof result).toBe("number"); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(Number.MAX_SAFE_INTEGER); +}); + +it("crypto.randomInt with one argument", () => { + const result = crypto.randomInt(100); + expect(typeof result).toBe("number"); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(100); +}); + // https://github.com/oven-sh/bun/issues/1839 describe("createHash", () => { it("update & digest", () => { @@ -22,6 +43,50 @@ describe("createHash", () => { expect(Buffer.isBuffer(hash.digest())).toBeTrue(); }); + const otherEncodings = { + ucs2: [ + 11626, 2466, 37699, 38942, 64564, 53010, 48101, 47943, 44761, 18499, 12442, 26994, 46434, 62582, 39395, 20542, + ], + latin1: [ + 106, 45, 162, 9, 67, 147, 30, 152, 52, 252, 18, 207, 229, 187, 71, 187, 217, 174, 67, 72, 154, 48, 114, 105, 98, + 181, 118, 244, 227, 153, 62, 80, + ], + binary: [ + 106, 45, 162, 9, 67, 147, 30, 152, 52, 252, 18, 207, 229, 187, 71, 187, 217, 174, 67, 72, 154, 48, 114, 105, 98, + 181, 118, 244, 227, 153, 62, 80, + ], + base64: [ + 97, 105, 50, 105, 67, 85, 79, 84, 72, 112, 103, 48, 47, 66, 76, 80, 53, 98, 116, 72, 117, 57, 109, 117, 81, 48, + 105, 97, 77, 72, 74, 112, 89, 114, 86, 50, 57, 79, 79, 90, 80, 108, 65, 61, + ], + hex: [ + 54, 97, 50, 100, 97, 50, 48, 57, 52, 51, 57, 51, 49, 101, 57, 56, 51, 52, 102, 99, 49, 50, 99, 102, 101, 53, 98, + 98, 52, 55, 98, 98, 100, 57, 97, 101, 52, 51, 52, 56, 57, 97, 51, 48, 55, 50, 54, 57, 54, 50, 98, 53, 55, 54, 102, + 52, 101, 51, 57, 57, 51, 101, 53, 48, + ], + ascii: [ + 106, 45, 34, 9, 67, 19, 30, 24, 52, 124, 18, 79, 101, 59, 71, 59, 89, 46, 67, 72, 26, 48, 114, 105, 98, 53, 118, + 116, 99, 25, 62, 80, + ], + utf8: [ + 106, 45, 65533, 9, 67, 65533, 30, 65533, 52, 65533, 18, 65533, 65533, 71, 65533, 1646, 67, 72, 65533, 48, 114, + 105, 98, 65533, 118, 65533, 65533, 62, 80, + ], + }; + + for (let encoding in otherEncodings) { + it("digest " + encoding, () => { + const hash = crypto.createHash("sha256"); + hash.update("some data to hash"); + expect( + hash + .digest(encoding) + .split("") + .map(a => a.charCodeAt(0)), + ).toEqual(otherEncodings[encoding]); + }); + } + it("stream (sync)", () => { const hash = crypto.createHash("sha256"); hash.write("some data to hash"); diff --git a/test/js/node/disabled-module.test.cjs b/test/js/node/disabled-module.test.cjs new file mode 100644 index 000000000..bc4817b8d --- /dev/null +++ b/test/js/node/disabled-module.test.cjs @@ -0,0 +1,6 @@ +test("not implemented yet module masquerades as undefined in cjs and throws an error", () => { + const worker_threads = require("worker_threads"); + + expect(typeof worker_threads).toBe("undefined"); + expect(typeof worker_threads.getEnvironmentData).toBe("undefined"); +}); diff --git a/test/js/node/disabled-module.test.js b/test/js/node/disabled-module.test.js index d02a6b6df..bb707a122 100644 --- a/test/js/node/disabled-module.test.js +++ b/test/js/node/disabled-module.test.js @@ -1,15 +1,16 @@ import { expect, test } from "bun:test"; +import { AsyncResource, AsyncLocalStorage } from "async_hooks"; +import * as worker_threads from "worker_threads"; +import worker_threads_default from "worker_threads"; test("not implemented yet module masquerades as undefined and throws an error", () => { - const worker_threads = import.meta.require("worker_threads"); - - expect(typeof worker_threads).toBe("undefined"); + expect(typeof worker_threads.default).toBe("undefined"); + expect(typeof worker_threads_default).toBe("undefined"); expect(typeof worker_threads.getEnvironmentData).toBe("undefined"); + expect(typeof worker_threads_default.getEnvironmentData).toBe("undefined"); }); test("AsyncLocalStorage polyfill", () => { - const { AsyncLocalStorage } = import.meta.require("async_hooks"); - const store = new AsyncLocalStorage(); var called = false; expect(store.getStore()).toBe(null); @@ -22,8 +23,6 @@ test("AsyncLocalStorage polyfill", () => { }); test("AsyncResource polyfill", () => { - const { AsyncResource } = import.meta.require("async_hooks"); - const resource = new AsyncResource("prisma-client-request"); var called = false; resource.runInAsyncScope( @@ -36,3 +35,9 @@ test("AsyncResource polyfill", () => { ); expect(called).toBe(true); }); + +test("esbuild functions with worker_threads stub", async () => { + const esbuild = await import("esbuild"); + const result = await esbuild.transform('console . log( "hello world" )', { minify: true }); + expect(result.code).toBe('console.log("hello world");\n'); +}); diff --git a/test/js/node/dns/dns.node.mjs b/test/js/node/dns/dns.node.mjs deleted file mode 100644 index e69de29bb..000000000 --- a/test/js/node/dns/dns.node.mjs +++ /dev/null diff --git a/test/js/node/dns/node-dns.test.js b/test/js/node/dns/node-dns.test.js index 5fb8e0739..5de840146 100644 --- a/test/js/node/dns/node-dns.test.js +++ b/test/js/node/dns/node-dns.test.js @@ -1,5 +1,6 @@ import { expect, test } from "bun:test"; import * as dns from "node:dns"; +import * as dns_promises from "node:dns/promises"; // TODO: test("it exists", () => { @@ -18,6 +19,38 @@ test("it exists", () => { expect(dns.resolveNs).toBeDefined(); expect(dns.resolvePtr).toBeDefined(); expect(dns.resolveCname).toBeDefined(); + + expect(dns.promises).toBeDefined(); + expect(dns.promises.lookup).toBeDefined(); + expect(dns.promises.lookupService).toBeDefined(); + expect(dns.promises.resolve).toBeDefined(); + expect(dns.promises.resolve4).toBeDefined(); + expect(dns.promises.resolve6).toBeDefined(); + expect(dns.promises.resolveSrv).toBeDefined(); + expect(dns.promises.resolveTxt).toBeDefined(); + expect(dns.promises.resolveSoa).toBeDefined(); + expect(dns.promises.resolveNaptr).toBeDefined(); + expect(dns.promises.resolveMx).toBeDefined(); + expect(dns.promises.resolveCaa).toBeDefined(); + expect(dns.promises.resolveNs).toBeDefined(); + expect(dns.promises.resolvePtr).toBeDefined(); + expect(dns.promises.resolveCname).toBeDefined(); + + expect(dns_promises).toBeDefined(); + expect(dns_promises.lookup).toBeDefined(); + expect(dns_promises.lookupService).toBeDefined(); + expect(dns_promises.resolve).toBeDefined(); + expect(dns_promises.resolve4).toBeDefined(); + expect(dns_promises.resolve6).toBeDefined(); + expect(dns_promises.resolveSrv).toBeDefined(); + expect(dns_promises.resolveTxt).toBeDefined(); + expect(dns_promises.resolveSoa).toBeDefined(); + expect(dns_promises.resolveNaptr).toBeDefined(); + expect(dns_promises.resolveMx).toBeDefined(); + expect(dns_promises.resolveCaa).toBeDefined(); + expect(dns_promises.resolveNs).toBeDefined(); + expect(dns_promises.resolvePtr).toBeDefined(); + expect(dns_promises.resolveCname).toBeDefined(); }); // //TODO: use a bun.sh SRV for testing diff --git a/test/js/node/events/event-emitter.test.ts b/test/js/node/events/event-emitter.test.ts index cef309d48..5a1385383 100644 --- a/test/js/node/events/event-emitter.test.ts +++ b/test/js/node/events/event-emitter.test.ts @@ -1,5 +1,6 @@ import { test, describe, expect } from "bun:test"; import { sleep } from "bun"; +import { createRequire } from "module"; // this is also testing that imports with default and named imports in the same statement work // our transpiler transform changes this to a var with import.meta.require @@ -534,4 +535,10 @@ describe("EventEmitter constructors", () => { expect(called).toBe(true); }); } + + test("with createRequire, events is callable", () => { + const req = createRequire(import.meta.path); + const events = req("events"); + new events(); + }); }); diff --git a/test/js/node/events/events-cjs.test.js b/test/js/node/events/events-cjs.test.js new file mode 100644 index 000000000..5bee9979f --- /dev/null +++ b/test/js/node/events/events-cjs.test.js @@ -0,0 +1,4 @@ +test("in cjs, events is callable", () => { + const events = require("events"); + new events(); +}); diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 37c3253a4..48aa9d3b9 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -29,6 +29,8 @@ import fs, { realpathSync, readlinkSync, symlinkSync, + writevSync, + readvSync, } from "node:fs"; import _promises from "node:fs/promises"; @@ -157,6 +159,18 @@ it("readdirSync on import.meta.dir", () => { expect(match).toBe(true); }); +it("statSync throwIfNoEntry", () => { + expect(statSync("/tmp/404/not-found/ok", { throwIfNoEntry: false })).toBeUndefined(); + expect(lstatSync("/tmp/404/not-found/ok", { throwIfNoEntry: false })).toBeUndefined(); +}); + +it("statSync throwIfNoEntry: true", () => { + expect(() => statSync("/tmp/404/not-found/ok", { throwIfNoEntry: true })).toThrow("No such file or directory"); + expect(() => statSync("/tmp/404/not-found/ok")).toThrow("No such file or directory"); + expect(() => lstatSync("/tmp/404/not-found/ok", { throwIfNoEntry: true })).toThrow("No such file or directory"); + expect(() => lstatSync("/tmp/404/not-found/ok")).toThrow("No such file or directory"); +}); + // https://github.com/oven-sh/bun/issues/1887 it("mkdtempSync, readdirSync, rmdirSync and unlinkSync with non-ascii", () => { const tempdir = mkdtempSync(`${tmpdir()}/emoji-fruit-🍇 🍈 🍉 🍊 🍋`); @@ -276,6 +290,41 @@ it("readdirSync throws when given a file path with trailing slash", () => { describe("readSync", () => { const firstFourBytes = new Uint32Array(new TextEncoder().encode("File").buffer)[0]; + + it("works on large files", () => { + const dest = join(tmpdir(), "readSync-large-file.txt"); + rmSync(dest, { force: true }); + + const writefd = openSync(dest, "w"); + writeSync(writefd, Buffer.from([0x10]), 0, 1, 4_900_000_000); + closeSync(writefd); + + const fd = openSync(dest, "r"); + const out = Buffer.alloc(1); + const bytes = readSync(fd, out, 0, 1, 4_900_000_000); + expect(bytes).toBe(1); + expect(out[0]).toBe(0x10); + closeSync(fd); + rmSync(dest, { force: true }); + }); + + it("works with bigint on read", () => { + const dest = join(tmpdir(), "readSync-large-file-bigint.txt"); + rmSync(dest, { force: true }); + + const writefd = openSync(dest, "w"); + writeSync(writefd, Buffer.from([0x10]), 0, 1, 400); + closeSync(writefd); + + const fd = openSync(dest, "r"); + const out = Buffer.alloc(1); + const bytes = readSync(fd, out, 0, 1, 400n as any); + expect(bytes).toBe(1); + expect(out[0]).toBe(0x10); + closeSync(fd); + rmSync(dest, { force: true }); + }); + it("works with a position set to 0", () => { const fd = openSync(import.meta.dir + "/readFileSync.txt", "r"); const four = new Uint8Array(4); @@ -301,7 +350,87 @@ describe("readSync", () => { }); }); +it("writevSync", () => { + var fd = openSync(`${tmpdir()}/writevSync.txt`, "w"); + fs.ftruncateSync(fd, 0); + const buffers = [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6]), new Uint8Array([7, 8, 9])]; + const result = writevSync(fd, buffers); + expect(result).toBe(9); + closeSync(fd); + + fd = openSync(`${tmpdir()}/writevSync.txt`, "r"); + const buf = new Uint8Array(9); + readSync(fd, buf, 0, 9, 0); + expect(buf).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9])); +}); + +it("pwritevSync", () => { + var fd = openSync(`${tmpdir()}/pwritevSync.txt`, "w"); + fs.ftruncateSync(fd, 0); + writeSync(fd, "lalalala", 0); + const buffers = [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6]), new Uint8Array([7, 8, 9])]; + const result = writevSync(fd, buffers, "lalalala".length); + expect(result).toBe(9); + closeSync(fd); + + const out = readFileSync(`${tmpdir()}/pwritevSync.txt`); + expect(out.slice(0, "lalalala".length).toString()).toBe("lalalala"); + expect(out.slice("lalalala".length)).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9])); +}); + +it("readvSync", () => { + var fd = openSync(`${tmpdir()}/readv.txt`, "w"); + fs.ftruncateSync(fd, 0); + + const buf = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]); + writeSync(fd, buf, 0, 9, 0); + closeSync(fd); + + var fd = openSync(`${tmpdir()}/readv.txt`, "r"); + const buffers = [new Uint8Array(3), new Uint8Array(3), new Uint8Array(3)]; + const result = readvSync(fd, buffers); + expect(result).toBe(9); + expect(buffers[0]).toEqual(new Uint8Array([1, 2, 3])); + expect(buffers[1]).toEqual(new Uint8Array([4, 5, 6])); + expect(buffers[2]).toEqual(new Uint8Array([7, 8, 9])); + closeSync(fd); +}); + +it("preadv", () => { + var fd = openSync(`${tmpdir()}/preadv.txt`, "w"); + fs.ftruncateSync(fd, 0); + + const buf = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + writeSync(fd, buf, 0, buf.byteLength, 0); + closeSync(fd); + + var fd = openSync(`${tmpdir()}/preadv.txt`, "r"); + const buffers = [new Uint8Array(3), new Uint8Array(3), new Uint8Array(3)]; + const result = readvSync(fd, buffers, 3); + expect(result).toBe(9); + expect(buffers[0]).toEqual(new Uint8Array([4, 5, 6])); + expect(buffers[1]).toEqual(new Uint8Array([7, 8, 9])); + expect(buffers[2]).toEqual(new Uint8Array([10, 11, 12])); +}); + describe("writeSync", () => { + it("works with bigint", () => { + const dest = join(tmpdir(), "writeSync-large-file-bigint.txt"); + rmSync(dest, { force: true }); + + const writefd = openSync(dest, "w"); + writeSync(writefd, Buffer.from([0x10]), 0, 1, 400n as any); + closeSync(writefd); + + const fd = openSync(dest, "r"); + const out = Buffer.alloc(1); + const bytes = readSync(fd, out, 0, 1, 400 as any); + expect(bytes).toBe(1); + expect(out[0]).toBe(0x10); + closeSync(fd); + rmSync(dest, { force: true }); + }); + it("works with a position set to 0", () => { const fd = openSync(import.meta.dir + "/writeFileSync.txt", "w+"); const four = new Uint8Array(4); @@ -1068,6 +1197,44 @@ describe("createWriteStream", () => { expect(exception.code).toBe("ERR_INVALID_ARG_TYPE"); } }); + + it("writing in append mode should not truncate the file", async () => { + const path = `${tmpdir()}/fs.test.js/${Date.now()}.createWriteStreamAppend.txt`; + const stream = createWriteStream(path, { + // @ts-ignore-next-line + flags: "a", + }); + stream.write("first line\n"); + stream.end(); + + await new Promise((resolve, reject) => { + stream.on("error", e => { + reject(e); + }); + + stream.on("finish", () => { + resolve(true); + }); + }); + + const stream2 = createWriteStream(path, { + // @ts-ignore-next-line + flags: "a", + }); + stream2.write("second line\n"); + stream2.end(); + + return await new Promise((resolve, reject) => { + stream2.on("error", e => { + reject(e); + }); + + stream2.on("finish", () => { + expect(readFileSync(path, "utf8")).toBe("first line\nsecond line\n"); + resolve(true); + }); + }); + }); }); describe("fs/promises", () => { @@ -1293,3 +1460,26 @@ describe("utimesSync", () => { expect(finalStats.atime).toEqual(prevAccessTime); }); }); + +it("createReadStream on a large file emits readable event correctly", () => { + return new Promise<void>((resolve, reject) => { + const tmp = mkdtempSync(`${tmpdir()}/readable`); + // write a 10mb file + writeFileSync(`${tmp}/large.txt`, "a".repeat(10 * 1024 * 1024)); + var stream = createReadStream(`${tmp}/large.txt`); + var ended = false; + var timer: Timer; + stream.on("readable", () => { + const v = stream.read(); + if (ended) { + clearTimeout(timer); + reject(new Error("readable emitted after end")); + } else if (v == null) { + ended = true; + timer = setTimeout(() => { + resolve(); + }, 20); + } + }); + }); +}); diff --git a/test/js/node/fs/node-fetch.cjs.test.js b/test/js/node/fs/node-fetch.cjs.test.js new file mode 100644 index 000000000..9a6a4b407 --- /dev/null +++ b/test/js/node/fs/node-fetch.cjs.test.js @@ -0,0 +1,13 @@ +const fetch = require("node-fetch"); + +test("require('node-fetch') fetches", async () => { + const server = Bun.serve({ + port: 0, + fetch(req, server) { + server.stop(); + return new Response(); + }, + }); + expect(await fetch("http://" + server.hostname + ":" + server.port)).toBeInstanceOf(Response); + server.stop(true); +}); diff --git a/test/js/node/fs/node-fetch.test.js b/test/js/node/fs/node-fetch.test.js index 11c5e0ed3..33af3252d 100644 --- a/test/js/node/fs/node-fetch.test.js +++ b/test/js/node/fs/node-fetch.test.js @@ -1,4 +1,4 @@ -import { fetch, Response, Request, Headers } from "node-fetch"; +import fetch2, { fetch, Response, Request, Headers } from "node-fetch"; import { test, expect } from "bun:test"; @@ -19,3 +19,15 @@ test("node-fetch fetches", async () => { expect(await fetch("http://" + server.hostname + ":" + server.port)).toBeInstanceOf(Response); server.stop(true); }); + +test("node-fetch.default fetches", async () => { + const server = Bun.serve({ + port: 0, + fetch(req, server) { + server.stop(); + return new Response(); + }, + }); + expect(await fetch2("http://" + server.hostname + ":" + server.port)).toBeInstanceOf(Response); + server.stop(true); +}); diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index b1910a1f7..0e7b3ca13 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -1,5 +1,14 @@ // @ts-nocheck -import { createServer, request, get, Agent, globalAgent, Server } from "node:http"; +import { + createServer, + request, + get, + Agent, + globalAgent, + Server, + validateHeaderName, + validateHeaderValue, +} from "node:http"; import { createTest } from "node-harness"; const { describe, expect, it, beforeAll, afterAll, createDoneDotAll } = createTest(import.meta.path); @@ -137,6 +146,29 @@ describe("node:http", () => { res.end("Path correct!\n"); return; } + if (reqUrl.pathname === "/customWriteHead") { + function createWriteHead(prevWriteHead, listener) { + let fired = false; + return function writeHead() { + if (!fired) { + fired = true; + listener.call(this); + } + return prevWriteHead.apply(this, arguments); + }; + } + + function addPoweredBy() { + if (!this.getHeader("X-Powered-By")) { + this.setHeader("X-Powered-By", "Bun"); + } + } + + res.writeHead = createWriteHead(res.writeHead, addPoweredBy); + res.setHeader("Content-Type", "text/plain"); + res.end("Hello World"); + return; + } } res.writeHead(200, { "Content-Type": "text/plain" }); @@ -498,6 +530,16 @@ describe("node:http", () => { req.end(); }); }); + it("reassign writeHead method, issue#3585", done => { + runTest(done, (server, serverPort, done) => { + const req = request(`http://localhost:${serverPort}/customWriteHead`, res => { + expect(res.headers["content-type"]).toBe("text/plain"); + expect(res.headers["x-powered-by"]).toBe("Bun"); + done(); + }); + req.end(); + }); + }); }); describe("signal", () => { @@ -624,4 +666,16 @@ describe("node:http", () => { }); }); }); + + test("validateHeaderName", () => { + validateHeaderName("Foo"); + expect(() => validateHeaderName("foo:")).toThrow(); + expect(() => validateHeaderName("foo:bar")).toThrow(); + }); + + test("validateHeaderValue", () => { + validateHeaderValue("Foo", "Bar"); + expect(() => validateHeaderValue("Foo", undefined as any)).toThrow(); + expect(() => validateHeaderValue("Foo", "Bar\r")).toThrow(); + }); }); diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index 549b5e085..434bac829 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -1,5 +1,29 @@ import { expect, test } from "bun:test"; +import { _nodeModulePaths } from "module"; +import Module from "module"; test("module.globalPaths exists", () => { expect(Array.isArray(require("module").globalPaths)).toBe(true); }); + +test("Module exists", () => { + expect(Module).toBeDefined(); +}); + +test("_nodeModulePaths() works", () => { + expect(() => { + _nodeModulePaths(); + }).toThrow(); + expect(_nodeModulePaths(".").length).toBeGreaterThan(0); + expect(_nodeModulePaths(".").pop()).toBe("/node_modules"); + expect(_nodeModulePaths("")).toEqual(_nodeModulePaths(".")); + expect(_nodeModulePaths("/")).toEqual(["/node_modules"]); + expect(_nodeModulePaths("/a/b/c/d")).toEqual([ + "/a/b/c/d/node_modules", + "/a/b/c/node_modules", + "/a/b/node_modules", + "/a/node_modules", + "/node_modules", + ]); + expect(_nodeModulePaths("/a/b/../d")).toEqual(["/a/d/node_modules", "/a/node_modules", "/node_modules"]); +}); diff --git a/test/js/node/net/node-net-server.test.ts b/test/js/node/net/node-net-server.test.ts index 398959bd6..3cdaa17e1 100644 --- a/test/js/node/net/node-net-server.test.ts +++ b/test/js/node/net/node-net-server.test.ts @@ -181,61 +181,6 @@ describe("net.createServer listen", () => { ); }); - it("should listen on the correct port", done => { - const { mustCall, mustNotCall } = createCallCheckCtx(done); - - const server: Server = createServer(); - - let timeout: Timer; - const closeAndFail = () => { - clearTimeout(timeout); - server.close(); - mustNotCall()(); - }; - server.on("error", closeAndFail); - timeout = setTimeout(closeAndFail, 100); - - server.listen( - 49027, - mustCall(() => { - const address = server.address() as AddressInfo; - expect(address.address).toStrictEqual("::"); - expect(address.port).toStrictEqual(49027); - expect(address.family).toStrictEqual("IPv6"); - server.close(); - done(); - }), - ); - }); - - it("should listen on the correct port with IPV4", done => { - const { mustCall, mustNotCall } = createCallCheckCtx(done); - - const server: Server = createServer(); - - let timeout: Timer; - const closeAndFail = () => { - clearTimeout(timeout); - server.close(); - mustNotCall()(); - }; - server.on("error", closeAndFail); - timeout = setTimeout(closeAndFail, 100); - - server.listen( - 49026, - "0.0.0.0", - mustCall(() => { - const address = server.address() as AddressInfo; - expect(address.address).toStrictEqual("0.0.0.0"); - expect(address.port).toStrictEqual(49026); - expect(address.family).toStrictEqual("IPv4"); - server.close(); - done(); - }), - ); - }); - it("should listen on unix domain socket", done => { const { mustCall, mustNotCall } = createCallCheckCtx(done); diff --git a/test/js/node/os/os.test.js b/test/js/node/os/os.test.js index d7229b56d..8b4d54bb7 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -43,11 +43,16 @@ it("tmpdir", () => { expect(os.tmpdir()).toBe(process.env.TEMP || process.env.TMP); expect(os.tmpdir()).toBe(`${process.env.SystemRoot || process.env.windir}\\temp`); } else { + const originalEnv = process.env.TMPDIR; let dir = process.env.TMPDIR || process.env.TMP || process.env.TEMP || "/tmp"; if (dir.length > 1 && dir.endsWith("/")) { dir = dir.substring(0, dir.length - 1); } expect(realpathSync(os.tmpdir())).toBe(realpathSync(dir)); + + process.env.TMPDIR = "/boop"; + expect(os.tmpdir()).toBe("/boop"); + process.env.TMPDIR = originalEnv; } }); diff --git a/test/js/node/path/path.test.js b/test/js/node/path/path.test.js index 94b0568f6..8f5a76da7 100644 --- a/test/js/node/path/path.test.js +++ b/test/js/node/path/path.test.js @@ -1,7 +1,7 @@ const { file } = import.meta; import { describe, it, expect } from "bun:test"; -import * as path from "node:path"; +import path from "node:path"; import assert from "assert"; import { hideFromStackTrace } from "harness"; diff --git a/test/js/node/process/call-raise.js b/test/js/node/process/call-raise.js new file mode 100644 index 000000000..898906759 --- /dev/null +++ b/test/js/node/process/call-raise.js @@ -0,0 +1,15 @@ +import { dlopen } from "bun:ffi"; + +var lazyRaise; +export function raise(signal) { + if (!lazyRaise) { + const suffix = process.platform === "darwin" ? "dylib" : "so.6"; + lazyRaise = dlopen(`libc.${suffix}`, { + raise: { + args: ["int"], + returns: "int", + }, + }).symbols.raise; + } + lazyRaise(signal); +} diff --git a/test/js/node/process/print-process-args.js b/test/js/node/process/print-process-args.js index 0ab238122..e9d2295c8 100644 --- a/test/js/node/process/print-process-args.js +++ b/test/js/node/process/print-process-args.js @@ -1,3 +1,11 @@ +import assert from "assert"; + +// ensure process.argv and Bun.argv are the same +assert.deepStrictEqual(process.argv, Bun.argv, "process.argv does not equal Bun.argv"); +assert(process.argv === process.argv, "process.argv isn't cached"); +// assert(Bun.argv === Bun.argv, 'Bun.argv isn\'t cached'); +// assert(Bun.argv === process.argv, 'Bun.argv doesnt share same ref as process.argv'); + var writer = Bun.stdout.writer(); writer.write(JSON.stringify(process.argv)); await writer.flush(true); diff --git a/test/js/node/process/process-exit-fixture.js b/test/js/node/process/process-exit-fixture.js new file mode 100644 index 000000000..c5a492285 --- /dev/null +++ b/test/js/node/process/process-exit-fixture.js @@ -0,0 +1,16 @@ +process.on("beforeExit", () => { + throw new Error("process.on('beforeExit') called"); +}); + +if (process._exiting) { + throw new Error("process._exiting should be undefined"); +} + +process.on("exit", () => { + if (!process._exiting) { + throw new Error("process.on('exit') called with process._exiting false"); + } + console.log("PASS"); +}); + +process.exit(0); diff --git a/test/js/node/process/process-exitCode-fixture.js b/test/js/node/process/process-exitCode-fixture.js new file mode 100644 index 000000000..2d5182d93 --- /dev/null +++ b/test/js/node/process/process-exitCode-fixture.js @@ -0,0 +1,7 @@ +process.exitCode = Number(process.argv.at(-1)); +process.on("exit", code => { + if (code !== process.exitCode) { + throw new Error("process.exitCode should be " + process.exitCode); + } + console.log("PASS"); +}); diff --git a/test/js/node/process/process-exitCode-with-exit.js b/test/js/node/process/process-exitCode-with-exit.js new file mode 100644 index 000000000..610975bc2 --- /dev/null +++ b/test/js/node/process/process-exitCode-with-exit.js @@ -0,0 +1,8 @@ +process.exitCode = Number(process.argv.at(-1)); +process.on("exit", code => { + if (code !== process.exitCode) { + throw new Error("process.exitCode should be " + process.exitCode); + } + console.log("PASS"); +}); +process.exit(); diff --git a/test/js/node/process/process-onBeforeExit-fixture.js b/test/js/node/process/process-onBeforeExit-fixture.js new file mode 100644 index 000000000..8cbdcebf0 --- /dev/null +++ b/test/js/node/process/process-onBeforeExit-fixture.js @@ -0,0 +1,7 @@ +process.on("beforeExit", () => { + console.log("beforeExit"); +}); + +process.on("exit", () => { + console.log("exit"); +}); diff --git a/test/js/node/process/process-onBeforeExit-keepAlive.js b/test/js/node/process/process-onBeforeExit-keepAlive.js new file mode 100644 index 000000000..45b20b763 --- /dev/null +++ b/test/js/node/process/process-onBeforeExit-keepAlive.js @@ -0,0 +1,18 @@ +let counter = 0; +process.on("beforeExit", () => { + if (process._exiting) { + throw new Error("process._exiting should be undefined"); + } + + console.log("beforeExit:", counter); + if (!counter++) { + setTimeout(() => {}, 1); + } +}); + +process.on("exit", () => { + if (!process._exiting) { + throw new Error("process.on('exit') called with process._exiting false"); + } + console.log("exit:", counter); +}); diff --git a/test/js/node/process/process-signal-handler.fixture.js b/test/js/node/process/process-signal-handler.fixture.js new file mode 100644 index 000000000..de5a78bda --- /dev/null +++ b/test/js/node/process/process-signal-handler.fixture.js @@ -0,0 +1,63 @@ +import os from "os"; +import { raise } from "./call-raise"; + +var counter = 0; +function done() { + counter++; + if (counter === 2) { + setTimeout(() => { + if (counter !== 2) { + console.log(counter); + console.log("FAIL"); + process.exit(1); + } + + console.log("PASS"); + process.exit(0); + }, 1); + } +} + +var counter2 = 0; +function done2() { + counter2++; + if (counter2 === 2) { + setTimeout(() => { + if (counter2 !== 2) { + console.log(counter2); + console.log("FAIL"); + process.exit(1); + } + + console.log("PASS"); + process.exit(0); + }, 1); + } +} + +const SIGUSR1 = os.constants.signals.SIGUSR1; +const SIGUSR2 = os.constants.signals.SIGUSR2; + +switch (process.argv.at(-1)) { + case "SIGUSR1": { + process.on("SIGUSR1", () => { + done(); + }); + process.on("SIGUSR1", () => { + done(); + }); + raise(SIGUSR1); + break; + } + case "SIGUSR2": { + process.on("SIGUSR2", () => { + done2(); + }); + process.emit("SIGUSR2"); + raise(SIGUSR2); + break; + } + default: { + throw new Error("Unknown argument: " + process.argv.at(-1)); + } +} diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index ee181e70c..890f5032e 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -1,8 +1,8 @@ -import { resolveSync, which } from "bun"; +import { spawnSync, which } from "bun"; import { describe, expect, it } from "bun:test"; -import { existsSync, readFileSync, realpathSync } from "fs"; -import { bunExe } from "harness"; -import { basename, resolve } from "path"; +import { existsSync, readFileSync } from "fs"; +import { bunEnv, bunExe } from "harness"; +import { basename, join, resolve } from "path"; it("process", () => { // this property isn't implemented yet but it should at least return a string @@ -162,7 +162,8 @@ it("process.umask()", () => { expect(process.umask()).toBe(orig); }); -const versions = existsSync(import.meta.dir + "/../../src/generated_versions_list.zig"); +const generated_versions_list = join(import.meta.dir, "../../../../src/generated_versions_list.zig"); +const versions = existsSync(generated_versions_list); (versions ? it : it.skip)("process.versions", () => { // Generate a list of all the versions in the versions object // example: @@ -178,7 +179,7 @@ const versions = existsSync(import.meta.dir + "/../../src/generated_versions_lis // pub const c_ares = "0e7a5dee0fbb04080750cf6eabbe89d8bae87faa"; // pub const usockets = "fafc241e8664243fc0c51d69684d5d02b9805134"; const versions = Object.fromEntries( - readFileSync(import.meta.dir + "/../../src/generated_versions_list.zig", "utf8") + readFileSync(generated_versions_list, "utf8") .split("\n") .filter(line => line.startsWith("pub const") && !line.includes("zig") && line.includes(' = "')) .map(line => line.split(" = ")) @@ -226,11 +227,254 @@ it("process.binding", () => { expect(() => process.binding("buffer")).toThrow(); }); -it("process.argv", () => { +it("process.argv in testing", () => { expect(process.argv).toBeInstanceOf(Array); expect(process.argv[0]).toBe(bunExe()); - expect(process.argv).toEqual(Bun.argv); // assert we aren't creating a new process.argv each call expect(process.argv).toBe(process.argv); }); + +describe("process.exitCode", () => { + it("validates int", () => { + expect(() => (process.exitCode = "potato")).toThrow("exitCode must be a number"); + expect(() => (process.exitCode = 1.2)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = NaN)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = Infinity)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = -Infinity)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = -1)).toThrow("exitCode must be between 0 and 127"); + }); + + it("works with implicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exitCode-with-exit.js"), "42"], + env: bunEnv, + }); + expect(exitCode).toBe(42); + expect(stdout.toString().trim()).toBe("PASS"); + }); + + it("works with explicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exitCode-fixture.js"), "42"], + env: bunEnv, + }); + expect(exitCode).toBe(42); + expect(stdout.toString().trim()).toBe("PASS"); + }); +}); + +it("process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exit-fixture.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("PASS"); +}); + +describe("process.onBeforeExit", () => { + it("emitted", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-onBeforeExit-fixture.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("beforeExit\nexit"); + }); + + it("works with explicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-onBeforeExit-keepAlive.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("beforeExit: 0\nbeforeExit: 1\nexit: 2"); + }); +}); + +it("process.memoryUsage", () => { + expect(process.memoryUsage()).toEqual({ + rss: expect.any(Number), + heapTotal: expect.any(Number), + heapUsed: expect.any(Number), + external: expect.any(Number), + arrayBuffers: expect.any(Number), + }); +}); + +it("process.memoryUsage.rss", () => { + expect(process.memoryUsage.rss()).toEqual(expect.any(Number)); +}); + +describe("process.cpuUsage", () => { + it("works", () => { + expect(process.cpuUsage()).toEqual({ + user: expect.any(Number), + system: expect.any(Number), + }); + }); + + it("works with diff", () => { + const init = process.cpuUsage(); + for (let i = 0; i < 1000; i++) {} + const delta = process.cpuUsage(init); + expect(delta.user).toBeGreaterThan(0); + expect(delta.system).toBeGreaterThan(0); + }); + + it("works with diff of different structure", () => { + const init = { + user: 0, + system: 0, + }; + for (let i = 0; i < 1000; i++) {} + const delta = process.cpuUsage(init); + expect(delta.user).toBeGreaterThan(0); + expect(delta.system).toBeGreaterThan(0); + }); + + it("throws on invalid property", () => { + const fixtures = [ + {}, + { user: null }, + { user: {} }, + { user: "potato" }, + + { user: 123 }, + { user: 123, system: null }, + { user: 123, system: "potato" }, + ]; + for (const fixture of fixtures) { + expect(() => process.cpuUsage(fixture)).toThrow(); + } + }); + + // Skipped on Linux because it seems to not change as often as on macOS + it.skipIf(process.platform === "linux")("increases monotonically", () => { + const init = process.cpuUsage(); + for (let i = 0; i < 10000; i++) {} + const another = process.cpuUsage(); + expect(another.user).toBeGreaterThan(init.user); + expect(another.system).toBeGreaterThan(init.system); + }); +}); + +it("process.getegid", () => { + expect(typeof process.getegid()).toBe("number"); +}); +it("process.geteuid", () => { + expect(typeof process.geteuid()).toBe("number"); +}); +it("process.getgid", () => { + expect(typeof process.getgid()).toBe("number"); +}); +it("process.getgroups", () => { + expect(process.getgroups()).toBeInstanceOf(Array); + expect(process.getgroups().length).toBeGreaterThan(0); +}); +it("process.getuid", () => { + expect(typeof process.getuid()).toBe("number"); +}); + +it("process.getuid", () => { + expect(typeof process.getuid()).toBe("number"); +}); + +describe("signal", () => { + const fixture = join(import.meta.dir, "./process-signal-handler.fixture.js"); + it("simple case works", async () => { + const child = Bun.spawn({ + cmd: [bunExe(), fixture, "SIGUSR1"], + env: bunEnv, + }); + + expect(await child.exited).toBe(0); + expect(await new Response(child.stdout).text()).toBe("PASS\n"); + }); + it("process.emit will call signal events", async () => { + const child = Bun.spawn({ + cmd: [bunExe(), fixture, "SIGUSR2"], + env: bunEnv, + }); + + expect(await child.exited).toBe(0); + expect(await new Response(child.stdout).text()).toBe("PASS\n"); + }); + + it("process.kill(2) works", async () => { + const child = Bun.spawn({ + cmd: ["bash", "-c", "sleep 1000000"], + stdout: "pipe", + }); + const prom = child.exited; + process.kill(child.pid, "SIGTERM"); + await prom; + expect(child.signalCode).toBe("SIGTERM"); + }); + + it("process._kill(2) works", async () => { + const child = Bun.spawn({ + cmd: ["bash", "-c", "sleep 1000000"], + stdout: "pipe", + }); + const prom = child.exited; + process.kill(child.pid, 9); + await prom; + expect(child.signalCode).toBe("SIGKILL"); + }); + + it("process.kill(2) throws on invalid input", async () => { + expect(() => process.kill(0, "SIGPOOP")).toThrow(); + expect(() => process.kill(0, 456)).toThrow(); + }); +}); + +const undefinedStubs = [ + "_debugEnd", + "_debugProcess", + "_fatalException", + "_linkedBinding", + "_rawDebug", + "_startProfilerIdleNotifier", + "_stopProfilerIdleNotifier", + "_tickCallback", +]; + +for (const stub of undefinedStubs) { + it(`process.${stub}`, () => { + expect(process[stub]()).toBeUndefined(); + }); +} + +const arrayStubs = ["getActiveResourcesInfo", "_getActiveRequests", "_getActiveHandles"]; + +for (const stub of arrayStubs) { + it(`process.${stub}`, () => { + expect(process[stub]()).toBeInstanceOf(Array); + }); +} + +const emptyObjectStubs = ["_preload_modules"]; +const emptySetStubs = ["allowedNodeEnvironmentFlags"]; +const emptyArrayStubs = ["moduleLoadList"]; + +for (const stub of emptyObjectStubs) { + it(`process.${stub}`, () => { + expect(process[stub]).toEqual({}); + }); +} + +for (const stub of emptySetStubs) { + it(`process.${stub}`, () => { + expect(process[stub]).toBeInstanceOf(Set); + expect(process[stub].size).toBe(0); + }); +} + +for (const stub of emptyArrayStubs) { + it(`process.${stub}`, () => { + expect(process[stub]).toBeInstanceOf(Array); + expect(process[stub]).toHaveLength(0); + }); +} diff --git a/test/js/node/string_decoder/string-decoder.test.js b/test/js/node/string_decoder/string-decoder.test.js index f37326678..aba73401a 100644 --- a/test/js/node/string_decoder/string-decoder.test.js +++ b/test/js/node/string_decoder/string-decoder.test.js @@ -241,3 +241,12 @@ for (const StringDecoder of [FakeStringDecoderCall, RealStringDecoder]) { }); }); } + +it("invalid utf-8 input, pr #3562", () => { + const decoder = new RealStringDecoder("utf-8"); + let output = ""; + output += decoder.write(Buffer.from("B9", "hex")); + output += decoder.write(Buffer.from("A9", "hex")); + output += decoder.end(); + expect(output).toStrictEqual("\uFFFD\uFFFD"); +}); diff --git a/test/js/node/stubs.test.js b/test/js/node/stubs.test.js index e6bce8aee..1025907ab 100644 --- a/test/js/node/stubs.test.js +++ b/test/js/node/stubs.test.js @@ -99,6 +99,9 @@ for (let specifier of specifiers) { } } } + } else { + // TODO: uncomment this after node:module can be default imported + // throw new Error(`Module ${specifier} has no default export`); } }); } diff --git a/test/js/node/timers/node-timers.test.ts b/test/js/node/timers/node-timers.test.ts index e6fa48010..412eabc22 100644 --- a/test/js/node/timers/node-timers.test.ts +++ b/test/js/node/timers/node-timers.test.ts @@ -1,17 +1,18 @@ import { describe, test } from "bun:test"; -import { setTimeout, clearTimeout, setInterval, setImmediate } from "node:timers"; +import { setTimeout, clearTimeout, setInterval, clearInterval, setImmediate } from "node:timers"; -for (const fn of [setTimeout, setInterval, setImmediate]) { +for (const fn of [setTimeout, setInterval]) { describe(fn.name, () => { test("unref is possible", done => { const timer = fn(() => { done(new Error("should not be called")); - }, 1); - fn(() => { + }, 1).unref(); + const other = fn(() => { + clearInterval(other); done(); }, 2); - timer.unref(); - if (fn !== setImmediate) clearTimeout(timer); + if (fn === setTimeout) clearTimeout(timer); + if (fn === setInterval) clearInterval(timer); }); }); } diff --git a/test/js/node/tls/node-tls-connect.test.ts b/test/js/node/tls/node-tls-connect.test.ts new file mode 100644 index 000000000..791dba88a --- /dev/null +++ b/test/js/node/tls/node-tls-connect.test.ts @@ -0,0 +1,32 @@ +import { TLSSocket, connect } from "tls"; + +it("should work with alpnProtocols", done => { + try { + let socket: TLSSocket | null = connect({ + ALPNProtocols: ["http/1.1"], + host: "bun.sh", + servername: "bun.sh", + port: 443, + rejectUnauthorized: false, + }); + + const timeout = setTimeout(() => { + socket?.end(); + done("timeout"); + }, 3000); + + socket.on("error", err => { + clearTimeout(timeout); + done(err); + }); + + socket.on("secureConnect", () => { + clearTimeout(timeout); + done(socket?.alpnProtocol === "http/1.1" ? undefined : "alpnProtocol is not http/1.1"); + socket?.end(); + socket = null; + }); + } catch (err) { + done(err); + } +}); diff --git a/test/js/node/tls/node-tls-server.test.ts b/test/js/node/tls/node-tls-server.test.ts index 6879d0927..2a6101b9f 100644 --- a/test/js/node/tls/node-tls-server.test.ts +++ b/test/js/node/tls/node-tls-server.test.ts @@ -195,61 +195,6 @@ describe("tls.createServer listen", () => { ); }); - it("should listen on the correct port", done => { - const { mustCall, mustNotCall } = createCallCheckCtx(done); - - const server: Server = createServer(COMMON_CERT); - - let timeout: Timer; - const closeAndFail = () => { - clearTimeout(timeout); - server.close(); - mustNotCall()(); - }; - server.on("error", closeAndFail); - timeout = setTimeout(closeAndFail, 100); - - server.listen( - 49027, - mustCall(() => { - const address = server.address() as AddressInfo; - expect(address.address).toStrictEqual("::"); - expect(address.port).toStrictEqual(49027); - expect(address.family).toStrictEqual("IPv6"); - server.close(); - done(); - }), - ); - }); - - it("should listen on the correct port with IPV4", done => { - const { mustCall, mustNotCall } = createCallCheckCtx(done); - - const server: Server = createServer(COMMON_CERT); - - let timeout: Timer; - const closeAndFail = () => { - clearTimeout(timeout); - server.close(); - mustNotCall()(); - }; - server.on("error", closeAndFail); - timeout = setTimeout(closeAndFail, 100); - - server.listen( - 49026, - "0.0.0.0", - mustCall(() => { - const address = server.address() as AddressInfo; - expect(address.address).toStrictEqual("0.0.0.0"); - expect(address.port).toStrictEqual(49026); - expect(address.family).toStrictEqual("IPv4"); - server.close(); - done(); - }), - ); - }); - it("should listen on unix domain socket", done => { const { mustCall, mustNotCall } = createCallCheckCtx(done); diff --git a/test/js/node/util/test-util-types.test.js b/test/js/node/util/test-util-types.test.js index f33ab4b1a..a75b9eac0 100644 --- a/test/js/node/util/test-util-types.test.js +++ b/test/js/node/util/test-util-types.test.js @@ -1,6 +1,9 @@ -const assert = require("assert"); -import { test, expect } from "bun:test"; -const types = require("util/types"); +import assert from "assert"; +import { describe, test, expect } from "bun:test"; +import def from "util/types"; +import * as ns from "util/types"; +const req = require("util/types"); +const types = def; function inspect(val) { return Bun.inspect(val); @@ -52,15 +55,21 @@ for (const [value, _method] of [ assert(method in types, `Missing ${method} for ${inspect(value)}`); assert(types[method](value), `Want ${inspect(value)} to match ${method}`); - for (const key of Object.keys(types)) { - if ( - ((types.isArrayBufferView(value) || types.isAnyArrayBuffer(value)) && key.includes("Array")) || - key === "isBoxedPrimitive" - ) { - continue; - } + for (const [types, label] of [ + [def, "default import"], + [ns, "ns import"], + [req, "require esm"], + ]) { + for (const key of Object.keys(types).filter(x => x !== "default")) { + if ( + ((types.isArrayBufferView(value) || types.isAnyArrayBuffer(value)) && key.includes("Array")) || + key === "isBoxedPrimitive" + ) { + continue; + } - expect(types[key](value)).toBe(key === method); + expect(types[key](value)).toBe(key === method); + } } }); } @@ -238,3 +247,4 @@ test("isBoxedPrimitive", () => { }); } } +// */ diff --git a/test/js/node/util/util-callbackify.test.js b/test/js/node/util/util-callbackify.test.js new file mode 100644 index 000000000..38c20f5c0 --- /dev/null +++ b/test/js/node/util/util-callbackify.test.js @@ -0,0 +1,323 @@ +import { callbackify } from "util"; +import { createTest } from "node-harness"; + +const { describe, expect, it, createCallCheckCtx } = createTest(import.meta.path); + +const values = [ + "hello world", + null, + undefined, + false, + 0, + {}, + { key: "value" }, + Symbol("I am a symbol"), + function ok() {}, + ["array", "with", 4, "values"], + new Error("boo"), +]; + +describe("util.callbackify", () => { + describe("rejection reason", () => { + for (const value of values) { + it(`callback is async function, value is ${String(value)}`, done => { + const { mustCall } = createCallCheckCtx(done); + async function asyncFn() { + return Promise.reject(value); + } + + const cbAsyncFn = callbackify(asyncFn); + cbAsyncFn( + mustCall((err, ret) => { + try { + expect(ret).toBeUndefined(); + if (err instanceof Error) { + if ("reason" in err) { + expect(!value).toBeTrue(); + expect(err.code).toStrictEqual("ERR_FALSY_VALUE_REJECTION"); + expect(err.reason).toStrictEqual(value); + } else { + expect(String(value)).toEndWith(err.message); + } + } else { + expect(err).toStrictEqual(value); + } + + done(); + } catch (error) { + done(error); + } + }), + ); + }); + + it(`callback is promise, value is ${String(value)}`, done => { + const { mustCall } = createCallCheckCtx(done); + function promiseFn() { + return Promise.reject(value); + } + const obj = {}; + Object.defineProperty(promiseFn, "name", { + value: obj, + writable: false, + enumerable: false, + configurable: true, + }); + + const cbPromiseFn = callbackify(promiseFn); + try { + expect(promiseFn.name).toStrictEqual(obj); + } catch (error) { + done(error); + } + + cbPromiseFn( + mustCall((err, ret) => { + try { + expect(ret).toBeUndefined(); + if (err instanceof Error) { + if ("reason" in err) { + expect(!value).toBeTrue(); + expect(err.code).toStrictEqual("ERR_FALSY_VALUE_REJECTION"); + expect(err.reason).toStrictEqual(value); + } else { + expect(String(value)).toEndWith(err.message); + } + } else { + expect(err).toStrictEqual(value); + } + + done(); + } catch (error) { + done(error); + } + }), + ); + }); + + it(`callback is thenable, value is ${String(value)}`, done => { + const { mustCall } = createCallCheckCtx(done); + function thenableFn() { + return { + then(onRes, onRej) { + onRej(value); + }, + }; + } + + const cbThenableFn = callbackify(thenableFn); + cbThenableFn( + mustCall((err, ret) => { + try { + expect(ret).toBeUndefined(); + if (err instanceof Error) { + if ("reason" in err) { + expect(!value).toBeTrue(); + expect(err.code).toStrictEqual("ERR_FALSY_VALUE_REJECTION"); + expect(err.reason).toStrictEqual(value); + } else { + expect(String(value)).toEndWith(err.message); + } + } else { + expect(err).toStrictEqual(value); + } + + done(); + } catch (error) { + done(error); + } + }), + ); + }); + } + }); + + describe("return value", () => { + for (const value of values) { + it(`callback is async function, value is ${String(value)}`, done => { + const { mustSucceed } = createCallCheckCtx(done); + async function asyncFn() { + return value; + } + + const cbAsyncFn = callbackify(asyncFn); + cbAsyncFn( + mustSucceed(ret => { + try { + expect(ret).toStrictEqual(value); + expect(ret).toStrictEqual(value); + + done(); + } catch (error) { + done(error); + } + }), + ); + }); + + it(`callback is promise, value is ${String(value)}`, done => { + const { mustSucceed } = createCallCheckCtx(done); + function promiseFn() { + return Promise.resolve(value); + } + + const cbPromiseFn = callbackify(promiseFn); + cbPromiseFn( + mustSucceed(ret => { + try { + expect(ret).toStrictEqual(value); + done(); + } catch (error) { + done(error); + } + }), + ); + }); + + it(`callback is thenable, value is ${String(value)}`, done => { + const { mustSucceed } = createCallCheckCtx(done); + function thenableFn() { + return { + then(onRes, onRej) { + onRes(value); + }, + }; + } + + const cbThenableFn = callbackify(thenableFn); + cbThenableFn( + mustSucceed(ret => { + try { + expect(ret).toStrictEqual(value); + done(); + } catch (error) { + done(error); + } + }), + ); + }); + } + }); + + describe("arguments", () => { + for (const value of values) { + it(`callback is async function, value is ${String(value)}`, done => { + const { mustSucceed } = createCallCheckCtx(done); + async function asyncFn(arg) { + try { + expect(arg).toStrictEqual(value); + } catch (error) { + done(error); + } + return arg; + } + + const cbAsyncFn = callbackify(asyncFn); + cbAsyncFn( + value, + mustSucceed(ret => { + try { + expect(ret).toStrictEqual(value); + done(); + } catch (error) { + done(error); + } + }), + ); + }); + + it(`callback is promise, value is ${String(value)}`, done => { + const { mustSucceed } = createCallCheckCtx(done); + function promiseFn(arg) { + try { + expect(arg).toStrictEqual(value); + } catch (error) { + done(error); + } + + return Promise.resolve(arg); + } + const obj = {}; + Object.defineProperty(promiseFn, "length", { + value: obj, + writable: false, + enumerable: false, + configurable: true, + }); + const cbPromiseFn = callbackify(promiseFn); + try { + expect(promiseFn.length).toStrictEqual(obj); + } catch (error) { + done(error); + } + + cbPromiseFn( + value, + mustSucceed(ret => { + try { + expect(ret).toStrictEqual(value); + done(); + } catch (error) { + done(error); + } + }), + ); + }); + } + }); + + describe("this binding", () => { + const value = "hello world"; + it("callback is sync function", done => { + // TODO: + // const { mustSucceed } = createCallCheckCtx(done); + const iAmThis = { + fn(arg) { + try { + expect(this).toStrictEqual(iAmThis); + } catch (error) { + done(error); + } + return Promise.resolve(arg); + }, + }; + + iAmThis.cbFn = callbackify(iAmThis.fn); + iAmThis.cbFn(value, function (rej, ret) { + try { + expect(ret).toStrictEqual(value); + expect(this).toStrictEqual(iAmThis); + + done(); + } catch (error) { + done(error); + } + }); + }); + + it("callback is async function", done => { + const iAmThis = { + async fn(arg) { + try { + expect(this).toStrictEqual(iAmThis); + } catch (error) { + done(error); + } + return Promise.resolve(arg); + }, + }; + + iAmThis.cbFn = callbackify(iAmThis.fn); + iAmThis.cbFn(value, function (rej, ret) { + try { + expect(ret).toStrictEqual(value); + expect(this).toStrictEqual(iAmThis); + + done(); + } catch (error) { + done(error); + } + }); + }); + }); +}); diff --git a/test/js/node/util/util.test.js b/test/js/node/util/util.test.js index 45ecffda8..741b27d19 100644 --- a/test/js/node/util/util.test.js +++ b/test/js/node/util/util.test.js @@ -36,6 +36,23 @@ const deepStrictEqual = (...args) => { // Tests adapted from https://github.com/nodejs/node/blob/main/test/parallel/test-util.js describe("util", () => { + it("toUSVString", () => { + const strings = [ + // Lone high surrogate + "ab\uD800", + "ab\uD800c", + // Lone low surrogate + "\uDFFFab", + "c\uDFFFab", + // Well-formed + "abc", + "ab\uD83D\uDE04c", + ]; + const outputs = ["ab�", "ab�c", "�ab", "c�ab", "abc", "ab😄c"]; + for (let i = 0; i < strings.length; i++) { + expect(util.toUSVString(strings[i])).toBe(outputs[i]); + } + }); describe("isArray", () => { it("all cases", () => { strictEqual(util.isArray([]), true); diff --git a/test/js/node/v8/capture-stack-trace.test.js b/test/js/node/v8/capture-stack-trace.test.js index d96f91483..cb2624681 100644 --- a/test/js/node/v8/capture-stack-trace.test.js +++ b/test/js/node/v8/capture-stack-trace.test.js @@ -5,6 +5,18 @@ afterEach(() => { Error.prepareStackTrace = origPrepareStackTrace; }); +test("Regular .stack", () => { + var err; + class Foo { + constructor() { + err = new Error("wat"); + } + } + + new Foo(); + expect(err.stack).toMatch(/at new Foo/); +}); + test("capture stack trace", () => { function f1() { f2(); diff --git a/test/js/node/watch/fixtures/close.js b/test/js/node/watch/fixtures/close.js new file mode 100644 index 000000000..8eeeb79a3 --- /dev/null +++ b/test/js/node/watch/fixtures/close.js @@ -0,0 +1,7 @@ +import fs from "fs"; +fs.watch(import.meta.path, { signal: AbortSignal.timeout(4000) }) + .on("error", err => { + console.error(err.message); + process.exit(1); + }) + .close(); diff --git a/test/js/node/watch/fixtures/persistent.js b/test/js/node/watch/fixtures/persistent.js new file mode 100644 index 000000000..72a2b6564 --- /dev/null +++ b/test/js/node/watch/fixtures/persistent.js @@ -0,0 +1,5 @@ +import fs from "fs"; +fs.watch(import.meta.path, { persistent: false, signal: AbortSignal.timeout(4000) }).on("error", err => { + console.error(err.message); + process.exit(1); +}); diff --git a/test/js/node/watch/fixtures/relative.js b/test/js/node/watch/fixtures/relative.js new file mode 100644 index 000000000..26e09da1a --- /dev/null +++ b/test/js/node/watch/fixtures/relative.js @@ -0,0 +1,23 @@ +import fs from "fs"; +const watcher = fs.watch("relative.txt", { signal: AbortSignal.timeout(2000) }); + +watcher.on("change", function (event, filename) { + if (filename !== "relative.txt" && event !== "change") { + console.error("fail"); + clearInterval(interval); + watcher.close(); + process.exit(1); + } else { + clearInterval(interval); + watcher.close(); + } +}); +watcher.on("error", err => { + clearInterval(interval); + console.error(err.message); + process.exit(1); +}); + +const interval = setInterval(() => { + fs.writeFileSync("relative.txt", "world"); +}, 10); diff --git a/test/js/node/watch/fixtures/unref.js b/test/js/node/watch/fixtures/unref.js new file mode 100644 index 000000000..a0c506a04 --- /dev/null +++ b/test/js/node/watch/fixtures/unref.js @@ -0,0 +1,7 @@ +import fs from "fs"; +fs.watch(import.meta.path, { signal: AbortSignal.timeout(4000) }) + .on("error", err => { + console.error(err.message); + process.exit(1); + }) + .unref(); diff --git a/test/js/node/watch/fs.watch.test.ts b/test/js/node/watch/fs.watch.test.ts new file mode 100644 index 000000000..aa7959bed --- /dev/null +++ b/test/js/node/watch/fs.watch.test.ts @@ -0,0 +1,522 @@ +import fs, { FSWatcher } from "node:fs"; +import path from "path"; +import { tempDirWithFiles, bunRun, bunRunAsScript } from "harness"; +import { pathToFileURL } from "bun"; + +import { describe, expect, test } from "bun:test"; +// Because macOS (and possibly other operating systems) can return a watcher +// before it is actually watching, we need to repeat the operation to avoid +// a race condition. +function repeat(fn: any) { + const interval = setInterval(fn, 20); + return interval; +} +const encodingFileName = `新建文夹件.txt`; +const testDir = tempDirWithFiles("watch", { + "watch.txt": "hello", + "relative.txt": "hello", + "abort.txt": "hello", + "url.txt": "hello", + "close.txt": "hello", + "close-close.txt": "hello", + "sym-sync.txt": "hello", + "sym.txt": "hello", + [encodingFileName]: "hello", +}); + +describe("fs.watch", () => { + test("non-persistent watcher should not block the event loop", done => { + try { + // https://github.com/joyent/node/issues/2293 - non-persistent watcher should not block the event loop + bunRun(path.join(import.meta.dir, "fixtures", "persistent.js")); + done(); + } catch (e: any) { + done(e); + } + }); + + test("watcher should close and not block the event loop", done => { + try { + bunRun(path.join(import.meta.dir, "fixtures", "close.js")); + done(); + } catch (e: any) { + done(e); + } + }); + + test("unref watcher should not block the event loop", done => { + try { + bunRun(path.join(import.meta.dir, "fixtures", "unref.js")); + done(); + } catch (e: any) { + done(e); + } + }); + + test("should work with relative files", done => { + try { + bunRunAsScript(testDir, path.join(import.meta.dir, "fixtures", "relative.js")); + done(); + } catch (e: any) { + done(e); + } + }); + + test("add file/folder to folder", done => { + let count = 0; + const root = path.join(testDir, "add-directory"); + try { + fs.mkdirSync(root); + } catch {} + let err: Error | undefined = undefined; + const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); + watcher.on("change", (event, filename) => { + count++; + try { + expect(event).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(filename); + if (count >= 2) { + watcher.close(); + } + } catch (e: any) { + err = e; + watcher.close(); + } + }); + + watcher.on("error", e => (err = e)); + watcher.on("close", () => { + clearInterval(interval); + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(root, "new-folder.txt")); + fs.rmdirSync(path.join(root, "new-folder.txt")); + }); + }); + + test("add file/folder to subfolder", done => { + let count = 0; + const root = path.join(testDir, "add-subdirectory"); + try { + fs.mkdirSync(root); + } catch {} + const subfolder = path.join(root, "subfolder"); + fs.mkdirSync(subfolder); + const watcher = fs.watch(root, { recursive: true, signal: AbortSignal.timeout(3000) }); + let err: Error | undefined = undefined; + watcher.on("change", (event, filename) => { + const basename = path.basename(filename as string); + + if (basename === "subfolder") return; + count++; + try { + expect(event).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(basename); + if (count >= 2) { + watcher.close(); + } + } catch (e: any) { + err = e; + watcher.close(); + } + }); + watcher.on("error", e => (err = e)); + watcher.on("close", () => { + clearInterval(interval); + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(subfolder, "new-folder.txt")); + fs.rmdirSync(path.join(subfolder, "new-folder.txt")); + }); + }); + + test("should emit event when file is deleted", done => { + const testsubdir = tempDirWithFiles("subdir", { + "deleted.txt": "hello", + }); + const filepath = path.join(testsubdir, "deleted.txt"); + let err: Error | undefined = undefined; + const watcher = fs.watch(testsubdir, function (event, filename) { + try { + expect(event).toBe("rename"); + expect(filename).toBe("deleted.txt"); + } catch (e: any) { + err = e; + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + watcher.once("close", () => { + done(err); + }); + + const interval = repeat(() => { + fs.rmSync(filepath, { force: true }); + const fd = fs.openSync(filepath, "w"); + fs.closeSync(fd); + }); + }); + + test("should emit 'change' event when file is modified", done => { + const filepath = path.join(testDir, "watch.txt"); + + const watcher = fs.watch(filepath); + let err: Error | undefined = undefined; + watcher.on("change", function (event, filename) { + try { + expect(event).toBe("change"); + expect(filename).toBe("watch.txt"); + } catch (e: any) { + err = e; + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + watcher.once("close", () => { + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + }); + + test("should error on invalid path", done => { + try { + fs.watch(path.join(testDir, "404.txt")); + done(new Error("should not reach here")); + } catch (err: any) { + expect(err).toBeInstanceOf(Error); + expect(err.code).toBe("ENOENT"); + expect(err.syscall).toBe("watch"); + done(); + } + }); + + const encodings = ["utf8", "buffer", "hex", "ascii", "base64", "utf16le", "ucs2", "latin1", "binary"] as const; + + test(`should work with encodings ${encodings.join(", ")}`, async () => { + const watchers: FSWatcher[] = []; + const filepath = path.join(testDir, encodingFileName); + + const promises: Promise<any>[] = []; + encodings.forEach(name => { + const encoded_filename = + name !== "buffer" ? Buffer.from(encodingFileName, "utf8").toString(name) : Buffer.from(encodingFileName); + + promises.push( + new Promise((resolve, reject) => { + watchers.push( + fs.watch(filepath, { encoding: name }, (event, filename) => { + try { + expect(event).toBe("change"); + + if (name !== "buffer") { + expect(filename).toBe(encoded_filename); + } else { + expect(filename).toBeInstanceOf(Buffer); + expect((filename as any as Buffer)!.toString("utf8")).toBe(encodingFileName); + } + + resolve(undefined); + } catch (e: any) { + reject(e); + } + }), + ); + }), + ); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + + try { + await Promise.all(promises); + } finally { + clearInterval(interval); + watchers.forEach(watcher => watcher.close()); + } + }); + + test("should work with url", done => { + const filepath = path.join(testDir, "url.txt"); + try { + const watcher = fs.watch(pathToFileURL(filepath)); + let err: Error | undefined = undefined; + watcher.on("change", function (event, filename) { + try { + expect(event).toBe("change"); + expect(filename).toBe("url.txt"); + } catch (e: any) { + err = e; + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + watcher.once("close", () => { + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + } catch (e: any) { + done(e); + } + }); + + test("calling close from error event should not throw", done => { + const filepath = path.join(testDir, "close.txt"); + try { + const ac = new AbortController(); + const watcher = fs.watch(pathToFileURL(filepath), { signal: ac.signal }); + watcher.once("error", () => { + try { + watcher.close(); + done(); + } catch (e: any) { + done("Should not error when calling close from error event"); + } + }); + ac.abort(); + } catch (e: any) { + done(e); + } + }); + + test("calling close from close event should not throw", done => { + const filepath = path.join(testDir, "close-close.txt"); + try { + const ac = new AbortController(); + const watcher = fs.watch(pathToFileURL(filepath), { signal: ac.signal }); + + watcher.once("close", () => { + try { + watcher.close(); + done(); + } catch (e: any) { + done("Should not error when calling close from close event"); + } + }); + + ac.abort(); + } catch (e: any) { + done(e); + } + }); + + test("Signal aborted after creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const ac = new AbortController(); + const promise = new Promise((resolve, reject) => { + const watcher = fs.watch(filepath, { signal: ac.signal }); + watcher.once("error", err => (err.message === "The operation was aborted." ? resolve(undefined) : reject(err))); + watcher.once("close", () => reject()); + }); + await Bun.sleep(10); + ac.abort(); + await promise; + }); + + test("Signal aborted before creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const signal = AbortSignal.abort(); + await new Promise((resolve, reject) => { + const watcher = fs.watch(filepath, { signal }); + watcher.once("error", err => (err.message === "The operation was aborted." ? resolve(undefined) : reject(err))); + watcher.once("close", () => reject()); + }); + }); + + test("should work with symlink", async () => { + const filepath = path.join(testDir, "sym-symlink2.txt"); + await fs.promises.symlink(path.join(testDir, "sym-sync.txt"), filepath); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "hello"); + }); + + const promise = new Promise((resolve, reject) => { + let timeout: any = null; + const watcher = fs.watch(filepath, event => { + clearTimeout(timeout); + clearInterval(interval); + try { + resolve(event); + } catch (e: any) { + reject(e); + } finally { + watcher.close(); + } + }); + setTimeout(() => { + clearInterval(interval); + watcher?.close(); + reject("timeout"); + }, 3000); + }); + expect(promise).resolves.toBe("change"); + }); +}); + +describe("fs.promises.watch", () => { + test("add file/folder to folder", async () => { + let count = 0; + const root = path.join(testDir, "add-promise-directory"); + try { + fs.mkdirSync(root); + } catch {} + let success = false; + let err: Error | undefined = undefined; + try { + const ac = new AbortController(); + const watcher = fs.promises.watch(root, { signal: ac.signal }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(root, "new-folder.txt")); + fs.rmdirSync(path.join(root, "new-folder.txt")); + }); + + for await (const event of watcher) { + count++; + try { + expect(event.eventType).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(event.filename); + + if (count >= 2) { + success = true; + clearInterval(interval); + ac.abort(); + } + } catch (e: any) { + err = e; + clearInterval(interval); + ac.abort(); + } + } + } catch (e: any) { + if (!success) { + throw err || e; + } + } + }); + + test("add file/folder to subfolder", async () => { + let count = 0; + const root = path.join(testDir, "add-promise-subdirectory"); + try { + fs.mkdirSync(root); + } catch {} + const subfolder = path.join(root, "subfolder"); + fs.mkdirSync(subfolder); + let success = false; + let err: Error | undefined = undefined; + + try { + const ac = new AbortController(); + const watcher = fs.promises.watch(root, { recursive: true, signal: ac.signal }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(subfolder, "new-folder.txt")); + fs.rmdirSync(path.join(subfolder, "new-folder.txt")); + }); + for await (const event of watcher) { + const basename = path.basename(event.filename!); + if (basename === "subfolder") continue; + + count++; + try { + expect(event.eventType).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(basename); + + if (count >= 2) { + success = true; + clearInterval(interval); + ac.abort(); + } + } catch (e: any) { + err = e; + clearInterval(interval); + ac.abort(); + } + } + } catch (e: any) { + if (!success) { + throw err || e; + } + } + }); + + test("Signal aborted after creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const ac = new AbortController(); + const watcher = fs.promises.watch(filepath, { signal: ac.signal }); + + const promise = (async () => { + try { + for await (const _ of watcher); + } catch (e: any) { + expect(e.message).toBe("The operation was aborted."); + } + })(); + await Bun.sleep(10); + ac.abort(); + await promise; + }); + + test("Signal aborted before creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const signal = AbortSignal.abort(); + const watcher = fs.promises.watch(filepath, { signal }); + await (async () => { + try { + for await (const _ of watcher); + } catch (e: any) { + expect(e.message).toBe("The operation was aborted."); + } + })(); + }); + + test("should work with symlink", async () => { + const filepath = path.join(testDir, "sym-symlink.txt"); + await fs.promises.symlink(path.join(testDir, "sym.txt"), filepath); + + const watcher = fs.promises.watch(filepath); + const interval = repeat(() => { + fs.writeFileSync(filepath, "hello"); + }); + + const promise = (async () => { + try { + for await (const event of watcher) { + return event.eventType; + } + } catch (e: any) { + expect("unreacheable").toBe(false); + } finally { + clearInterval(interval); + } + })(); + expect(promise).resolves.toBe("change"); + }); +}); diff --git a/test/js/third_party/esbuild/bun.lockb b/test/js/third_party/esbuild/bun.lockb Binary files differindex f46c6aa5d..1fcce3837 100755 --- a/test/js/third_party/esbuild/bun.lockb +++ b/test/js/third_party/esbuild/bun.lockb diff --git a/test/js/third_party/esbuild/package.json b/test/js/third_party/esbuild/package.json index ad6bc55f6..46535bc29 100644 --- a/test/js/third_party/esbuild/package.json +++ b/test/js/third_party/esbuild/package.json @@ -1,6 +1,6 @@ { "type": "module", "dependencies": { - "esbuild": "^0.17.11" + "esbuild": "0.17.11" } -}
\ No newline at end of file +} diff --git a/test/js/third_party/nodemailer/nodemailer.test.ts b/test/js/third_party/nodemailer/nodemailer.test.ts new file mode 100644 index 000000000..36a229524 --- /dev/null +++ b/test/js/third_party/nodemailer/nodemailer.test.ts @@ -0,0 +1,19 @@ +import { test, expect, describe } from "bun:test"; +import { bunRun } from "harness"; +import path from "path"; + +const it = process.env.SMTP_SENDGRID_KEY && process.env.SMTP_SENDGRID_SENDER ? test : test.skip; +describe("nodemailer", () => { + it("basic smtp", async () => { + try { + const info = bunRun(path.join(import.meta.dir, "process-nodemailer-fixture.js"), { + SMTP_SENDGRID_SENDER: process.env.SMTP_SENDGRID_SENDER as string, + SMTP_SENDGRID_KEY: process.env.SMTP_SENDGRID_KEY as string, + }); + expect(info.stdout).toBe("true"); + expect(info.stderr || "").toBe(""); + } catch (err: any) { + expect(err?.message || err).toBe(""); + } + }, 10000); +}); diff --git a/test/js/third_party/nodemailer/package.json b/test/js/third_party/nodemailer/package.json new file mode 100644 index 000000000..08e98074f --- /dev/null +++ b/test/js/third_party/nodemailer/package.json @@ -0,0 +1,6 @@ +{ + "name": "nodemailer", + "dependencies": { + "nodemailer": "6.9.3" + } +} diff --git a/test/js/third_party/nodemailer/process-nodemailer-fixture.js b/test/js/third_party/nodemailer/process-nodemailer-fixture.js new file mode 100644 index 000000000..49ab6a516 --- /dev/null +++ b/test/js/third_party/nodemailer/process-nodemailer-fixture.js @@ -0,0 +1,20 @@ +const nodemailer = require("nodemailer"); +const transporter = nodemailer.createTransport({ + host: "smtp.sendgrid.net", + port: 587, + secure: false, + auth: { + user: "apikey", // generated ethereal user + pass: process.env.SMTP_SENDGRID_KEY, // generated ethereal password + }, +}); + +// send mail with defined transport object +let info = await transporter.sendMail({ + from: process.env.SMTP_SENDGRID_SENDER, // sender address + to: process.env.SMTP_SENDGRID_SENDER, // list of receivers + subject: "Hello ✔", // Subject line + text: "Hello world?", // plain text body + html: "<b>Hello world?</b>", // html body +}); +console.log(typeof info?.messageId === "string"); diff --git a/test/js/third_party/postgres/package.json b/test/js/third_party/postgres/package.json new file mode 100644 index 000000000..90809f48f --- /dev/null +++ b/test/js/third_party/postgres/package.json @@ -0,0 +1,8 @@ +{ + "name": "postgres", + "dependencies": { + "pg": "8.11.1", + "postgres": "3.3.5", + "pg-connection-string": "2.6.1" + } +} diff --git a/test/js/third_party/postgres/postgres.test.ts b/test/js/third_party/postgres/postgres.test.ts new file mode 100644 index 000000000..490192ae7 --- /dev/null +++ b/test/js/third_party/postgres/postgres.test.ts @@ -0,0 +1,56 @@ +import { test, expect, describe } from "bun:test"; +import { Pool } from "pg"; +import { parse } from "pg-connection-string"; +import postgres from "postgres"; + +const CONNECTION_STRING = process.env.TLS_POSTGRES_DATABASE_URL; + +const it = CONNECTION_STRING ? test : test.skip; + +describe("pg", () => { + it("should connect using TLS", async () => { + const pool = new Pool(parse(CONNECTION_STRING as string)); + try { + const { rows } = await pool.query("SELECT version()", []); + const [{ version }] = rows; + + expect(version).toMatch(/PostgreSQL/); + } finally { + pool.end(); + } + }); +}); + +describe("postgres", () => { + it("should connect using TLS", async () => { + const sql = postgres(CONNECTION_STRING as string); + try { + const [{ version }] = await sql`SELECT version()`; + expect(version).toMatch(/PostgreSQL/); + } finally { + sql.end(); + } + }); + + it("should insert, select and delete", async () => { + const sql = postgres(CONNECTION_STRING as string); + try { + await sql`CREATE TABLE IF NOT EXISTS users ( + user_id serial PRIMARY KEY, + username VARCHAR ( 50 ) NOT NULL + );`; + + const [{ user_id, username }] = await sql`insert into users (username) values ('bun') returning *`; + expect(username).toBe("bun"); + + const [{ user_id: user_id2, username: username2 }] = await sql`select * from users where user_id = ${user_id}`; + expect(username2).toBe("bun"); + expect(user_id2).toBe(user_id); + + const [{ username: username3 }] = await sql`delete from users where user_id = ${user_id} returning *`; + expect(username3).toBe("bun"); + } finally { + sql.end(); + } + }); +}); diff --git a/test/js/third_party/prisma/package.json b/test/js/third_party/prisma/package.json index 369bd42ba..ac8694e36 100644 --- a/test/js/third_party/prisma/package.json +++ b/test/js/third_party/prisma/package.json @@ -3,11 +3,11 @@ "module": "index.ts", "type": "module", "devDependencies": { - "bun-types": "^0.6.0", + "bun-types": "0.6.12", "prisma": "4.15.0" }, "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "5.0.0" }, "dependencies": { "@prisma/client": "4.15.0" diff --git a/test/js/third_party/prisma/prisma.test.ts b/test/js/third_party/prisma/prisma.test.ts index abe6f0635..2c87a9fcb 100644 --- a/test/js/third_party/prisma/prisma.test.ts +++ b/test/js/third_party/prisma/prisma.test.ts @@ -10,7 +10,7 @@ function* TestIDGenerator() { } const test_id = TestIDGenerator(); -["sqlite", "postgres", "mongodb"].forEach(async type => { +["sqlite" /*"postgres", "mongodb"*/].forEach(async type => { let Client: typeof PrismaClient; try { diff --git a/test/js/third_party/socket.io/package.json b/test/js/third_party/socket.io/package.json index edeb84845..e54aa3de9 100644 --- a/test/js/third_party/socket.io/package.json +++ b/test/js/third_party/socket.io/package.json @@ -2,9 +2,8 @@ "name": "socket.io", "version": "1.0.0", "dependencies": { - "socket.io": "^4.6.1", - "socket.io-client": "^4.6.1", - "supertest": "^6.1.6", - "uWebSockets.js": "uNetworking/uWebSockets.js#v20.24.0" + "socket.io": "4.6.1", + "socket.io-client": "4.6.1", + "supertest": "6.3.3" } } diff --git a/test/js/third_party/socket.io/support/util.ts b/test/js/third_party/socket.io/support/util.ts index 597b40d65..b5f515568 100644 --- a/test/js/third_party/socket.io/support/util.ts +++ b/test/js/third_party/socket.io/support/util.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import type { Server } from "socket.io"; import request from "supertest"; diff --git a/test/js/third_party/webpack/package.json b/test/js/third_party/webpack/package.json new file mode 100644 index 000000000..fb08bfdc5 --- /dev/null +++ b/test/js/third_party/webpack/package.json @@ -0,0 +1,7 @@ +{ + "name": "webpack-test", + "version": "2.0.0", + "dependencies": { + "webpack": "5.88.0" + } +}
\ No newline at end of file diff --git a/test/js/third_party/webpack/test.js b/test/js/third_party/webpack/test.js new file mode 100644 index 000000000..bfd3e80a1 --- /dev/null +++ b/test/js/third_party/webpack/test.js @@ -0,0 +1,11 @@ +import { world } from "./world.js"; + +function component() { + const element = document.createElement("div"); + + element.innerHTML = "hello " + world(); + + return element; +} + +document.body.appendChild(component()); diff --git a/test/js/third_party/webpack/webpack.test.ts b/test/js/third_party/webpack/webpack.test.ts new file mode 100644 index 000000000..ffc8195c6 --- /dev/null +++ b/test/js/third_party/webpack/webpack.test.ts @@ -0,0 +1,27 @@ +import { bunExe, bunEnv } from "harness"; +import { existsSync, rmdirSync } from "fs"; +import { join } from "path"; + +afterEach(() => { + rmdirSync(join(import.meta.dir, "dist"), { recursive: true }); +}); + +test("webpack works", () => { + Bun.spawnSync({ + cmd: [bunExe(), "-b", "webpack", "--entry", "./test.js", "-o", "./dist/test1/main.js"], + cwd: import.meta.dir, + env: bunEnv, + }); + + expect(existsSync(join(import.meta.dir, "dist", "test1/main.js"))).toBe(true); +}); + +test("webpack --watch works", async () => { + Bun.spawnSync({ + cmd: ["timeout", "3", bunExe(), "-b", "webpack", "--entry", "./test.js", "-o", "./dist/test2/main.js", "--watch"], + cwd: import.meta.dir, + env: bunEnv, + }); + + expect(existsSync(join(import.meta.dir, "dist", "test2/main.js"))).toBe(true); +}); diff --git a/test/js/third_party/webpack/world.js b/test/js/third_party/webpack/world.js new file mode 100644 index 000000000..3fa21bf67 --- /dev/null +++ b/test/js/third_party/webpack/world.js @@ -0,0 +1,3 @@ +export function world() { + return "world"; +} diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt index 97191c8be..332322665 100644 --- a/test/js/web/console/console-log.expected.txt +++ b/test/js/web/console/console-log.expected.txt @@ -1,4 +1,6 @@ Hello World! +0 +-0 123 -123 123.567 @@ -7,6 +9,8 @@ true false null undefined +Infinity +-Infinity Symbol(Symbol Description) 2000-06-27T02:24:34.304Z [ 123, 456, 789 ] @@ -29,7 +33,9 @@ Symbol(Symbol Description) } Promise { <pending> } [Function] -[Function: Foo] +[Function] +[class Foo] +[class] {} [Function: foooo] /FooRegex/ diff --git a/test/js/web/console/console-log.js b/test/js/web/console/console-log.js index e23a3e9cb..4db40aaac 100644 --- a/test/js/web/console/console-log.js +++ b/test/js/web/console/console-log.js @@ -1,4 +1,6 @@ console.log("Hello World!"); +console.log(0); +console.log(-0); console.log(123); console.log(-123); console.log(123.567); @@ -7,6 +9,8 @@ console.log(true); console.log(false); console.log(null); console.log(undefined); +console.log(Infinity); +console.log(-Infinity); console.log(Symbol("Symbol Description")); console.log(new Date(Math.pow(2, 34) * 56)); console.log([123, 456, 789]); @@ -27,7 +31,9 @@ console.log(new Promise(() => {})); class Foo {} console.log(() => {}); +console.log(function () {}); console.log(Foo); +console.log(class {}); console.log(new Foo()); console.log(function foooo() {}); diff --git a/test/js/web/fetch/fetch-leak-test-fixture.js b/test/js/web/fetch/fetch-leak-test-fixture.js index 07275a425..c83bbea83 100644 --- a/test/js/web/fetch/fetch-leak-test-fixture.js +++ b/test/js/web/fetch/fetch-leak-test-fixture.js @@ -29,6 +29,6 @@ await (async function runAll() { await Bun.sleep(10); Bun.gc(true); -if ((heapStats().objectTypeCounts.Response ?? 0) > 10) { +if ((heapStats().objectTypeCounts.Response ?? 0) > 1 + ((COUNT / 2) | 0)) { throw new Error("Too many Response objects: " + heapStats().objectTypeCounts.Response); } diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts index 4d529b231..768818420 100644 --- a/test/js/web/fetch/fetch.test.ts +++ b/test/js/web/fetch/fetch.test.ts @@ -387,6 +387,25 @@ describe("fetch", () => { }).toThrow("fetch() request with GET/HEAD/OPTIONS method cannot have body."); }), ); + + it("content length is inferred", async () => { + startServer({ + fetch(req) { + return new Response(req.headers.get("content-length")); + }, + hostname: "localhost", + }); + + // POST with body + const url = `http://${server.hostname}:${server.port}`; + const response = await fetch(url, { method: "POST", body: "buntastic" }); + expect(response.status).toBe(200); + expect(await response.text()).toBe("9"); + + const response2 = await fetch(url, { method: "POST", body: "" }); + expect(response2.status).toBe(200); + expect(await response2.text()).toBe("0"); + }); }); it("simultaneous HTTPS fetch", async () => { @@ -1156,6 +1175,10 @@ it("#2794", () => { expect(typeof Bun.fetch.bind).toBe("function"); }); +it("#3545", () => { + expect(() => fetch("http://example.com?a=b")).not.toThrow(); +}); + it("invalid header doesnt crash", () => { expect(() => fetch("http://example.com", { diff --git a/test/js/web/html/FormData.test.ts b/test/js/web/html/FormData.test.ts index cbaf5aaa7..45b4f2f5a 100644 --- a/test/js/web/html/FormData.test.ts +++ b/test/js/web/html/FormData.test.ts @@ -301,17 +301,18 @@ describe("FormData", () => { expect(await (body.get("foo") as Blob).text()).toBe("baz"); server.stop(true); }); - + type FetchReqArgs = [request: Request, init?: RequestInit]; + type FetchURLArgs = [url: string | URL | Request, init?: FetchRequestInit]; for (let useRequestConstructor of [true, false]) { describe(useRequestConstructor ? "Request constructor" : "fetch()", () => { - function send(args: Parameters<typeof fetch>) { + function send(args: FetchReqArgs | FetchURLArgs) { if (useRequestConstructor) { - return fetch(new Request(...args)); + return fetch(new Request(...(args as FetchReqArgs))); } else { - return fetch(...args); + return fetch(...(args as FetchURLArgs)); } } - for (let headers of [{}, undefined, { headers: { X: "Y" } }]) { + for (let headers of [{} as {}, undefined, { headers: { X: "Y" } }]) { describe("headers: " + Bun.inspect(headers).replaceAll(/([\n ])/gim, ""), () => { it("send on HTTP server with FormData & Blob (roundtrip)", async () => { let contentType = ""; @@ -330,11 +331,10 @@ describe("FormData", () => { form.append("bar", "baz"); // @ts-ignore - const reqBody = [ + const reqBody: FetchURLArgs = [ `http://${server.hostname}:${server.port}`, { body: form, - headers, method: "POST", }, @@ -364,7 +364,6 @@ describe("FormData", () => { form.append("foo", file); form.append("bar", "baz"); - // @ts-ignore const reqBody = [ `http://${server.hostname}:${server.port}`, { @@ -374,7 +373,7 @@ describe("FormData", () => { method: "POST", }, ]; - const res = await send(reqBody); + const res = await send(reqBody as FetchURLArgs); const body = await res.formData(); expect(await (body.get("foo") as Blob).text()).toBe(text); expect(contentType).toContain("multipart/form-data"); @@ -410,7 +409,7 @@ describe("FormData", () => { method: "POST", }, ]; - const res = await send(reqBody); + const res = await send(reqBody as FetchURLArgs); const body = await res.formData(); expect(contentType).toContain("multipart/form-data"); expect(body.get("foo")).toBe("boop"); diff --git a/test/js/web/html/URLSearchParams.test.ts b/test/js/web/html/URLSearchParams.test.ts index 120bb2321..41c42c25d 100644 --- a/test/js/web/html/URLSearchParams.test.ts +++ b/test/js/web/html/URLSearchParams.test.ts @@ -7,15 +7,20 @@ describe("URLSearchParams", () => { params.append("foo", "bar"); params.append("foo", "boop"); params.append("bar", "baz"); + // @ts-ignore expect(params.length).toBe(3); params.delete("foo"); + // @ts-ignore expect(params.length).toBe(1); params.append("foo", "bar"); + // @ts-ignore expect(params.length).toBe(2); params.delete("foo"); params.delete("foo"); + // @ts-ignore expect(params.length).toBe(1); params.delete("bar"); + // @ts-ignore expect(params.length).toBe(0); }); diff --git a/test/js/web/timers/process-setImmediate-fixture.js b/test/js/web/timers/process-setImmediate-fixture.js new file mode 100644 index 000000000..6ffd91c8d --- /dev/null +++ b/test/js/web/timers/process-setImmediate-fixture.js @@ -0,0 +1,9 @@ +setImmediate(() => { + console.log("setImmediate"); + return { + a: 1, + b: 2, + c: 3, + d: 4, + }; +}); diff --git a/test/js/web/timers/setImmediate.test.js b/test/js/web/timers/setImmediate.test.js index 9cd6fa1c9..d00224e0f 100644 --- a/test/js/web/timers/setImmediate.test.js +++ b/test/js/web/timers/setImmediate.test.js @@ -1,4 +1,6 @@ import { it, expect } from "bun:test"; +import { bunExe, bunEnv } from "harness"; +import path from "path"; it("setImmediate", async () => { var lastID = -1; @@ -45,3 +47,28 @@ it("clearImmediate", async () => { }); expect(called).toBe(false); }); + +it("setImmediate should not keep the process alive forever", async () => { + let process = null; + const success = async () => { + process = Bun.spawn({ + cmd: [bunExe(), "run", path.join(import.meta.dir, "process-setImmediate-fixture.js")], + stdout: "ignore", + env: { + ...bunEnv, + NODE_ENV: undefined, + }, + }); + await process.exited; + process = null; + return true; + }; + + const fail = async () => { + await Bun.sleep(500); + process?.kill(); + return false; + }; + + expect(await Promise.race([success(), fail()])).toBe(true); +}); diff --git a/test/js/web/timers/setTimeout-unref-fixture-2.js b/test/js/web/timers/setTimeout-unref-fixture-2.js new file mode 100644 index 000000000..6a78f13cd --- /dev/null +++ b/test/js/web/timers/setTimeout-unref-fixture-2.js @@ -0,0 +1,9 @@ +setTimeout(() => { + console.log("TEST FAILED!"); +}, 100) + .ref() + .unref(); + +setTimeout(() => { + // this one should always run +}, 1); diff --git a/test/js/web/timers/setTimeout-unref-fixture-3.js b/test/js/web/timers/setTimeout-unref-fixture-3.js new file mode 100644 index 000000000..41808f5fc --- /dev/null +++ b/test/js/web/timers/setTimeout-unref-fixture-3.js @@ -0,0 +1,7 @@ +setTimeout(() => { + setTimeout(() => {}, 999_999); +}, 100).unref(); + +setTimeout(() => { + // this one should always run +}, 1); diff --git a/test/js/web/timers/setTimeout-unref-fixture-4.js b/test/js/web/timers/setTimeout-unref-fixture-4.js new file mode 100644 index 000000000..9968f3b36 --- /dev/null +++ b/test/js/web/timers/setTimeout-unref-fixture-4.js @@ -0,0 +1,5 @@ +setTimeout(() => { + console.log("TEST PASSED!"); +}, 1) + .unref() + .ref(); diff --git a/test/js/web/timers/setTimeout-unref-fixture-5.js b/test/js/web/timers/setTimeout-unref-fixture-5.js new file mode 100644 index 000000000..e5caa1be4 --- /dev/null +++ b/test/js/web/timers/setTimeout-unref-fixture-5.js @@ -0,0 +1,5 @@ +setTimeout(() => { + console.log("TEST FAILED!"); +}, 100) + .ref() + .unref(); diff --git a/test/js/web/timers/setTimeout-unref-fixture.js b/test/js/web/timers/setTimeout-unref-fixture.js new file mode 100644 index 000000000..97a0f78a2 --- /dev/null +++ b/test/js/web/timers/setTimeout-unref-fixture.js @@ -0,0 +1,12 @@ +const timer = setTimeout(() => {}, 999_999_999); +if (timer.unref() !== timer) throw new Error("Expected timer.unref() === timer"); + +var ranCount = 0; +const going2Refresh = setTimeout(() => { + if (ranCount < 1) going2Refresh.refresh(); + ranCount++; + + if (ranCount === 2) { + console.log("SUCCESS"); + } +}, 1); diff --git a/test/js/web/timers/setTimeout.test.js b/test/js/web/timers/setTimeout.test.js index dbe89dea8..eef6bbae0 100644 --- a/test/js/web/timers/setTimeout.test.js +++ b/test/js/web/timers/setTimeout.test.js @@ -1,5 +1,7 @@ +import { spawnSync } from "bun"; import { it, expect } from "bun:test"; - +import { bunEnv, bunExe } from "harness"; +import path from "node:path"; it("setTimeout", async () => { var lastID = -1; const result = await new Promise((resolve, reject) => { @@ -172,11 +174,56 @@ it.skip("order of setTimeouts", done => { Promise.resolve().then(maybeDone(() => nums.push(1))); }); +it("setTimeout -> refresh", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), path.join(import.meta.dir, "setTimeout-unref-fixture.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString()).toBe("SUCCESS\n"); +}); + +it("setTimeout -> unref -> ref works", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), path.join(import.meta.dir, "setTimeout-unref-fixture-4.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString()).toBe("TEST PASSED!\n"); +}); + +it("setTimeout -> ref -> unref works, even if there is another timer", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), path.join(import.meta.dir, "setTimeout-unref-fixture-2.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString()).toBe(""); +}); + +it("setTimeout -> ref -> unref works", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), path.join(import.meta.dir, "setTimeout-unref-fixture-5.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString()).toBe(""); +}); + +it("setTimeout -> unref doesn't keep event loop alive forever", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), path.join(import.meta.dir, "setTimeout-unref-fixture-3.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString()).toBe(""); +}); + it("setTimeout should refresh N times", done => { let count = 0; let timer = setTimeout(() => { count++; - timer.refresh(); + expect(timer.refresh()).toBe(timer); }, 50); setTimeout(() => { diff --git a/test/js/web/util/atob.test.js b/test/js/web/util/atob.test.js index 4945829e1..20c029f14 100644 --- a/test/js/web/util/atob.test.js +++ b/test/js/web/util/atob.test.js @@ -60,18 +60,15 @@ it("btoa", () => { expect(btoa("abcde")).toBe("YWJjZGU="); expect(btoa("abcdef")).toBe("YWJjZGVm"); expect(typeof btoa).toBe("function"); - try { - btoa(); - throw new Error("Expected error"); - } catch (error) { - expect(error.name).toBe("TypeError"); - } + expect(() => btoa()).toThrow("btoa requires 1 argument (a string)"); var window = "[object Window]"; expect(btoa("")).toBe(""); expect(btoa(null)).toBe("bnVsbA=="); expect(btoa(undefined)).toBe("dW5kZWZpbmVk"); expect(btoa(window)).toBe("W29iamVjdCBXaW5kb3dd"); expect(btoa("éé")).toBe("6ek="); + // check for utf16 + expect(btoa("🧐éé".substring("🧐".length))).toBe("6ek="); expect(btoa("\u0080\u0081")).toBe("gIE="); expect(btoa(Bun)).toBe(btoa("[object Bun]")); }); diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js index b7a243190..d687a1290 100644 --- a/test/js/web/web-globals.test.js +++ b/test/js/web/web-globals.test.js @@ -138,6 +138,25 @@ it("crypto.randomUUID", () => { }); }); +it("crypto.randomUUID version, issues#3575", () => { + var uuid = crypto.randomUUID(); + + function validate(uuid) { + const regex = + /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; + return typeof uuid === "string" && regex.test(uuid); + } + function version(uuid) { + if (!validate(uuid)) { + throw TypeError("Invalid UUID"); + } + + return parseInt(uuid.slice(14, 15), 16); + } + + expect(version(uuid)).toBe(4); +}); + it("URL.prototype.origin", () => { const url = new URL("https://html.spec.whatwg.org/"); const { origin, host, hostname } = url; diff --git a/test/js/web/websocket/websocket.test.js b/test/js/web/websocket/websocket.test.js index 867b86123..76ff16ecb 100644 --- a/test/js/web/websocket/websocket.test.js +++ b/test/js/web/websocket/websocket.test.js @@ -6,16 +6,33 @@ const TEST_WEBSOCKET_HOST = process.env.TEST_WEBSOCKET_HOST || "wss://ws.postman describe("WebSocket", () => { it("should connect", async () => { - const ws = new WebSocket(TEST_WEBSOCKET_HOST); - await new Promise((resolve, reject) => { + const server = Bun.serve({ + port: 0, + fetch(req, server) { + if (server.upgrade(req)) { + server.stop(); + return; + } + + return new Response(); + }, + websocket: { + open(ws) {}, + message(ws) { + ws.close(); + }, + }, + }); + const ws = new WebSocket(`ws://${server.hostname}:${server.port}`, {}); + await new Promise(resolve => { ws.onopen = resolve; - ws.onerror = reject; }); - var closed = new Promise((resolve, reject) => { + var closed = new Promise(resolve => { ws.onclose = resolve; }); ws.close(); await closed; + server.stop(true); }); it("should connect over https", async () => { @@ -59,17 +76,18 @@ describe("WebSocket", () => { const server = Bun.serve({ port: 0, fetch(req, server) { - server.stop(); done(); + server.stop(); return new Response(); }, websocket: { - open(ws) { + open(ws) {}, + message(ws) { ws.close(); }, }, }); - const ws = new WebSocket(`http://${server.hostname}:${server.port}`, {}); + new WebSocket(`http://${server.hostname}:${server.port}`, {}); }); describe("nodebuffer", () => { it("should support 'nodebuffer' binaryType", done => { @@ -93,6 +111,7 @@ describe("WebSocket", () => { expect(ws.binaryType).toBe("nodebuffer"); Bun.gc(true); ws.onmessage = ({ data }) => { + ws.close(); expect(Buffer.isBuffer(data)).toBe(true); expect(data).toEqual(new Uint8Array([1, 2, 3])); server.stop(true); @@ -117,6 +136,7 @@ describe("WebSocket", () => { ws.sendBinary(new Uint8Array([1, 2, 3])); setTimeout(() => { client.onmessage = ({ data }) => { + client.close(); expect(Buffer.isBuffer(data)).toBe(true); expect(data).toEqual(new Uint8Array([1, 2, 3])); server.stop(true); diff --git a/test/js/workerd/html-rewriter.test.js b/test/js/workerd/html-rewriter.test.js index 1ca92a567..44961df3b 100644 --- a/test/js/workerd/html-rewriter.test.js +++ b/test/js/workerd/html-rewriter.test.js @@ -300,4 +300,91 @@ describe("HTMLRewriter", () => { .text(), ).toEqual("<div></div>"); }); + + it("it supports lastInTextNode", async () => { + let lastInTextNode; + + await new HTMLRewriter() + .on("p", { + text(text) { + lastInTextNode ??= text.lastInTextNode; + }, + }) + .transform(new Response("<p>Lorem ipsum!</p>")) + .text(); + + expect(lastInTextNode).toBeBoolean(); + }); +}); + +// By not segfaulting, this test passes +it("#3334 regression", async () => { + for (let i = 0; i < 10; i++) { + const headers = new Headers({ + "content-type": "text/html", + }); + const response = new Response("<div>content</div>", { headers }); + + const result = await new HTMLRewriter() + .on("div", { + element(elem) { + elem.setInnerContent("new"); + }, + }) + .transform(response) + .text(); + expect(result).toEqual("<div>new</div>"); + } + Bun.gc(true); +}); + +it("#3489", async () => { + var el; + await new HTMLRewriter() + .on("p", { + element(element) { + el = element.getAttribute("id"); + }, + }) + .transform(new Response('<p id="Šžõäöü"></p>')) + .text(); + expect(el).toEqual("Šžõäöü"); +}); + +it("get attribute - ascii", async () => { + for (let i = 0; i < 10; i++) { + var el; + await new HTMLRewriter() + .on("p", { + element(element) { + el = element.getAttribute("id"); + }, + }) + .transform(new Response(`<p id="asciii"></p>`)) + .text(); + expect(el).toEqual("asciii"); + } +}); + +it("#3520", async () => { + const pairs = []; + + await new HTMLRewriter() + .on("p", { + element(element) { + for (const pair of element.attributes) { + pairs.push(pair); + } + }, + }) + .transform(new Response('<p šž="Õäöü" ab="Õäöü" šž="Õäöü" šž="dc" šž="🕵🏻"></p>')) + .text(); + + expect(pairs).toEqual([ + ["šž", "Õäöü"], + ["ab", "Õäöü"], + ["šž", "Õäöü"], + ["šž", "dc"], + ["šž", "🕵🏻"], + ]); }); |