diff options
author | 2023-07-11 19:14:34 -0700 | |
---|---|---|
committer | 2023-07-11 19:14:34 -0700 | |
commit | cbb88672f217a90db1aa1eb29cd92d5d9035b22b (patch) | |
tree | 43a00501f3cde495967e116f0b660777051551f8 /test/js/node | |
parent | 1f900cff453700b19bca2acadfe26da4468c1282 (diff) | |
parent | 34b0e7a2bbd8bf8097341cdb0075d0908283e834 (diff) | |
download | bun-jarred/esm-conditions.tar.gz bun-jarred/esm-conditions.tar.zst bun-jarred/esm-conditions.zip |
Merge branch 'main' into jarred/esm-conditionsjarred/esm-conditions
Diffstat (limited to 'test/js/node')
42 files changed, 1974 insertions, 147 deletions
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"); + }); +}); |