175 lines · 5.9 KB · TypeScript
RawDownload
// @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);  });});