Ctrl K
ComunidadGuías…

test/scripts/coverage-check.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { join } from "node:path";
import { assert, assertStringIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import { spy } from "@std/testing/mock";
import {
  type CoverageFailure,
  findCoverageFailures,
  printCoverageReport,
  printFailureSummary,
} from "#scripts/coverage-check.ts";
import { projectRoot } from "#scripts/project-root.ts";
import { withEnvironment } from "#scripts/test-environment.ts";
import { expectIncludesAll } from "#test-utils/assertions.ts";
import { withTempDir } from "#test-utils/files.ts";

/** Wrap raw lcov lines into one record. */
const recordOf = (lines: string[]): string =>
  `${lines.join("\n")}\nend_of_record\n`;

const linesOf = (source: string, data: string[]): string =>
  recordOf([`SF:${source}`, ...data]);

const failingRecord = (source: string): string =>
  linesOf(source, [
    "FN:1,main",
    "FNDA:5,main",
    "FNF:1",
    "FNH:1",
    "BRDA:3,0,0,1",
    "BRDA:5,0,0,0",
    "BRDA:5,2,0,0",
    "BRDA:6,1,0,-",
    "BRDA:7,0,0,1",
    "BRF:5",
    "BRH:2",
    "DA:1,9",
    "DA:2,0",
    "DA:4,3",
    "DA:12,0",
    "LF:4",
    "LH:2",
  ]);

const cleanRecord = (source: string): string =>
  linesOf(source, ["DA:1,7", "LF:1", "LH:1", "BRDA:1,0,0,1", "BRF:1", "BRH:1"]);

const runWithCapturedErrors = async <Result>(
  run: () => Promise<Result>,
): Promise<{ errors: string[]; result: Result }> => {
  const errors = spy(console, "error");
  try {
    const result = await run();
    return {
      errors: errors.calls.map((call) => String(call.args[0])),
      result,
    };
  } finally {
    errors.restore();
  }
};

describe("findCoverageFailures", () => {
  test("returns null when no record names a source file", () => {
    expect(findCoverageFailures("end_of_record\nDA:1,0\nend_of_record\n")).toBe(
      null,
    );
  });

  test("reports uncovered lines and branches with their line numbers", () => {
    const failures = findCoverageFailures(failingRecord("/tmp/probe.ts"))!;

    expect(failures.length).toBe(1);
    const failure: CoverageFailure = failures[0]!;
    expect(failure.file).toBe("/tmp/probe.ts");
    expect(failure.lines).toEqual({
      covered: 2,
      total: 4,
      uncovered: [2, 12],
    });
    expect(failure.branches).toEqual({
      covered: 2,
      total: 5,
      // Line 5 is named by two zero branches and must appear once.
      uncovered: [5, 6],
    });
  });

  test("keeps a relative SF path as the file name", () => {
    const failures = findCoverageFailures(
      linesOf("src/rel.ts", ["DA:1,0", "LF:1", "LH:0"]),
    )!;

    expect(failures[0]?.file).toBe("src/rel.ts");
    expect(failures[0]?.sourceFile).toBe("src/rel.ts");
  });

  test("drops files that the gate excludes by name", () => {
    const excluded = failingRecord(
      `${projectRoot}/scripts/compact-test-reporter.ts`,
    );

    expect(findCoverageFailures(excluded + cleanRecord("src/ok.ts"))).toEqual(
      [],
    );
  });

  test("reports a lines-only failure when branch totals are healthy", () => {
    const failures = findCoverageFailures(
      linesOf("/tmp/lines-only.ts", [
        "DA:1,4",
        "DA:2,0",
        "LF:2",
        "LH:1",
        "BRF:1",
        "BRH:1",
      ]),
    )!;

    expect(failures[0]?.lines?.uncovered).toEqual([2]);
    expect(failures[0]?.branches).toBe(undefined);
  });

  test("counts fewer hits than found lines as gaps with no line numbers", () => {
    const failures = findCoverageFailures(
      linesOf("/tmp/unknown-lines.ts", [
        "DA:1,9",
        "DA:4,8",
        "LF:3",
        "LH:2",
        "BRF:1",
        "BRH:1",
      ]),
    )!;

    expect(failures[0]?.lines).toEqual({ covered: 2, total: 3, uncovered: [] });
  });

  test("reports a branches-only failure when line totals are missing", () => {
    const failures = findCoverageFailures(
      linesOf("/tmp/branches-only.ts", ["BRDA:5,0,0,0", "BRF:2", "BRH:1"]),
    )!;

    expect(failures[0]?.branches).toEqual({
      covered: 1,
      total: 2,
      uncovered: [5],
    });
    expect(failures[0]?.lines).toBe(undefined);
  });

  test("reports nothing for records whose metrics are complete", () => {
    const failures = findCoverageFailures(
      cleanRecord("src/clean.ts") + failingRecord("/x.ts"),
    )!;

    expect(failures.length).toBe(1);
    expect(failures[0]?.file).toBe("/x.ts");
  });
});

describe("printFailureSummary", () => {
  test("prints source lines around each uncovered line", async () => {
    await withTempDir(async (dir) => {
      const source = join(dir, "sourced.txt");
      await Deno.writeTextFile(
        source,
        Array.from({ length: 12 }, (_, i) => `row number ${i + 1}`).join("\n"),
      );

      const failure: CoverageFailure = {
        branches: { covered: 1, total: 2, uncovered: [10] },
        file: "sourced.txt",
        lines: { covered: 9, total: 12, uncovered: [2, 10] },
        sourceFile: source,
      };
      const { errors } = await runWithCapturedErrors(() =>
        printFailureSummary(failure),
      );
      const text = errors.join("\n");
      expectIncludesAll(text, [
        "missing 2, 10",
        "missing 10",
        "| row number 2",
        "| row number 10",
        // The jump from the lines around 2 to the lines around 10 is shown.
        "    ...",
      ]);
    });
  });

  test("prints no snippet block when the source file is missing", async () => {
    const failure: CoverageFailure = {
      branches: { covered: 0, total: 1, uncovered: [1] },
      file: "gone.ts",
      sourceFile: "/nonexistent/gone.ts",
    };
    const { errors } = await runWithCapturedErrors(() =>
      printFailureSummary(failure),
    );
    const text = errors.join("\n");
    assertStringIncludes(text, "missing 1");
    assert(!text.includes("snippets"));
  });

  test("prints snippets for a failure that only has branches", async () => {
    await withTempDir(async (dir) => {
      const source = join(dir, "branchy.txt");
      await Deno.writeTextFile(source, "alpha\nbeta\ngamma\n");

      const failure: CoverageFailure = {
        branches: { covered: 0, total: 1, uncovered: [2] },
        file: "branchy.txt",
        sourceFile: source,
      };
      const { errors } = await runWithCapturedErrors(() =>
        printFailureSummary(failure),
      );
      assertStringIncludes(errors.join("\n"), "| beta");
    });
  });

  test("prints the unknown range when no line is dead", async () => {
    await withTempDir(async (dir) => {
      const source = join(dir, "unknown.txt");
      await Deno.writeTextFile(source, "only one line here\n");

      const failure: CoverageFailure = {
        file: "unknown.txt",
        lines: { covered: 2, total: 3, uncovered: [] },
        sourceFile: source,
      };
      const { errors } = await runWithCapturedErrors(() =>
        printFailureSummary(failure),
      );
      assertStringIncludes(errors.join("\n"), "missing unknown");
    });
  });

  test("caps the visible snippet lines and counts the rest", async () => {
    await withTempDir(async (dir) => {
      const source = join(dir, "long.txt");
      await Deno.writeTextFile(
        source,
        `${Array.from({ length: 40 }, (_, i) => `line ${i + 1}`).join("\n")}\n`,
      );

      const failure: CoverageFailure = {
        file: "long.txt",
        // Uncovered lines beyond the first line and past the file end.
        lines: {
          covered: 10,
          total: 40,
          uncovered: [2, ...Array.from({ length: 27 }, (_, i) => i + 3), 55],
        },
        sourceFile: source,
      };
      const { errors } = await runWithCapturedErrors(() =>
        printFailureSummary(failure),
      );
      const text = errors.join("\n");
      expectIncludesAll(text, ["| line 1", "more source lines"]);
    });
  });
});

describe("printCoverageReport", () => {
  const withLinesFailing = (): CoverageFailure[] =>
    findCoverageFailures(
      linesOf("/tmp/x.ts", ["DA:1,0", "LF:1", "LH:0", "BRF:0", "BRH:0"]),
    )!;

  test("returns 0 and names the clean state for no failures", async () => {
    const logged = spy(console, "log");
    try {
      expect(await printCoverageReport([])).toBe(0);
    } finally {
      logged.restore();
    }
    assertStringIncludes(
      logged.calls[0]?.args[0],
      "All files have 100% line and branch coverage",
    );
  });

  test("returns 1 for missing coverage data", async () => {
    expect(await printCoverageReport(null)).toBe(1);
  });

  test("returns 1 for failures and prints the rules", async () => {
    const { errors, result } = await runWithCapturedErrors(() =>
      printCoverageReport(withLinesFailing()),
    );

    expect(result).toBe(1);
    assertStringIncludes(errors.join("\n"), "Test quality rules");
  });

  test("prints no GitHub annotations outside a CI run", async () => {
    const { errors } = await withEnvironment(
      { GITHUB_ACTIONS: undefined },
      () =>
        runWithCapturedErrors(() => printCoverageReport(withLinesFailing())),
    );

    assert(!errors.join("\n").includes("::error"));
  });

  test("prints GitHub annotations in a CI run and caps them", async () => {
    const entries = Array.from(
      { length: 101 },
      (_, i) => `BRDA:${i + 1},0,0,0`,
    );
    const failure = findCoverageFailures(
      linesOf("/tmp/many.ts", [
        "DA:1,0",
        "LF:1",
        "LH:0",
        ...entries,
        "BRF:101",
        "BRH:0",
      ]) + linesOf("/tmp/branch-only.ts", ["BRDA:2,0,0,0", "BRF:1", "BRH:0"]),
    );

    const { errors } = await withEnvironment({ GITHUB_ACTIONS: "1" }, () =>
      runWithCapturedErrors(() => printCoverageReport(failure!)),
    );

    const text = errors.join("\n");
    expectIncludesAll(text, [
      "::error file=",
      "additional coverage annotations omitted",
    ]);
  });
});