From a3645dc042ad699205ad25e022746ddaadd4c9e9 Mon Sep 17 00:00:00 2001 From: Filip Seman Date: Fri, 6 Mar 2026 10:49:36 +0100 Subject: [PATCH] refactor: extract coverage parser and percent helpers --- dprint.json | 2 +- package.json | 6 +++--- src/context.ts | 34 ++++++++++++++++++++++--------- src/context_test.ts | 15 +++++++++----- src/diff.ts | 23 ++++++++++++--------- src/diff_test.ts | 26 ++++++++++++++++++++++++ src/go.ts | 3 ++- src/index.ts | 43 +++++++++++++++++++++------------------ src/lcov.ts | 5 ++--- src/parser.ts | 23 +++++++++++++++++++++ src/parser_test.ts | 35 ++++++++++++++++++++++++++++++++ src/percent.ts | 19 ++++++++++++++++++ src/percent_test.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++ src/render.ts | 49 ++++++++++++++++++++++++--------------------- src/render_test.ts | 31 ++++++++++++++++++++++++++-- 15 files changed, 285 insertions(+), 77 deletions(-) create mode 100644 src/parser.ts create mode 100644 src/parser_test.ts create mode 100644 src/percent.ts create mode 100644 src/percent_test.ts diff --git a/dprint.json b/dprint.json index 6502028..c3d68b6 100644 --- a/dprint.json +++ b/dprint.json @@ -1,6 +1,6 @@ { "$schema": "https://dprint.dev/schemas/v0.json", - "lineWidth": 100, + "lineWidth": 1000, "useTabs": true, "indentWidth": 4, "yaml": { diff --git a/package.json b/package.json index a13874f..95cda86 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "fmt": "dprint fmt", "fmt:check": "dprint check", "typecheck": "tsc --noEmit", - "test": "bun test", - "test:watch": "bun test --watch", - "test:coverage": "bun test --coverage", + "test": "bun test --concurrent", + "test:watch": "bun test --watch --concurrent", + "test:coverage": "bun test --coverage --concurrent", "lint": "bun run typecheck && bun run fmt:check" }, "dependencies": { diff --git a/src/context.ts b/src/context.ts index 8225087..3f797d7 100644 --- a/src/context.ts +++ b/src/context.ts @@ -2,6 +2,14 @@ import * as github from "@actions/github"; type Context = typeof github.context; +function isPrEvent(context: Context): boolean { + return context.eventName === "pull_request" || context.eventName === "pull_request_target"; +} + +function stripRefsPrefix(ref: string): string { + return ref.replace("refs/heads/", ""); +} + /** * Resolve the PR number from the event context using a priority chain: * @@ -24,7 +32,7 @@ export async function resolvePrNumber( } // 2. Direct PR trigger - if (context.eventName === "pull_request" || context.eventName === "pull_request_target") { + if (isPrEvent(context)) { const num = context.payload.pull_request?.number; if (typeof num === "number") return num; } @@ -83,8 +91,8 @@ export function resolveHeadSha(context: Context = github.context): string { * Resolve the base branch for cache key scoping. * * Under `workflow_run`, `context.ref` points to the *default* branch, not the - * PR base. Use `head_branch` from the triggering run instead. Falls back to the - * explicit `base-branch` input or `main`. + * PR base. Prefer the base ref of the triggering PR when it is available. + * Falls back to the explicit `base-branch` input or the current ref. */ export function resolveBaseBranch( inputBaseBranch: string, @@ -92,15 +100,21 @@ export function resolveBaseBranch( ): string { if (inputBaseBranch) return inputBaseBranch; - if (context.eventName === "pull_request" || context.eventName === "pull_request_target") { + if (isPrEvent(context)) { return context.payload.pull_request?.base?.ref ?? "main"; } if (context.eventName === "workflow_run") { - return context.payload.workflow_run?.head_branch ?? "main"; + const prs: unknown[] | undefined = context.payload.workflow_run?.pull_requests; + if (Array.isArray(prs) && prs.length > 0) { + const first = prs[0] as { base?: { ref?: string; }; }; + if (first.base?.ref) return first.base.ref; + } + + return stripRefsPrefix(context.ref) || "main"; } - return context.ref.replace("refs/heads/", "") || "main"; + return stripRefsPrefix(context.ref) || "main"; } /** @@ -109,15 +123,15 @@ export function resolveBaseBranch( * Under `workflow_run`, the head branch comes from the triggering workflow. */ export function resolveCurrentBranch(context: Context = github.context): string { - if (context.eventName === "pull_request" || context.eventName === "pull_request_target") { + if (isPrEvent(context)) { return context.payload.pull_request?.head?.ref - ?? context.ref.replace("refs/heads/", ""); + ?? stripRefsPrefix(context.ref); } if (context.eventName === "workflow_run") { return context.payload.workflow_run?.head_branch - ?? context.ref.replace("refs/heads/", ""); + ?? stripRefsPrefix(context.ref); } - return context.ref.replace("refs/heads/", ""); + return stripRefsPrefix(context.ref); } diff --git a/src/context_test.ts b/src/context_test.ts index e977b4d..d6c125f 100644 --- a/src/context_test.ts +++ b/src/context_test.ts @@ -116,7 +116,6 @@ describe("resolveHeadSha", () => { test("returns workflow_run.head_sha for workflow_run event", () => { const ctx = makeContext({ eventName: "workflow_run", - sha: "default-sha", payload: { workflow_run: { head_sha: "wr-sha-456" } }, }); expect(resolveHeadSha(ctx)).toBe("wr-sha-456"); @@ -171,10 +170,15 @@ describe("resolveBaseBranch", () => { expect(resolveBaseBranch("", ctx)).toBe("develop"); }); - test("returns workflow_run.head_branch for workflow_run event", () => { + test("returns workflow_run pull request base ref for workflow_run event", () => { const ctx = makeContext({ eventName: "workflow_run", - payload: { workflow_run: { head_branch: "wr-base" } }, + payload: { + workflow_run: { + head_branch: "feature-branch", + pull_requests: [{ base: { ref: "wr-base" } }], + }, + }, }); expect(resolveBaseBranch("", ctx)).toBe("wr-base"); }); @@ -184,12 +188,13 @@ describe("resolveBaseBranch", () => { expect(resolveBaseBranch("", ctx)).toBe("main"); }); - test("returns main when workflow_run has no head_branch", () => { + test("falls back to current ref when workflow_run has no PR base ref", () => { const ctx = makeContext({ eventName: "workflow_run", + ref: "refs/heads/default-branch", payload: { workflow_run: {} }, }); - expect(resolveBaseBranch("", ctx)).toBe("main"); + expect(resolveBaseBranch("", ctx)).toBe("default-branch"); }); test("input override takes precedence over PR payload", () => { diff --git a/src/diff.ts b/src/diff.ts index e9c5542..8d2e1c7 100644 --- a/src/diff.ts +++ b/src/diff.ts @@ -1,3 +1,7 @@ +import { + calculatePercent, + roundDelta, +} from "./percent.js"; import type { CoverageArtifact, CoverageReport, @@ -29,7 +33,7 @@ export function computeFileDiffs( baseCoveredLines: base?.coveredLines ?? null, baseTotalLines: base?.totalLines ?? null, basePercent: base?.percent ?? null, - delta: base ? Math.round((head.percent - base.percent) * 100) / 100 : null, + delta: base ? roundDelta(head.percent, base.percent) : null, }); } @@ -70,7 +74,7 @@ export function buildToolReport( const coveredLines = headFiles.reduce((s, f) => s + f.coveredLines, 0); const totalLines = headFiles.reduce((s, f) => s + f.totalLines, 0); - const percent = totalLines > 0 ? Math.round((coveredLines / totalLines) * 10000) / 100 : 100; + const percent = calculatePercent(coveredLines, totalLines); let baseCoveredLines: number | null = null; let baseTotalLines: number | null = null; @@ -80,10 +84,8 @@ export function buildToolReport( if (baseFiles && baseFiles.length > 0) { baseCoveredLines = baseFiles.reduce((s, f) => s + f.coveredLines, 0); baseTotalLines = baseFiles.reduce((s, f) => s + f.totalLines, 0); - basePercent = baseTotalLines > 0 - ? Math.round((baseCoveredLines / baseTotalLines) * 10000) / 100 - : 100; - delta = Math.round((percent - basePercent) * 100) / 100; + basePercent = calculatePercent(baseCoveredLines, baseTotalLines); + delta = roundDelta(percent, basePercent); } return { @@ -108,15 +110,16 @@ export function buildToolReport( export function buildFullReport(toolReports: ToolCoverageReport[]): CoverageReport { const coveredLines = toolReports.reduce((s, t) => s + t.summary.coveredLines, 0); const totalLines = toolReports.reduce((s, t) => s + t.summary.totalLines, 0); - const percent = totalLines > 0 ? Math.round((coveredLines / totalLines) * 10000) / 100 : 100; + const percent = calculatePercent(coveredLines, totalLines); let basePercent: number | null = null; let delta: number | null = null; + const allToolsComparable = toolReports.every((t) => t.summary.baseTotalLines !== null); const baseCovered = toolReports.reduce((s, t) => s + (t.summary.baseCoveredLines ?? 0), 0); const baseTotal = toolReports.reduce((s, t) => s + (t.summary.baseTotalLines ?? 0), 0); - if (baseTotal > 0) { - basePercent = Math.round((baseCovered / baseTotal) * 10000) / 100; - delta = Math.round((percent - basePercent) * 100) / 100; + if (allToolsComparable && baseTotal > 0) { + basePercent = calculatePercent(baseCovered, baseTotal); + delta = roundDelta(percent, basePercent); } return { diff --git a/src/diff_test.ts b/src/diff_test.ts index 1109843..873955f 100644 --- a/src/diff_test.ts +++ b/src/diff_test.ts @@ -124,4 +124,30 @@ describe("buildFullReport", () => { expect(report.overall.percent).toBe(65); expect(report.tools).toHaveLength(2); }); + + test("omits overall delta when any tool lacks base data", () => { + const withBase = buildToolReport( + "bun", + [{ file: "a.ts", coveredLines: 8, totalLines: 10, percent: 80 }], + { + tool: "bun", + files: [{ file: "a.ts", coveredLines: 7, totalLines: 10, percent: 70 }], + commitSha: "abc123", + branch: "main", + timestamp: "2025-01-01T00:00:00Z", + }, + [], + ); + const withoutBase = buildToolReport( + "go", + [{ file: "b.go", coveredLines: 8, totalLines: 10, percent: 80 }], + null, + [], + ); + + const report = buildFullReport([withBase, withoutBase]); + + expect(report.overall.basePercent).toBeNull(); + expect(report.overall.delta).toBeNull(); + }); }); diff --git a/src/go.ts b/src/go.ts index a871004..2aeee50 100644 --- a/src/go.ts +++ b/src/go.ts @@ -1,3 +1,4 @@ +import { calculatePercent } from "./percent.js"; import type { FileCoverage } from "./types.js"; /** @@ -59,7 +60,7 @@ export function parseGoCover(content: string): FileCoverage[] { const results: FileCoverage[] = []; for (const [file, { covered, total }] of fileMap) { - const percent = total > 0 ? Math.round((covered / total) * 10000) / 100 : 100; + const percent = calculatePercent(covered, total); results.push({ file, coveredLines: covered, totalLines: total, percent }); } diff --git a/src/index.ts b/src/index.ts index 013eb4f..ed0830a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,8 +20,14 @@ import { buildFullReport, buildToolReport, } from "./diff.js"; -import { parseGoCover } from "./go.js"; -import { parseLcov } from "./lcov.js"; +import { + getCoverageParser, + getSupportedCoverageTools, +} from "./parser.js"; +import { + formatPercent, + formatPercentValue, +} from "./percent.js"; import { renderReport } from "./render.js"; import type { ArtifactInput, @@ -69,18 +75,19 @@ function parseFile(tool: string, filePath: string): { files: FileCoverage[]; war } try { - switch (tool) { - case "lcov": - case "bun": - case "node": - return { files: parseLcov(content), warnings }; - case "go": - case "gocover": - return { files: parseGoCover(content), warnings }; - default: - warnings.push(`Unknown tool "${tool}". Supported: bun, lcov, node, go, gocover.`); - return { files: [], warnings }; + const parser = getCoverageParser(tool); + if (!parser) { + warnings.push(`Unknown tool "${tool}". Supported: ${getSupportedCoverageTools().join(", ")}.`); + return { + files: [], + warnings, + }; } + + return { + files: parser(content), + warnings, + }; } catch (err: unknown) { warnings.push( `Failed to parse \`${filePath}\` as ${tool} coverage: ${(err as Error).message}`, @@ -142,8 +149,8 @@ async function run(): Promise { const report = buildToolReport(input.tool, headFiles, baseArtifact, warnings); toolReports.push(report); - // Check for decreases - if (report.summary.delta !== null && report.summary.delta < 0) { + // Match the public contract: any file-level decrease trips the flag. + if (report.files.some((file) => file.delta !== null && file.delta < 0)) { anyDecrease = true; } @@ -172,7 +179,7 @@ async function run(): Promise { const markdown = renderReport(fullReport, marker, colorize, commitInfo); // Set outputs - core.setOutput("overall-coverage", fullReport.overall.percent.toFixed(2)); + core.setOutput("overall-coverage", formatPercentValue(fullReport.overall.percent)); core.setOutput("coverage-decreased", anyDecrease ? "true" : "false"); // Post / update PR comment @@ -209,9 +216,7 @@ async function run(): Promise { // Threshold check if (threshold > 0 && fullReport.overall.percent < threshold) { core.setFailed( - `Overall coverage ${ - fullReport.overall.percent.toFixed(2) - }% is below threshold ${threshold}%`, + `Overall coverage ${formatPercent(fullReport.overall.percent)} is below threshold ${threshold}%`, ); return; } diff --git a/src/lcov.ts b/src/lcov.ts index fae7046..4ce2bc8 100644 --- a/src/lcov.ts +++ b/src/lcov.ts @@ -1,3 +1,4 @@ +import { calculatePercent } from "./percent.js"; import type { FileCoverage } from "./types.js"; /** @@ -62,9 +63,7 @@ export function parseLcov(content: string): FileCoverage[] { coveredLines = lhProvided ?? 0; } - const percent = totalLines > 0 - ? Math.round((coveredLines / totalLines) * 10000) / 100 - : 100; + const percent = calculatePercent(coveredLines, totalLines); results.push({ file, coveredLines, totalLines, percent }); } diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..47b22ca --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,23 @@ +import { parseGoCover } from "./go.js"; +import { parseLcov } from "./lcov.js"; +import type { FileCoverage } from "./types.js"; + +export type CoverageParser = (content: string) => FileCoverage[]; + +const COVERAGE_PARSER_ENTRIES: [string, CoverageParser][] = [ + ["lcov", parseLcov], + ["bun", parseLcov], + ["node", parseLcov], + ["go", parseGoCover], + ["gocover", parseGoCover], +]; + +const COVERAGE_PARSERS = new Map(COVERAGE_PARSER_ENTRIES); + +export function getCoverageParser(tool: string): CoverageParser | undefined { + return COVERAGE_PARSERS.get(tool); +} + +export function getSupportedCoverageTools(): string[] { + return COVERAGE_PARSER_ENTRIES.map(([tool]) => tool); +} diff --git a/src/parser_test.ts b/src/parser_test.ts new file mode 100644 index 0000000..fecbdce --- /dev/null +++ b/src/parser_test.ts @@ -0,0 +1,35 @@ +import { + describe, + expect, + test, +} from "bun:test"; + +import { parseGoCover } from "./go"; +import { parseLcov } from "./lcov"; +import { + getCoverageParser, + getSupportedCoverageTools, +} from "./parser"; + +describe("getCoverageParser", () => { + test("returns the LCOV parser for LCOV aliases", () => { + expect(getCoverageParser("lcov")).toBe(parseLcov); + expect(getCoverageParser("bun")).toBe(parseLcov); + expect(getCoverageParser("node")).toBe(parseLcov); + }); + + test("returns the Go parser for Go aliases", () => { + expect(getCoverageParser("go")).toBe(parseGoCover); + expect(getCoverageParser("gocover")).toBe(parseGoCover); + }); + + test("returns undefined for unsupported tools", () => { + expect(getCoverageParser("python")).toBeUndefined(); + }); +}); + +describe("getSupportedCoverageTools", () => { + test("returns the registered tools in warning order", () => { + expect(getSupportedCoverageTools()).toEqual(["lcov", "bun", "node", "go", "gocover"]); + }); +}); diff --git a/src/percent.ts b/src/percent.ts new file mode 100644 index 0000000..10f0ba4 --- /dev/null +++ b/src/percent.ts @@ -0,0 +1,19 @@ +const PERCENT_SCALE = 10000; +const PERCENT_DIVISOR = 100; + +export function calculatePercent(covered: number, total: number): number { + if (total <= 0) return 100; + return Math.round((covered / total) * PERCENT_SCALE) / PERCENT_DIVISOR; +} + +export function formatPercentValue(percent: number): string { + return percent.toFixed(2); +} + +export function formatPercent(percent: number): string { + return `${formatPercentValue(percent)}%`; +} + +export function roundDelta(a: number, b: number): number { + return Math.round((a - b) * 100) / 100; +} diff --git a/src/percent_test.ts b/src/percent_test.ts new file mode 100644 index 0000000..eb0edba --- /dev/null +++ b/src/percent_test.ts @@ -0,0 +1,48 @@ +import { + describe, + expect, + test, +} from "bun:test"; + +import { + calculatePercent, + formatPercent, + formatPercentValue, + roundDelta, +} from "./percent"; + +describe("calculatePercent", () => { + test("rounds coverage to two decimal places", () => { + expect(calculatePercent(13, 16)).toBe(81.25); + expect(calculatePercent(1, 3)).toBe(33.33); + }); + + test("treats empty totals as fully covered", () => { + expect(calculatePercent(0, 0)).toBe(100); + expect(calculatePercent(5, 0)).toBe(100); + }); +}); + +describe("formatPercent helpers", () => { + test("formats raw percent values without a suffix", () => { + expect(formatPercentValue(80)).toBe("80.00"); + expect(formatPercentValue(33.333)).toBe("33.33"); + }); + + test("formats percent values with a suffix", () => { + expect(formatPercent(80)).toBe("80.00%"); + expect(formatPercent(-10)).toBe("-10.00%"); + }); +}); + +describe("roundDelta", () => { + test("rounds the signed difference to two decimal places", () => { + expect(roundDelta(90, 70)).toBe(20); + expect(roundDelta(70, 90)).toBe(-20); + expect(roundDelta(83.333, 80)).toBe(3.33); + }); + + test("returns zero for equal values", () => { + expect(roundDelta(75, 75)).toBe(0); + }); +}); diff --git a/src/render.ts b/src/render.ts index f7456b1..fefb1a9 100644 --- a/src/render.ts +++ b/src/render.ts @@ -1,3 +1,7 @@ +import { + formatPercent, + formatPercentValue, +} from "./percent.js"; import type { CoverageReport, ToolCoverageReport, @@ -10,6 +14,13 @@ export interface CommitInfo { repo: string; } +function commitLink(info: CommitInfo, sha: string): { short: string; url: string; } { + return { + short: sha.slice(0, 7), + url: `https://github.com/${info.owner}/${info.repo}/commit/${sha}`, + }; +} + function pad(n: number, width: number): string { const s = String(n); return " ".repeat(Math.max(0, width - s.length)) + s; @@ -18,7 +29,7 @@ function pad(n: number, width: number): string { function deltaStr(delta: number | null, colorize: boolean): string { if (delta === null) return ""; const sign = delta >= 0 ? "+" : ""; - const pct = `${sign}${delta.toFixed(2)}%`; + const pct = `${sign}${formatPercent(delta)}`; if (!colorize) return ` (${pct})`; if (delta > 0) return ` [+] ${pct}`; if (delta < 0) return ` [-] ${pct}`; @@ -44,7 +55,7 @@ function renderToolSection(report: ToolCoverageReport, colorize: boolean): strin const maxTotal = Math.max(...report.files.map((f) => String(f.totalLines).length), 1); for (const f of report.files) { - const pct = `${f.percent.toFixed(2)}%`.padStart(7); + const pct = formatPercent(f.percent).padStart(7); const covered = pad(f.coveredLines, maxCovered); const total = pad(f.totalLines, maxTotal); const delta = deltaStr(f.delta, colorize); @@ -55,7 +66,7 @@ function renderToolSection(report: ToolCoverageReport, colorize: boolean): strin const totalDelta = deltaStr(s.delta, colorize); lines.push(""); - lines.push(`${toolLabel} Coverage: ${s.percent.toFixed(2)}%${totalDelta}`); + lines.push(`${toolLabel} Coverage: ${formatPercent(s.percent)}${totalDelta}`); return lines.join("\n"); } @@ -83,8 +94,8 @@ function renderCoverageDiff(report: CoverageReport): string | null { function fmtPctDiff(d: number | null): string { if (d === null) return ""; - if (d === 0) return "0.00%"; - return `${d > 0 ? "+" : ""}${d.toFixed(2)}%`; + if (d === 0) return formatPercent(d); + return `${d > 0 ? "+" : ""}${formatPercent(d)}`; } const hitsDelta = o.coveredLines - baseCovered; @@ -94,8 +105,8 @@ function renderCoverageDiff(report: CoverageReport): string | null { { prefix: " ", label: "Coverage", - base: `${o.basePercent.toFixed(2)}%`, - head: `${o.percent.toFixed(2)}%`, + base: formatPercent(o.basePercent), + head: formatPercent(o.percent), diff: fmtPctDiff(o.delta), }, "sep", @@ -136,9 +147,7 @@ function renderCoverageDiff(report: CoverageReport): string | null { head: string, diff: string, ): string { - return `${prefix} ${label.padEnd(lw)} ${base.padStart(bw)} ${head.padStart(hw)} ${ - diff.padStart(dw) - }`; + return `${prefix} ${label.padEnd(lw)} ${base.padStart(bw)} ${head.padStart(hw)} ${diff.padStart(dw)}`; } function fmtRow(r: RowData): string { @@ -191,22 +200,16 @@ export function renderReport( parts.push("## Coverage Report\n"); // Project coverage summary line - const pct = report.overall.percent.toFixed(2); + const pct = formatPercentValue(report.overall.percent); if (commitInfo?.baseSha) { - const headShort = commitInfo.sha.slice(0, 7); - const headUrl = - `https://github.com/${commitInfo.owner}/${commitInfo.repo}/commit/${commitInfo.sha}`; - const baseShort = commitInfo.baseSha.slice(0, 7); - const baseUrl = - `https://github.com/${commitInfo.owner}/${commitInfo.repo}/commit/${commitInfo.baseSha}`; + const head = commitLink(commitInfo, commitInfo.sha); + const base = commitLink(commitInfo, commitInfo.baseSha); parts.push( - `Project coverage is ${pct}%. Comparing base ([\`${baseShort}\`](${baseUrl})) to head ([\`${headShort}\`](${headUrl})).\n`, + `Project coverage is ${pct}%. Comparing base ([\`${base.short}\`](${base.url})) to head ([\`${head.short}\`](${head.url})).\n`, ); } else if (commitInfo) { - const headShort = commitInfo.sha.slice(0, 7); - const headUrl = - `https://github.com/${commitInfo.owner}/${commitInfo.repo}/commit/${commitInfo.sha}`; - parts.push(`Project coverage is ${pct}%. Commit [\`${headShort}\`](${headUrl}).\n`); + const head = commitLink(commitInfo, commitInfo.sha); + parts.push(`Project coverage is ${pct}%. Commit [\`${head.short}\`](${head.url}).\n`); } else { parts.push(`Project coverage is ${pct}%.\n`); } @@ -229,7 +232,7 @@ export function renderReport( if (report.tools.length > 1) { const o = report.overall; const totalDelta = deltaStr(o.delta, colorize); - parts.push(`**Total Coverage: ${o.percent.toFixed(2)}%${totalDelta}**\n`); + parts.push(`**Total Coverage: ${formatPercent(o.percent)}${totalDelta}**\n`); } parts.push("---"); diff --git a/src/render_test.ts b/src/render_test.ts index 987b06e..2ddbf26 100644 --- a/src/render_test.ts +++ b/src/render_test.ts @@ -103,6 +103,33 @@ describe("renderReport", () => { expect(md).toContain("**Total Coverage: 65.00%**"); }); + test("omits diff table when overall baseline is incomplete", () => { + const withBase = buildToolReport( + "bun", + [{ file: "a.ts", coveredLines: 8, totalLines: 10, percent: 80 }], + { + tool: "bun", + files: [{ file: "a.ts", coveredLines: 7, totalLines: 10, percent: 70 }], + commitSha: "abc123", + branch: "main", + timestamp: "2025-01-01T00:00:00Z", + }, + [], + ); + const withoutBase = buildToolReport( + "go", + [{ file: "b.go", coveredLines: 8, totalLines: 10, percent: 80 }], + null, + [], + ); + const fullReport = buildFullReport([withBase, withoutBase]); + const md = renderReport(fullReport, "", true); + + expect(md).not.toContain("Coverage Diff"); + expect(md).toContain("Bun Coverage: 80.00% [+] +10.00%"); + expect(md).toContain("Go Coverage: 80.00%"); + }); + test("renders project coverage with base and head commit links", () => { const head: FileCoverage[] = [ { file: "src/index.ts", coveredLines: 8, totalLines: 10, percent: 80 }, @@ -211,7 +238,7 @@ describe("renderReport", () => { branch: "main", timestamp: "2025-01-01T00:00:00Z", }; - + const toolReport = buildToolReport("bun", head, base, []); const fullReport = buildFullReport([toolReport]); const md = renderReport(fullReport, "", true); @@ -234,7 +261,7 @@ describe("renderReport", () => { branch: "main", timestamp: "2025-01-01T00:00:00Z", }; - + const toolReport = buildToolReport("bun", head, base, []); const fullReport = buildFullReport([toolReport]); const md = renderReport(fullReport, "", true);