From acfd028e8f859a0e8139b7adab5d319e326c2373 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Tue, 5 Sep 2023 17:41:39 -0700 Subject: feat(runtime): Implement `fs.watchFile` (#4467) * really lame prototype * uses threads but badly * it works i guess * unwatchFile but lame * it works * test * a * aomitcs * fix unwatching race condition * use hasPendingActivity and GC stuff better * test * revert this --- test/js/node/watch/fs.watchFile.test.ts | 121 ++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 test/js/node/watch/fs.watchFile.test.ts (limited to 'test') diff --git a/test/js/node/watch/fs.watchFile.test.ts b/test/js/node/watch/fs.watchFile.test.ts new file mode 100644 index 000000000..25cd0c8c5 --- /dev/null +++ b/test/js/node/watch/fs.watchFile.test.ts @@ -0,0 +1,121 @@ +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", + [encodingFileName]: "hello", +}); + +describe("fs.watchFile", () => { + test("zeroed stats if does not exist", async () => { + let entries: any = []; + fs.watchFile(path.join(testDir, "does-not-exist"), (curr, prev) => { + entries.push([curr, prev]); + }); + + await Bun.sleep(35); + + fs.unwatchFile(path.join(testDir, "does-not-exist")); + + expect(entries.length).toBe(1); + expect(entries[0][0].size).toBe(0); + expect(entries[0][0].mtimeMs).toBe(0); + expect(entries[0][1].size).toBe(0); + expect(entries[0][1].mtimeMs).toBe(0); + }); + test("it watches a file", async () => { + let entries: any = []; + fs.watchFile(path.join(testDir, "watch.txt"), { interval: 50 }, (curr, prev) => { + entries.push([curr, prev]); + }); + await Bun.sleep(100); + fs.writeFileSync(path.join(testDir, "watch.txt"), "hello2"); + await Bun.sleep(100); + + fs.unwatchFile(path.join(testDir, "watch.txt")); + + expect(entries.length).toBeGreaterThan(0); + + expect(entries[0][0].size).toBe(6); + expect(entries[0][1].size).toBe(5); + expect(entries[0][0].mtimeMs).toBeGreaterThan(entries[0][1].mtimeMs); + }); + test("unicode file name", async () => { + let entries: any = []; + fs.watchFile(path.join(testDir, encodingFileName), { interval: 50 }, (curr, prev) => { + entries.push([curr, prev]); + }); + await Bun.sleep(100); + fs.writeFileSync(path.join(testDir, encodingFileName), "hello2"); + await Bun.sleep(100); + + fs.unwatchFile(path.join(testDir, encodingFileName)); + + expect(entries.length).toBeGreaterThan(0); + + expect(entries[0][0].size).toBe(6); + expect(entries[0][1].size).toBe(5); + expect(entries[0][0].mtimeMs).toBeGreaterThan(entries[0][1].mtimeMs); + }); + test("bigint stats", async () => { + let entries: any = []; + fs.watchFile(path.join(testDir, encodingFileName), { interval: 50, bigint: true }, (curr, prev) => { + entries.push([curr, prev]); + }); + await Bun.sleep(100); + fs.writeFileSync(path.join(testDir, encodingFileName), "hello2"); + await Bun.sleep(100); + + fs.unwatchFile(path.join(testDir, encodingFileName)); + + expect(entries.length).toBeGreaterThan(0); + + expect(typeof entries[0][0].mtimeMs === "bigint").toBe(true); + }); + + test("StatWatcherScheduler stress test (1000 watchers with random times)", async () => { + const EventEmitter = require("events"); + let defaultMaxListeners = EventEmitter.defaultMaxListeners; + try { + EventEmitter.defaultMaxListeners = 1000; + // This tests StatWatcher's scheduler for add/remove race conditions, + // as the actual stat()ing is done on another thread using a specialized linked list implementation + // so we're testing that here, less so that stats will properly notify js, since that code is already known to be very threadsafe. + const set = new Set(); + const { promise, resolve } = Promise.withResolvers(); + for (let i = 0; i < 1000; i++) { + const file = path.join(testDir, i + ".txt"); + setTimeout(() => { + let first = true; + fs.watchFile(file, { interval: 500 }, (curr, prev) => { + set.add(file); + if (first) { + first = false; + setTimeout(() => { + fs.unwatchFile(file); + + if (set.size === 1000) resolve(); + }, Math.random() * 2000); + } + }); + }, Math.random() * 2000); + } + await promise; + + expect(set.size).toBe(1000); + } finally { + EventEmitter.defaultMaxListeners = defaultMaxListeners; + } + }, 20000); +}); -- cgit v1.2.3