From 70a94af62a84fae53eab29b3377d2076d858c93c Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Thu, 9 Apr 2026 01:02:53 +0200 Subject: [PATCH] add a few more tests --- src/shims/fs/content-cache.test.js | 92 ++++++++++ src/shims/fs/fd.test.js | 275 +++++++++++++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 src/shims/fs/content-cache.test.js create mode 100644 src/shims/fs/fd.test.js diff --git a/src/shims/fs/content-cache.test.js b/src/shims/fs/content-cache.test.js new file mode 100644 index 0000000..396c344 --- /dev/null +++ b/src/shims/fs/content-cache.test.js @@ -0,0 +1,92 @@ +import { describe, it, expect, vi } from "vitest"; +import { ContentCache } from "./content-cache.js"; + +// -- Size accounting --------------------------------------------------- + +describe("ContentCache size accounting", () => { + it("set increases currentBytes by data length", () => { + const cache = new ContentCache(1024); + cache.set("a.md", "hello"); // 5 bytes + expect(cache.currentBytes).toBe(5); + }); + + it("delete returns currentBytes to 0", () => { + const cache = new ContentCache(1024); + cache.set("a.md", "hello"); + cache.delete("a.md"); + expect(cache.currentBytes).toBe(0); + }); + + it("replacing an entry reflects the new size, not old + new", () => { + const cache = new ContentCache(1024); + cache.set("a.md", "short"); + cache.set("a.md", "a much longer string"); + expect(cache.currentBytes).toBe("a much longer string".length); + }); + + it("deleting one of several entries leaves the sum of the rest", () => { + const cache = new ContentCache(1024); + cache.set("a.md", "aaa"); // 3 + cache.set("b.md", "bbbbb"); // 5 + cache.set("c.md", "cc"); // 2 + cache.delete("b.md"); + expect(cache.currentBytes).toBe(5); // 3 + 2 + }); +}); + +// -- LRU eviction ------------------------------------------------------ + +describe("ContentCache LRU eviction", () => { + it("evicts the least-recently-accessed entry when full", () => { + let now = 1000; + vi.spyOn(Date, "now").mockImplementation(() => now++); + + const cache = new ContentCache(10); + cache.set("a.md", "aaaa"); // 4 + cache.set("b.md", "bbbb"); // 4 + // At 8/10. Adding 4 more would exceed, so LRU (a.md) should be evicted. + cache.set("c.md", "cccc"); // 4 + expect(cache.has("a.md")).toBe(false); + expect(cache.has("b.md")).toBe(true); + expect(cache.has("c.md")).toBe(true); + + vi.restoreAllMocks(); + }); + + it("accessing an entry refreshes it so it survives eviction", () => { + let now = 1000; + vi.spyOn(Date, "now").mockImplementation(() => now++); + + const cache = new ContentCache(10); + cache.set("a.md", "aaaa"); // 4, accessedAt=1000 + cache.set("b.md", "bbbb"); // 4, accessedAt=1001 + // Touch a.md so b.md becomes the LRU + cache.get("a.md"); // a.md accessedAt=1002 + cache.set("c.md", "cccc"); // 4 -- should evict b.md (1001), not a.md (1002) + expect(cache.has("a.md")).toBe(true); + expect(cache.has("b.md")).toBe(false); + expect(cache.has("c.md")).toBe(true); + + vi.restoreAllMocks(); + }); + + it("entry larger than maxSize still gets stored", () => { + const cache = new ContentCache(5); + cache.set("small.md", "ab"); // 2 + cache.set("big.md", "abcdefghij"); // 10 -- larger than maxSize + expect(cache.has("small.md")).toBe(false); + expect(cache.has("big.md")).toBe(true); + expect(cache.currentBytes).toBe(10); + }); +}); + +// -- Path normalization ------------------------------------------------ + +describe("ContentCache path normalization", () => { + it("backslash and slash variants hit the same cache entry", () => { + const cache = new ContentCache(1024); + cache.set("foo\\bar\\baz.md", "content"); + expect(cache.has("foo/bar/baz.md")).toBe(true); + expect(cache.get("foo/bar/baz.md")).toBe("content"); + }); +}); diff --git a/src/shims/fs/fd.test.js b/src/shims/fs/fd.test.js new file mode 100644 index 0000000..bdcb2cc --- /dev/null +++ b/src/shims/fs/fd.test.js @@ -0,0 +1,275 @@ +import { describe, it, expect } from "vitest"; +import { createFdOps } from "./fd.js"; + +function makeStubs(files = {}) { + const meta = { + has(p) { + return p in files; + }, + toStat(p) { + if (!(p in files)) { + return null; + } + + return { + size: files[p].length, + isFile: () => true, + isDirectory: () => false, + }; + }, + }; + + const content = { + _store: {}, + get(p) { + return this._store[p] ?? null; + }, + set(p, data) { + this._store[p] = data; + }, + }; + + const transport = { + readFileSync(p) { + return files[p] ?? null; + }, + }; + + // Pre-populate content cache so ensureData doesn't hit transport + for (const [p, data] of Object.entries(files)) { + content.set(p, data); + } + + return { meta, content, transport }; +} + +function makeOps(files = {}) { + const { meta, content, transport } = makeStubs(files); + return createFdOps(meta, content, transport); +} + +// -- openSync / closeSync lifecycle ------------------------------------ + +describe("fd openSync / closeSync lifecycle", () => { + it("open returns an integer fd", () => { + const ops = makeOps({ "a.md": new Uint8Array([1, 2, 3]) }); + const fd = ops.openSync("a.md", "r"); + expect(typeof fd).toBe("number"); + expect(Number.isInteger(fd)).toBe(true); + }); + + it("multiple opens return distinct fds", () => { + const ops = makeOps({ "a.md": new Uint8Array([1]) }); + const fd1 = ops.openSync("a.md", "r"); + const fd2 = ops.openSync("a.md", "r"); + expect(fd1).not.toBe(fd2); + }); + + it("close removes the fd", () => { + const ops = makeOps({ "a.md": new Uint8Array([1]) }); + const fd = ops.openSync("a.md", "r"); + ops.closeSync(fd); + expect(() => ops.readSync(fd, new Uint8Array(1), 0, 1, 0)).toThrow( + "EBADF", + ); + }); + + it("open throws ENOENT for missing path", () => { + const ops = makeOps({}); + expect(() => ops.openSync("nope.md", "r")).toThrow("ENOENT"); + }); + + it("accessing a closed fd throws EBADF", () => { + const ops = makeOps({ "a.md": new Uint8Array([1]) }); + const fd = ops.openSync("a.md", "r"); + ops.closeSync(fd); + expect(() => ops.fstatSync(fd)).toThrow("EBADF"); + }); +}); + +// -- readSync ---------------------------------------------------------- + +describe("fd readSync", () => { + const data = new Uint8Array([10, 20, 30, 40, 50]); + + function openData() { + const ops = makeOps({ "f.bin": data }); + const fd = ops.openSync("f.bin", "r"); + return { ops, fd }; + } + + it("read from position 0, full length", () => { + const { ops, fd } = openData(); + const buf = new Uint8Array(5); + const n = ops.readSync(fd, buf, 0, 5, 0); + expect(n).toBe(5); + expect(buf).toEqual(data); + }); + + it("read from mid-file position", () => { + const { ops, fd } = openData(); + const buf = new Uint8Array(3); + const n = ops.readSync(fd, buf, 0, 3, 2); + expect(n).toBe(3); + expect(buf).toEqual(new Uint8Array([30, 40, 50])); + }); + + it("read with length exceeding remaining data", () => { + const { ops, fd } = openData(); + const buf = new Uint8Array(10); + const n = ops.readSync(fd, buf, 0, 10, 3); + expect(n).toBe(2); + expect(buf[0]).toBe(40); + expect(buf[1]).toBe(50); + }); + + it("read at position === data.length returns 0 bytes", () => { + const { ops, fd } = openData(); + const buf = new Uint8Array(5); + const n = ops.readSync(fd, buf, 0, 5, 5); + expect(n).toBe(0); + }); + + it("read at position > data.length returns 0 bytes", () => { + const { ops, fd } = openData(); + const buf = new Uint8Array(5); + const n = ops.readSync(fd, buf, 0, 5, 100); + expect(n).toBe(0); + }); + + it("read with offset > 0 places data at correct position", () => { + const { ops, fd } = openData(); + const buf = new Uint8Array(6); + buf.fill(0); + const n = ops.readSync(fd, buf, 3, 2, 0); + expect(n).toBe(2); + expect(buf).toEqual(new Uint8Array([0, 0, 0, 10, 20, 0])); + }); + + it("target buffer is actually modified", () => { + const { ops, fd } = openData(); + const buf = new Uint8Array(3); + buf.fill(255); + ops.readSync(fd, buf, 0, 2, 0); + expect(buf[0]).toBe(10); + expect(buf[1]).toBe(20); + expect(buf[2]).toBe(255); // untouched + }); +}); + +// -- fstatSync --------------------------------------------------------- + +describe("fd fstatSync", () => { + it("returns metadata from cache when available", () => { + const ops = makeOps({ "a.md": new Uint8Array([1, 2, 3]) }); + const fd = ops.openSync("a.md", "r"); + const stat = ops.fstatSync(fd); + expect(stat.size).toBe(3); + expect(stat.isFile()).toBe(true); + expect(stat.isDirectory()).toBe(false); + }); + + it("falls back to buffer length when metadata is missing", () => { + const { meta, content, transport } = makeStubs({ + "a.md": new Uint8Array([1, 2, 3, 4]), + }); + + // Override toStat to return null, simulating missing metadata + meta.toStat = () => null; + + const ops = createFdOps(meta, content, transport); + const fd = ops.openSync("a.md", "r"); + const stat = ops.fstatSync(fd); + expect(stat.size).toBe(4); + expect(stat.isFile()).toBe(true); + }); +}); + +// -- Async wrappers ---------------------------------------------------- + +describe("fd async wrappers", () => { + it("open() calls callback asynchronously", async () => { + const ops = makeOps({ "a.md": new Uint8Array([1]) }); + let called = false; + + const promise = new Promise((resolve, reject) => { + ops.open("a.md", "r", (err, fd) => { + called = true; + + if (err) { + reject(err); + } else { + resolve(fd); + } + }); + }); + + // Callback should not have fired synchronously + expect(called).toBe(false); + const fd = await promise; + expect(typeof fd).toBe("number"); + }); + + it("open() error path calls cb(err)", async () => { + const ops = makeOps({}); + + const err = await new Promise((resolve) => { + ops.open("nope.md", "r", (e) => resolve(e)); + }); + + expect(err).toBeTruthy(); + expect(err.code).toBe("ENOENT"); + }); + + it("read() delivers results via callback", async () => { + const ops = makeOps({ "a.md": new Uint8Array([10, 20]) }); + + const fd = await new Promise((resolve, reject) => { + ops.open("a.md", "r", (err, fd) => (err ? reject(err) : resolve(fd))); + }); + + const buf = new Uint8Array(2); + + const bytesRead = await new Promise((resolve, reject) => { + ops.read(fd, buf, 0, 2, 0, (err, n) => + err ? reject(err) : resolve(n), + ); + }); + + expect(bytesRead).toBe(2); + expect(buf).toEqual(new Uint8Array([10, 20])); + }); + + it("close() calls callback asynchronously", async () => { + const ops = makeOps({ "a.md": new Uint8Array([1]) }); + const fd = ops.openSync("a.md", "r"); + + const result = await new Promise((resolve) => { + ops.close(fd, (err) => resolve(err)); + }); + + expect(result).toBe(null); + }); + + it("fstat() calls callback asynchronously", async () => { + const ops = makeOps({ "a.md": new Uint8Array([1, 2, 3]) }); + const fd = ops.openSync("a.md", "r"); + + const stat = await new Promise((resolve, reject) => { + ops.fstat(fd, (err, s) => (err ? reject(err) : resolve(s))); + }); + + expect(stat.size).toBe(3); + }); + + it("fstat() error path calls cb(err) for bad fd", async () => { + const ops = makeOps({}); + + const err = await new Promise((resolve) => { + ops.fstat(99999, (e) => resolve(e)); + }); + + expect(err).toBeTruthy(); + expect(err.code).toBe("EBADF"); + }); +});