275 lines
7.4 KiB
JavaScript
275 lines
7.4 KiB
JavaScript
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");
|
|
});
|
|
});
|