// @effect-diagnostics nodeBuiltinImport:offimport * as NodeFS from "node:fs";import * as NodeOS from "node:os";import * as NodePath from "node:path";import { afterEach, describe, expect, it } from "vite-plus/test";import { HostProcessPlatform } from "./hostProcess.ts"; import { RotatingFileSink, RotatingFileSinkConfigurationError, RotatingFileSinkError,} from "./logging.ts"; const windowsHost = HostProcessPlatform.defaultValue() === "win32";const tempDirectories: string[] = []; const makeTempDirectory = (): string => { const directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3code-logging-")); tempDirectories.push(directory); return directory;}; const captureError = (run: () => unknown): unknown => { try { run(); } catch (cause) { return cause; } throw new Error("Expected operation to throw");}; afterEach(() => { for (const directory of tempDirectories.splice(0)) { NodeFS.rmSync(directory, { recursive: true, force: true }); }}); describe("RotatingFileSink", () => { it.each([ { option: "maxBytes" as const, maxBytes: 0, maxFiles: 1 }, { option: "maxFiles" as const, maxBytes: 1, maxFiles: 0 }, ])("reports invalid $option configuration structurally", (input) => { const thrown = captureError( () => new RotatingFileSink({ filePath: "/unused/log.ndjson", maxBytes: input.maxBytes, maxFiles: input.maxFiles, }), ); expect(thrown).toBeInstanceOf(RotatingFileSinkConfigurationError); expect(thrown).toMatchObject({ option: input.option, received: 0, minimum: 1, }); expect((thrown as Error).message).toBe(`${input.option} must be >= 1 (received 0)`); }); it("preserves directory initialization failures", () => { const directory = makeTempDirectory(); const parentFile = NodePath.join(directory, "not-a-directory"); const filePath = NodePath.join(parentFile, "log.ndjson"); NodeFS.writeFileSync(parentFile, "occupied"); const thrown = captureError(() => new RotatingFileSink({ filePath, maxBytes: 1, maxFiles: 1 })); expect(thrown).toBeInstanceOf(RotatingFileSinkError); expect(thrown).toMatchObject({ operation: "initialize", filePath }); expect((thrown as RotatingFileSinkError).cause).toBeInstanceOf(Error); }); // An over-long name is the one stat failure that is neither ENOENT nor a // permission problem on posix. Windows reports it as ENOENT, so the sink // correctly treats it as an absent file and there is nothing to assert. it.skipIf(windowsHost)("only treats a missing log file as an empty current size", () => { const directory = makeTempDirectory(); const filePath = NodePath.join(directory, "a".repeat(300)); const thrown = captureError(() => new RotatingFileSink({ filePath, maxBytes: 1, maxFiles: 1 })); expect(thrown).toBeInstanceOf(RotatingFileSinkError); expect(thrown).toMatchObject({ operation: "read", filePath }); expect((thrown as RotatingFileSinkError).cause).toMatchObject({ code: "ENAMETOOLONG" }); }); it("starts an absent log file at zero bytes", () => { const directory = makeTempDirectory(); const filePath = NodePath.join(directory, "log.ndjson"); const sink = new RotatingFileSink({ filePath, maxBytes: 100, maxFiles: 1 }); sink.write("entry"); expect(NodeFS.readFileSync(filePath, "utf8")).toBe("entry"); }); it("preserves write failures", () => { const directory = makeTempDirectory(); const filePath = NodePath.join(directory, "log.ndjson"); NodeFS.mkdirSync(filePath); const sink = new RotatingFileSink({ filePath, maxBytes: Number.MAX_SAFE_INTEGER, maxFiles: 1, throwOnError: true, }); const thrown = captureError(() => sink.write("entry")); expect(thrown).toBeInstanceOf(RotatingFileSinkError); expect(thrown).toMatchObject({ operation: "write", filePath }); expect((thrown as RotatingFileSinkError).cause).toMatchObject({ code: "EISDIR" }); }); it("preserves rotation failures without an artificial write wrapper", () => { const directory = makeTempDirectory(); const filePath = NodePath.join(directory, "log.ndjson"); NodeFS.writeFileSync(filePath, "a"); NodeFS.mkdirSync(`${filePath}.1`); const sink = new RotatingFileSink({ filePath, maxBytes: 1, maxFiles: 1, throwOnError: true, }); const thrown = captureError(() => sink.write("b")); expect(thrown).toBeInstanceOf(RotatingFileSinkError); expect(thrown).toMatchObject({ operation: "rotate", filePath }); expect((thrown as RotatingFileSinkError).cause).toBeInstanceOf(Error); }); it("never reports a rotation failure after successfully appending a chunk", () => { const directory = makeTempDirectory(); const filePath = NodePath.join(directory, "log.ndjson"); NodeFS.mkdirSync(`${filePath}.1`); const sink = new RotatingFileSink({ filePath, maxBytes: 1, maxFiles: 1, throwOnError: true, }); sink.write("oversized"); expect(NodeFS.readFileSync(filePath, "utf8")).toBe("oversized"); const thrown = captureError(() => sink.write("next")); expect(thrown).toMatchObject({ operation: "rotate", filePath }); expect(NodeFS.readFileSync(filePath, "utf8")).toBe("oversized"); }); it("preserves backup pruning failures", () => { const directory = makeTempDirectory(); const filePath = NodePath.join(directory, "log.ndjson"); const overflowBackup = `${filePath}.2`; NodeFS.mkdirSync(overflowBackup); NodeFS.writeFileSync(NodePath.join(overflowBackup, "entry"), "occupied"); const thrown = captureError( () => new RotatingFileSink({ filePath, maxBytes: 1, maxFiles: 1, throwOnError: true, }), ); expect(thrown).toBeInstanceOf(RotatingFileSinkError); expect(thrown).toMatchObject({ operation: "prune", filePath }); expect((thrown as RotatingFileSinkError).cause).toBeInstanceOf(Error); });});
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import { afterEach, describe, expect, it } from "vite-plus/test";
import { HostProcessPlatform } from "./hostProcess.ts";
import {
RotatingFileSink,
RotatingFileSinkConfigurationError,
RotatingFileSinkError,
} from "./logging.ts";
const windowsHost = HostProcessPlatform.defaultValue() === "win32";
const tempDirectories: string[] = [];
const makeTempDirectory = (): string => {
const directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3code-logging-"));
tempDirectories.push(directory);
return directory;
};
const captureError = (run: () => unknown): unknown => {
try {
run();
} catch (cause) {
return cause;
}
throw new Error("Expected operation to throw");
};
afterEach(() => {
for (const directory of tempDirectories.splice(0)) {
NodeFS.rmSync(directory, { recursive: true, force: true });
}
});
describe("RotatingFileSink", () => {
it.each([
{ option: "maxBytes" as const, maxBytes: 0, maxFiles: 1 },
{ option: "maxFiles" as const, maxBytes: 1, maxFiles: 0 },
])("reports invalid $option configuration structurally", (input) => {
const thrown = captureError(
() =>
new RotatingFileSink({
filePath: "/unused/log.ndjson",
maxBytes: input.maxBytes,
maxFiles: input.maxFiles,
}),
);
expect(thrown).toBeInstanceOf(RotatingFileSinkConfigurationError);
expect(thrown).toMatchObject({
option: input.option,
received: 0,
minimum: 1,
});
expect((thrown as Error).message).toBe(`${input.option} must be >= 1 (received 0)`);
});
it("preserves directory initialization failures", () => {
const directory = makeTempDirectory();
const parentFile = NodePath.join(directory, "not-a-directory");
const filePath = NodePath.join(parentFile, "log.ndjson");
NodeFS.writeFileSync(parentFile, "occupied");
const thrown = captureError(() => new RotatingFileSink({ filePath, maxBytes: 1, maxFiles: 1 }));
expect(thrown).toBeInstanceOf(RotatingFileSinkError);
expect(thrown).toMatchObject({ operation: "initialize", filePath });
expect((thrown as RotatingFileSinkError).cause).toBeInstanceOf(Error);
});
// An over-long name is the one stat failure that is neither ENOENT nor a
// permission problem on posix. Windows reports it as ENOENT, so the sink
// correctly treats it as an absent file and there is nothing to assert.
it.skipIf(windowsHost)("only treats a missing log file as an empty current size", () => {
const directory = makeTempDirectory();
const filePath = NodePath.join(directory, "a".repeat(300));
const thrown = captureError(() => new RotatingFileSink({ filePath, maxBytes: 1, maxFiles: 1 }));
expect(thrown).toBeInstanceOf(RotatingFileSinkError);
expect(thrown).toMatchObject({ operation: "read", filePath });
expect((thrown as RotatingFileSinkError).cause).toMatchObject({ code: "ENAMETOOLONG" });
});
it("starts an absent log file at zero bytes", () => {
const directory = makeTempDirectory();
const filePath = NodePath.join(directory, "log.ndjson");
const sink = new RotatingFileSink({ filePath, maxBytes: 100, maxFiles: 1 });
sink.write("entry");
expect(NodeFS.readFileSync(filePath, "utf8")).toBe("entry");
});
it("preserves write failures", () => {
const directory = makeTempDirectory();
const filePath = NodePath.join(directory, "log.ndjson");
NodeFS.mkdirSync(filePath);
const sink = new RotatingFileSink({
filePath,
maxBytes: Number.MAX_SAFE_INTEGER,
maxFiles: 1,
throwOnError: true,
});
const thrown = captureError(() => sink.write("entry"));
expect(thrown).toBeInstanceOf(RotatingFileSinkError);
expect(thrown).toMatchObject({ operation: "write", filePath });
expect((thrown as RotatingFileSinkError).cause).toMatchObject({ code: "EISDIR" });
});
it("preserves rotation failures without an artificial write wrapper", () => {
const directory = makeTempDirectory();
const filePath = NodePath.join(directory, "log.ndjson");
NodeFS.writeFileSync(filePath, "a");
NodeFS.mkdirSync(`${filePath}.1`);
const sink = new RotatingFileSink({
filePath,
maxBytes: 1,
maxFiles: 1,
throwOnError: true,
});
const thrown = captureError(() => sink.write("b"));
expect(thrown).toBeInstanceOf(RotatingFileSinkError);
expect(thrown).toMatchObject({ operation: "rotate", filePath });
expect((thrown as RotatingFileSinkError).cause).toBeInstanceOf(Error);
});
it("never reports a rotation failure after successfully appending a chunk", () => {
const directory = makeTempDirectory();
const filePath = NodePath.join(directory, "log.ndjson");
NodeFS.mkdirSync(`${filePath}.1`);
const sink = new RotatingFileSink({
filePath,
maxBytes: 1,
maxFiles: 1,
throwOnError: true,
});
sink.write("oversized");
expect(NodeFS.readFileSync(filePath, "utf8")).toBe("oversized");
const thrown = captureError(() => sink.write("next"));
expect(thrown).toMatchObject({ operation: "rotate", filePath });
expect(NodeFS.readFileSync(filePath, "utf8")).toBe("oversized");
});
it("preserves backup pruning failures", () => {
const directory = makeTempDirectory();
const filePath = NodePath.join(directory, "log.ndjson");
const overflowBackup = `${filePath}.2`;
NodeFS.mkdirSync(overflowBackup);
NodeFS.writeFileSync(NodePath.join(overflowBackup, "entry"), "occupied");
const thrown = captureError(
() =>
new RotatingFileSink({
filePath,
maxBytes: 1,
maxFiles: 1,
throwOnError: true,
}),
);
expect(thrown).toBeInstanceOf(RotatingFileSinkError);
expect(thrown).toMatchObject({ operation: "prune", filePath });
expect((thrown as RotatingFileSinkError).cause).toBeInstanceOf(Error);
});
});