From 904910f8759397d4e75293f18e2b9cb13dcfcddc Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sat, 3 Jan 2026 10:50:06 +0100 Subject: [PATCH 01/13] test: Update snapshots to expected markdown output --- .../core/__snapshots__/basic-coverage.snap.md | 14 +++++++--- tests/core/__snapshots__/glob-filter.snap.md | 18 +++++++++---- .../core/__snapshots__/merge-reports.snap.md | 20 +++++++++----- .../__snapshots__/multiple-packages.snap.md | 26 +++++++++---------- .../ellipsis-leading-1-line.snap.md | 17 +++++++++--- .../__snapshots__/ellipsis-leading.snap.md | 17 +++++++++--- .../ellipsis-trailing-1-line.snap.md | 17 +++++++++--- .../__snapshots__/ellipsis-trailing.snap.md | 15 ++++++++--- .../__snapshots__/empty-package.snap.md | 13 +++++++++- .../__snapshots__/file-not-found.snap.md | 15 ++++++++--- .../markdown/__snapshots__/gap-1-line.snap.md | 15 ++++++++--- .../__snapshots__/gap-2-plus-lines.snap.md | 15 ++++++++--- .../line-exceeds-file-length.snap.md | 15 ++++++++--- .../__snapshots__/line-gaps-ellipsis.snap.md | 20 +++++++++----- .../multiple-files-same-package.snap.md | 17 +++++++++--- .../__snapshots__/multiple-packages.snap.md | 19 ++++++++++---- .../__snapshots__/simple-report.snap.md | 16 +++++++++--- .../__snapshots__/truncation-files.snap.md | 22 +++++++++++----- .../__snapshots__/truncation-minimal.snap.md | 22 +++++++++++----- .../__snapshots__/truncation-none.snap.md | 17 +++++++++--- .../__snapshots__/truncation-packages.snap.md | 23 +++++++++++----- 21 files changed, 277 insertions(+), 96 deletions(-) diff --git a/tests/core/__snapshots__/basic-coverage.snap.md b/tests/core/__snapshots__/basic-coverage.snap.md index a0d4a1f..c9aa125 100644 --- a/tests/core/__snapshots__/basic-coverage.snap.md +++ b/tests/core/__snapshots__/basic-coverage.snap.md @@ -1,9 +1,15 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75.00%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) -**app** (LineCoverage: 75%, BranchCoverage: 100%) +--- -
src/calculator.ts +## PR Coverage +3/4 changed lines covered (Lines: 75%, Branches: 100%) + +### app +
+🔴 src/calculator.ts — 3/4 changed lines covered (Lines: 75%, Branches: 100%) ```typescript 1 🟩 export function add(a: number, b: number): number { @@ -14,4 +20,6 @@ ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/core/__snapshots__/glob-filter.snap.md b/tests/core/__snapshots__/glob-filter.snap.md index 5c3fd32..a481fa9 100644 --- a/tests/core/__snapshots__/glob-filter.snap.md +++ b/tests/core/__snapshots__/glob-filter.snap.md @@ -1,16 +1,24 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.00%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) -**filtered** (LineCoverage: 50%, BranchCoverage: 100%) +--- -
src/include/wanted.ts +## PR Coverage +1/2 changed lines covered (Lines: 50%, Branches: 100%) + +### filtered +
+🔴 src/include/wanted.ts — 1/2 changed lines covered (Lines: 50%, Branches: 100%) ```typescript 1 🟩 export function wanted(): string { 2 🟥 return 'I am wanted' - 3 ⬜ } - 4 ⬜ + 3 ⬛ } + 4 ⬛ ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/core/__snapshots__/merge-reports.snap.md b/tests/core/__snapshots__/merge-reports.snap.md index a5f66be..02dffef 100644 --- a/tests/core/__snapshots__/merge-reports.snap.md +++ b/tests/core/__snapshots__/merge-reports.snap.md @@ -1,9 +1,15 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) -**shared** (LineCoverage: 60%, BranchCoverage: 100%) +--- -
src/shared.ts +## PR Coverage +4/5 changed lines covered (Lines: 80%, Branches: 100%) + +### shared +
+🔴 src/shared.ts — 4/5 changed lines covered (Lines: 80%, Branches: 100%) ```typescript 1 🟩 export function sharedOne(): number { @@ -11,9 +17,11 @@ 3 🟥 } 4 🟩 export function sharedTwo(): number { 5 🟥 return 2 - 6 ⬜ } - 7 ⬜ + 6 ⬛ } + 7 ⬛ ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/core/__snapshots__/multiple-packages.snap.md b/tests/core/__snapshots__/multiple-packages.snap.md index 57cffd0..c762ad4 100644 --- a/tests/core/__snapshots__/multiple-packages.snap.md +++ b/tests/core/__snapshots__/multiple-packages.snap.md @@ -1,28 +1,28 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.00%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) -**core** (LineCoverage: 67%, BranchCoverage: 100%) +--- -
src/core/math.ts +## PR Coverage +5/6 changed lines covered (Lines: 83.33%, Branches: 100%) + +### core +
+🔴 src/core/math.ts — 2/3 changed lines covered (Lines: 67.00%, Branches: 100%) ```typescript 1 🟩 export function add(a: number, b: number): number { 2 🟩 return a + b 3 🟥 } - 4 ⬜ + 4 ⬛ ```
-**utils** (LineCoverage: 33%, BranchCoverage: 100%) +### utils -
src/utils/format.ts +✓ 🟢 src/utils/format.ts — 3/3 changed lines covered -```typescript - 1 🟩 export function formatNumber(n: number): string { - 2 🟥 return n.toFixed(2) - 3 🟥 } - 4 ⬜ -``` -
+--- -🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md index ece1de3..3e10451 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md @@ -1,11 +1,18 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75.00%25-green.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**TestPackage** (LineCoverage: 75%) +--- -
src/test.ts +## PR Coverage +3/4 changed lines covered (Lines: 75%, Branches: n/a) + +### TestPackage +
+🔴 src/test.ts — 3/4 changed lines covered (Lines: 75%, Branches: n/a) ```typescript - 1 ⬜ some content in line 1 + 1 ⬛ some content in line 1 2 🟩 some content in line 2 3 🟥 some content in line 3 4 🟩 some content in line 4 @@ -13,4 +20,6 @@ ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/ellipsis-leading.snap.md b/tests/markdown/__snapshots__/ellipsis-leading.snap.md index 662bcae..2781320 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading.snap.md @@ -1,16 +1,25 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.00%25-yellowgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**TestPackage** (LineCoverage: 50%) +--- -
src/test.ts +## PR Coverage +1/2 changed lines covered (Lines: 50%, Branches: n/a) + +### TestPackage +
+🔴 src/test.ts — 1/2 changed lines covered (Lines: 50%, Branches: n/a) ```typescript ... - 4 ⬜ some content in line 4 + 4 ⬛ some content in line 4 5 🟥 some content in line 5 6 🟩 some content in line 6 ... ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md index f8bb1e3..006c755 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md @@ -1,16 +1,25 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-80.00%25-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**TestPackage** (LineCoverage: 80%) +--- -
src/test.ts +## PR Coverage +4/5 changed lines covered (Lines: 80%, Branches: n/a) + +### TestPackage +
+🔴 src/test.ts — 4/5 changed lines covered (Lines: 80%, Branches: n/a) ```typescript 1 🟩 some content in line 1 2 🟩 some content in line 2 3 🟥 some content in line 3 4 🟩 some content in line 4 - 5 ⬜ some content in line 5 + 5 ⬛ some content in line 5 ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md index 7123dc1..2c3fe15 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md @@ -1,8 +1,15 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**TestPackage** (LineCoverage: 67%) +--- -
src/test.ts +## PR Coverage +2/3 changed lines covered (Lines: 67%, Branches: n/a) + +### TestPackage +
+🔴 src/test.ts — 2/3 changed lines covered (Lines: 67%, Branches: n/a) ```typescript 1 🟩 some content in line 1 @@ -12,4 +19,6 @@ ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/empty-package.snap.md b/tests/markdown/__snapshots__/empty-package.snap.md index f76bfc9..9246f94 100644 --- a/tests/markdown/__snapshots__/empty-package.snap.md +++ b/tests/markdown/__snapshots__/empty-package.snap.md @@ -1 +1,12 @@ -✅ All lines are covered! \ No newline at end of file +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) + +--- + +## PR Coverage + +0/0 changed lines covered + +--- + +Generated by `coverage-pr-comment` diff --git a/tests/markdown/__snapshots__/file-not-found.snap.md b/tests/markdown/__snapshots__/file-not-found.snap.md index c099c52..c9e4f3d 100644 --- a/tests/markdown/__snapshots__/file-not-found.snap.md +++ b/tests/markdown/__snapshots__/file-not-found.snap.md @@ -1,8 +1,15 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**TestPackage** (LineCoverage: 0%) +--- -
src/missing.ts +## PR Coverage +0/2 changed lines covered (Lines: 0%, Branches: n/a) + +### TestPackage +
+🔴 src/missing.ts — 0/2 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 🟥 @@ -10,4 +17,6 @@ ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/gap-1-line.snap.md b/tests/markdown/__snapshots__/gap-1-line.snap.md index 9a18a12..1063082 100644 --- a/tests/markdown/__snapshots__/gap-1-line.snap.md +++ b/tests/markdown/__snapshots__/gap-1-line.snap.md @@ -1,8 +1,15 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**TestPackage** (LineCoverage: 60%) +--- -
src/test.ts +## PR Coverage +3/5 changed lines covered (Lines: 60%, Branches: n/a) + +### TestPackage +
+🔴 src/test.ts — 3/5 changed lines covered (Lines: 60%, Branches: n/a) ```typescript 1 🟩 some content in line 1 @@ -13,4 +20,6 @@ ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md index 24e94b5..6d65765 100644 --- a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md +++ b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md @@ -1,8 +1,15 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**TestPackage** (LineCoverage: 67%) +--- -
src/test.ts +## PR Coverage +4/6 changed lines covered (Lines: 67%, Branches: n/a) + +### TestPackage +
+🔴 src/test.ts — 4/6 changed lines covered (Lines: 67%, Branches: n/a) ```typescript 1 🟩 some content in line 1 @@ -16,4 +23,6 @@ ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md index 9d13d3d..37dc8c8 100644 --- a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md +++ b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md @@ -1,8 +1,15 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**TestPackage** (LineCoverage: 67%) +--- -
src/short.ts +## PR Coverage +2/3 changed lines covered (Lines: 67%, Branches: n/a) + +### TestPackage +
+🔴 src/short.ts — 2/3 changed lines covered (Lines: 67%, Branches: n/a) ```typescript ... @@ -12,4 +19,6 @@ ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md index 0be87f4..ad31dd8 100644 --- a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md +++ b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md @@ -1,20 +1,28 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-40.00%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0.00%25-red.svg?style=flat) -**GapsPackage** (LineCoverage: 40%, BranchCoverage: 0%) +--- -
src/gaps.rs +## PR Coverage +2/5 changed lines covered (Lines: 40%, Branches: 0%) + +### GapsPackage +
+🔴 src/gaps.rs — 2/5 changed lines covered (Lines: 40%, Branches: 0%) ```rust ... - 9 ⬜ some content in line 9 + 9 ⬛ some content in line 9 10 🟥 some content in line 10 11 🟥 some content in line 11 - 12 ⬜ some content in line 12 + 12 ⬛ some content in line 12 ... - 49 ⬜ some content in line 49 + 49 ⬛ some content in line 49 50 🟨 some content in line 50 ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md index ba1a590..2f8f685 100644 --- a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md +++ b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md @@ -1,9 +1,18 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0.00%25-red.svg?style=flat) -**MultiFilePackage** (LineCoverage: 60%, BranchCoverage: 0%) +--- -
src/file2.py +## PR Coverage +3/5 changed lines covered (Lines: 60%, Branches: 0%) + +### MultiFilePackage + +✓ 🟢 src/file1.py — 2/2 changed lines covered + +
+🔴 src/file2.py — 1/3 changed lines covered (Lines: 33%, Branches: 0%) ```python 1 🟥 some content in line 1 @@ -12,4 +21,6 @@ ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/multiple-packages.snap.md b/tests/markdown/__snapshots__/multiple-packages.snap.md index cf9df69..f01916a 100644 --- a/tests/markdown/__snapshots__/multiple-packages.snap.md +++ b/tests/markdown/__snapshots__/multiple-packages.snap.md @@ -1,17 +1,26 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**Package.Tests** (LineCoverage: 0%) +--- -
tests/test_utils.cs +## PR Coverage +0/2 changed lines covered (Lines: 0%, Branches: n/a) + +### Package.Tests +
+🔴 tests/test_utils.cs — 0/2 changed lines covered (Lines: 0%, Branches: n/a) ```csharp ... - 4 ⬜ some content in line 4 + 4 ⬛ some content in line 4 5 🟥 some content in line 5 6 🟥 some content in line 6 - 7 ⬜ some content in line 7 + 7 ⬛ some content in line 7 ... ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/simple-report.snap.md b/tests/markdown/__snapshots__/simple-report.snap.md index 57bd921..87d973d 100644 --- a/tests/markdown/__snapshots__/simple-report.snap.md +++ b/tests/markdown/__snapshots__/simple-report.snap.md @@ -1,18 +1,26 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75.00%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0.00%25-red.svg?style=flat) -**TestPackage** (LineCoverage: 75%, BranchCoverage: 0%) +--- -
src/example.ts +## PR Coverage +3/4 changed lines covered (Lines: 75%, Branches: 0%) + +### TestPackage +
+🔴 src/example.ts — 3/4 changed lines covered (Lines: 75%, Branches: 0%) ```typescript 1 🟩 some content in line 1 2 🟩 some content in line 2 3 🟥 some content in line 3 4 🟨 some content in line 4 - 5 ⬜ some content in line 5 + 5 ⬛ some content in line 5 ... ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/truncation-files.snap.md b/tests/markdown/__snapshots__/truncation-files.snap.md index 0ef9607..d56b6e2 100644 --- a/tests/markdown/__snapshots__/truncation-files.snap.md +++ b/tests/markdown/__snapshots__/truncation-files.snap.md @@ -1,23 +1,33 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**MultiFilePkg** (LineCoverage: 0%) +--- -
file1.ts +## PR Coverage +0/1 changed lines covered (Lines: 0%, Branches: n/a) + +### MultiFilePkg +
+🔴 file1.ts — 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 🟥 some content in line 1 - 2 ⬜ some content in line 2 + 2 ⬛ some content in line 2 ```
-
file2.ts +
+🔴 file2.ts — 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 🟥 some content in line 1 - 2 ⬜ some content in line 2 + 2 ⬛ some content in line 2 ```
... (1 file(s) not shown due to size limit) -🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/truncation-minimal.snap.md b/tests/markdown/__snapshots__/truncation-minimal.snap.md index 11a875e..90fe21e 100644 --- a/tests/markdown/__snapshots__/truncation-minimal.snap.md +++ b/tests/markdown/__snapshots__/truncation-minimal.snap.md @@ -1,23 +1,33 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**LargePkg** (LineCoverage: 0%) +--- -
file1.ts +## PR Coverage +0/1 changed lines covered (Lines: 0%, Branches: n/a) + +### LargePkg +
+🔴 file1.ts — 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 🟥 some content in line 1 - 2 ⬜ some content in line 2 + 2 ⬛ some content in line 2 ```
-
file2.ts +
+🔴 file2.ts — 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 🟥 some content in line 1 - 2 ⬜ some content in line 2 + 2 ⬛ some content in line 2 ```
... (2 file(s) not shown due to size limit) -🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/truncation-none.snap.md b/tests/markdown/__snapshots__/truncation-none.snap.md index 931f294..4f19c82 100644 --- a/tests/markdown/__snapshots__/truncation-none.snap.md +++ b/tests/markdown/__snapshots__/truncation-none.snap.md @@ -1,13 +1,22 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**SmallPkg** (LineCoverage: 0%) +--- -
small.ts +## PR Coverage +0/1 changed lines covered (Lines: 0%, Branches: n/a) + +### SmallPkg +
+🔴 small.ts — 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 🟥 some content in line 1 - 2 ⬜ some content in line 2 + 2 ⬛ some content in line 2 ```
-🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
diff --git a/tests/markdown/__snapshots__/truncation-packages.snap.md b/tests/markdown/__snapshots__/truncation-packages.snap.md index 146b662..46e5ded 100644 --- a/tests/markdown/__snapshots__/truncation-packages.snap.md +++ b/tests/markdown/__snapshots__/truncation-packages.snap.md @@ -1,20 +1,29 @@ +## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) -**Pkg1** (LineCoverage: 0%) +--- -
pkg1/a.ts +## PR Coverage +0/1 changed lines covered (Lines: 0%, Branches: n/a) + +### Pkg1 +
+🔴 pkg1/a.ts — 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 🟥 some content in line 1 - 2 ⬜ some content in line 2 - 3 ⬜ some content in line 3 + 2 ⬛ some content in line 2 + 3 ⬛ some content in line 3 4 🟥 some content in line 4 - 5 ⬜ some content in line 5 + 5 ⬛ some content in line 5 ```
-**Pkg2** (LineCoverage: 0%) +### Pkg2 ... (2 file(s) and 1 package(s) not shown due to size limit) -🟥 Not covered, 🟨 Missing branch coverage, 🟩 Covered \ No newline at end of file +--- + +Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
From 75d9fe8a10d8bd03395cd2a270149583423c22c9 Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 13:55:42 +0100 Subject: [PATCH 02/13] feat: Improved markdown generation with repo summary --- src/markdown/generator.ts | 392 ++++++++++++------ .../core/__snapshots__/basic-coverage.snap.md | 2 +- tests/core/__snapshots__/glob-filter.snap.md | 2 +- .../core/__snapshots__/merge-reports.snap.md | 8 +- .../__snapshots__/multiple-packages.snap.md | 3 +- .../ellipsis-leading-1-line.snap.md | 4 +- .../__snapshots__/ellipsis-leading.snap.md | 4 +- .../ellipsis-trailing-1-line.snap.md | 4 +- .../__snapshots__/ellipsis-trailing.snap.md | 4 +- .../__snapshots__/empty-package.snap.md | 5 +- .../__snapshots__/file-not-found.snap.md | 4 +- .../markdown/__snapshots__/gap-1-line.snap.md | 4 +- .../__snapshots__/gap-2-plus-lines.snap.md | 4 +- .../line-exceeds-file-length.snap.md | 4 +- .../__snapshots__/line-gaps-ellipsis.snap.md | 2 +- .../multiple-files-same-package.snap.md | 3 +- .../__snapshots__/multiple-packages.snap.md | 6 +- .../__snapshots__/simple-report.snap.md | 2 +- 18 files changed, 291 insertions(+), 166 deletions(-) diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index 652ce2e..259e8f1 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -5,7 +5,14 @@ const COVERAGE_ICONS = { covered: '🟩', partial: '🟨', 'not-covered': '🟥', - 'no-info': '⬜', + 'no-info': '⬛', +} as const + +/** File status icons */ +const FILE_STATUS_ICONS = { + fullyCovered: '🟢', + hasUncovered: '🔴', + partialCoverage: '🟠', } as const /** Default maximum number of surrounding lines around an uncovered line */ @@ -29,7 +36,7 @@ export type MarkdownOptions = { * Generate markdown visualization for coverage packages. * This is a pure function suitable for snapshot testing. * - * @param packages - The normalized coverage packages + * @param packages - Array of packages * @param fileContents - Map of resolved (absolute) path to array of line contents * @param options - Optional configuration for markdown generation * @returns Markdown string representation @@ -61,45 +68,73 @@ export function generateMarkdown( } } + // Sort packages + sortPackages(packageSections) + + // Calculate PR summary metrics from packages (only files with uncovered lines) + const prMetrics = calculatePRMetrics(packages) + // Check if all lines are covered (no package sections with uncovered lines) const hasUncoveredContent = packageSections.length > 0 if (!hasUncoveredContent) { - // No uncovered lines - return badges + success message - // We assume that this always fits the minimum size - const successMessage = '✅ All lines are covered!' - return badges ? `${badges}\n\n${successMessage}` : successMessage + // No uncovered lines - return badges + summary + return buildMarkdownForNoUncoveredContent(badges, prMetrics) } - const separator = '\n\n' return buildMarkdownWithinLimitFileLevel({ badges, + prMetrics, legend, packageSections, - separator, maxCharacters, }) } +function buildMarkdownForNoUncoveredContent(badges: string, prMetrics: PRMetrics): string { + const parts: string[] = [] + + // Repo Coverage section + parts.push('## Repo Coverage') + parts.push(badges) + parts.push('') + parts.push('---') + parts.push('') + + // PR Coverage section + parts.push('## PR Coverage') + parts.push('') + parts.push(generatePRSummaryLine(prMetrics)) + parts.push('') + parts.push('---') + parts.push('') + parts.push(`Generated by \`coverage-pr-comment\``) + + return parts.join('\n') +} + function buildMarkdownWithinLimitFileLevel(params: { - badges: string | null + badges: string + prMetrics: PRMetrics legend: string packageSections: PackageSectionData[] - separator: string maxCharacters: number }): string { - const { badges, legend, packageSections, separator, maxCharacters } = params + const { badges, prMetrics, legend, packageSections, maxCharacters } = params - // Parts we will join with `separator` - const parts: string[] = [] - if (badges) parts.push(badges) + // Build output with explicit spacing + let output = '## Repo Coverage\n' + output += badges + '\n' + output += '\n---\n\n' + output += '## PR Coverage\n' + output += generatePRSummaryLine(prMetrics) + '\n\n' // We will always include legend at the end, so every "fit" check reserves room for it. - const legendOnlyLength = joinedLength([...parts, legend], separator) - if (legendOnlyLength > maxCharacters) { + const legendWithSeparator = '\n---\n\n' + legend + const currentLength = output.length + legendWithSeparator.length + if (currentLength > maxCharacters) { // Should not happen with MINIMUM_CHARACTERS, but be safe. - const out = [...parts, legend].join(separator) - return out.slice(0, maxCharacters) + return (output + legendWithSeparator).slice(0, maxCharacters) } let omittedFiles = 0 @@ -109,21 +144,27 @@ function buildMarkdownWithinLimitFileLevel(params: { for (let p = 0; p < packageSections.length; p++) { const pkg = packageSections[p]! - // Decide whether we can include this package header at all - if (!canFitWithLegend(parts, pkg.header, legend, separator, maxCharacters)) { + // Check if we can include this package header + const testOutput = output + pkg.header + '\n' + if (testOutput.length + legendWithSeparator.length > maxCharacters) { // Omit this package and all remaining omittedPackages += packageSections.length - p for (let i = p; i < packageSections.length; i++) omittedFiles += packageSections[i]!.files.length break } - // Include header - parts.push(pkg.header) + // Include header with empty line after + output += pkg.header + '\n' for (let f = 0; f < pkg.files.length; f++) { const file = pkg.files[f]!.content - if (!canFitWithLegend(parts, file, legend, separator, maxCharacters)) { + // Check if we can fit this file + // Files are separated by \n\n, last file before --- gets just \n + const isLastFile = f === pkg.files.length - 1 && p === packageSections.length - 1 + const spacingAfter = isLastFile ? '\n' : '\n\n' + const testOutputWithFile = output + file + spacingAfter + if (testOutputWithFile.length + legendWithSeparator.length > maxCharacters) { // Can't fit this file or remaining files in this package omittedFiles += pkg.files.length - f @@ -131,46 +172,87 @@ function buildMarkdownWithinLimitFileLevel(params: { omittedPackages += packageSections.length - (p + 1) for (let i = p + 1; i < packageSections.length; i++) omittedFiles += packageSections[i]!.files.length - // Optionally: if we added a package header but no files fit, you may want to keep it for context. - // Current behavior: we keep the header. - // If you *don't* want empty headers, you could remove it here when !includedAnyFileInThisPkg. - p = packageSections.length // break outer break } - parts.push(file) + // Add file with spacing + output += file + spacingAfter } } // Add truncation notice if anything omitted AND it fits if (omittedFiles > 0 || omittedPackages > 0) { const notice = generateTruncationNotice(omittedFiles, omittedPackages) - if (canFitWithLegend(parts, notice, legend, separator, maxCharacters)) { - parts.push(notice) + const testOutputWithNotice = output + notice + '\n\n' + if (testOutputWithNotice.length + legendWithSeparator.length <= maxCharacters) { + output += notice + '\n\n' } } - // Finish with legend - parts.push(legend) + // Add horizontal rule and legend + output += legendWithSeparator // Hard guarantee (should already be true) - let out = parts.join(separator) - if (out.length > maxCharacters) out = out.slice(0, maxCharacters) - return out + if (output.length > maxCharacters) output = output.slice(0, maxCharacters) + return output } -function joinedLength(parts: string[], separator: string): number { - if (parts.length === 0) return 0 - let len = 0 - for (const p of parts) len += p.length - len += separator.length * (parts.length - 1) - return len +/** PR metrics for summary line */ +type PRMetrics = { + coveredLines: number + totalLines: number + linePercent: number + branchPercent: number | null +} + +/** + * Calculate PR coverage metrics from packages (only changed lines). + * Counts all files, not just ones with uncovered lines. + */ +function calculatePRMetrics(packages: PackageCoverage[]): PRMetrics { + let coveredLines = 0 + let totalLines = 0 + let branchCovered = 0 + let branchTotal = 0 + let hasBranchData = false + + for (const pkg of packages) { + for (const file of pkg.files) { + totalLines += file.lineMetrics.total + coveredLines += file.lineMetrics.covered + + if (file.branchMetrics) { + hasBranchData = true + branchCovered += file.branchMetrics.covered + branchTotal += file.branchMetrics.total + } + } + } + + const linePercent = totalLines === 0 ? 0 : (coveredLines / totalLines) * 100 + const branchPercent = hasBranchData && branchTotal > 0 ? (branchCovered / branchTotal) * 100 : null + + return { + coveredLines, + totalLines, + linePercent, + branchPercent, + } } -function canFitWithLegend(parts: string[], candidate: string, legend: string, separator: string, max: number): boolean { - const test = [...parts, candidate, legend] - return joinedLength(test, separator) <= max +/** + * Generate PR summary line. + */ +function generatePRSummaryLine(metrics: PRMetrics): string { + if (metrics.totalLines === 0) { + return '0/0 changed lines covered' + } + + const linePercentStr = `${Math.round(metrics.linePercent)}%` + const branchPercentStr = metrics.branchPercent !== null ? `${Math.round(metrics.branchPercent)}%` : 'n/a' + + return `${metrics.coveredLines}/${metrics.totalLines} changed lines covered (Lines: ${linePercentStr}, Branches: ${branchPercentStr})` } /** Data structure for a package section with its files */ @@ -178,12 +260,18 @@ type PackageSectionData = { packageName: string header: string files: FileSectionData[] + // Metrics for sorting + totalUncoveredLines: number + totalPartialBranches: number } /** Data structure for a file section */ type FileSectionData = { filename: string content: string + // Metrics for sorting + uncoveredLines: number + partialBranches: number } /** @@ -209,41 +297,48 @@ function generatePackageSectionData( fileContents: Map, numberOfSurroundingLines: number, ): PackageSectionData | null { - // Filter to only files with uncovered lines - const filesWithUncovered = pkg.files.filter(hasUncoveredLines) + // Check if package has any files with uncovered lines + const hasUncovered = pkg.files.some(hasUncoveredLines) // Skip package entirely if all files are fully covered - if (filesWithUncovered.length === 0) { + if (!hasUncovered) { return null } - // Calculate aggregate metrics for the package - const packageMetrics = calculatePackageMetrics(pkg.files) + // Package header - simple H3 format + const header = `### ${pkg.name}` - // Package header with coverage summary - const linePercent = formatPercent(packageMetrics.lineMetrics) - const branchPercent = packageMetrics.branchMetrics ? formatPercent(packageMetrics.branchMetrics) : null - - let header = `**${pkg.name}** (LineCoverage: ${linePercent}` - if (branchPercent) { - header += `, BranchCoverage: ${branchPercent}` - } - header += ')' - - // Generate file sections + // Generate file sections - need to include all files, not just uncovered const files: FileSectionData[] = [] - for (const file of filesWithUncovered) { + let totalUncoveredLines = 0 + let totalPartialBranches = 0 + + for (const file of pkg.files) { const content = file.resolvedPath ? (fileContents.get(file.resolvedPath) ?? []) : [] + + // Calculate metrics for sorting + const uncoveredLines = file.lines.filter((line) => line.state === 'not-covered').length + const partialBranches = file.lines.filter((line) => line.state === 'partial').length + totalUncoveredLines += uncoveredLines + totalPartialBranches += partialBranches + files.push({ filename: file.filename, content: generateFileSection(file, content, numberOfSurroundingLines), + uncoveredLines, + partialBranches, }) } + // Sort files within package + sortFiles(files) + return { packageName: pkg.name, header, files, + totalUncoveredLines, + totalPartialBranches, } } @@ -255,13 +350,47 @@ function hasUncoveredLines(file: FileCoverage): boolean { } /** - * Generate a collapsible markdown section for a single file. + * Generate a markdown section for a single file. + * Uses inline format for fully covered files, details block for files with uncovered lines. */ function generateFileSection(file: FileCoverage, fileLines: string[], numberOfSurroundingLines: number): string { + const fileHasUncovered = hasUncoveredLines(file) + + if (!fileHasUncovered) { + // Fully covered - use inline format + const coveredCount = file.lineMetrics.covered + const totalCount = file.lineMetrics.total + return `✓ ${FILE_STATUS_ICONS.fullyCovered} ${file.filename} — ${coveredCount}/${totalCount} changed lines covered` + } + + // Has uncovered lines - use details block const lines: string[] = [] + // Determine file status icon + // 🔴 if has uncovered lines, 🟠 if only partial branches (no uncovered), 🟢 if fully covered + const fileHasUncoveredLines = file.lines.some((line) => line.state === 'not-covered') + const fileHasPartial = file.lines.some((line) => line.state === 'partial') + const statusIcon = fileHasUncoveredLines + ? FILE_STATUS_ICONS.hasUncovered + : fileHasPartial + ? FILE_STATUS_ICONS.partialCoverage + : FILE_STATUS_ICONS.fullyCovered + + // Calculate file metrics + const coveredCount = file.lineMetrics.covered + const totalCount = file.lineMetrics.total + const linePercent = totalCount === 0 ? 0 : Math.round((coveredCount / totalCount) * 100) + const branchPercent = file.branchMetrics + ? file.branchMetrics.total === 0 + ? 'n/a' + : `${Math.round((file.branchMetrics.covered / file.branchMetrics.total) * 100)}%` + : 'n/a' + // Start collapsible details section - lines.push(`
${file.filename}`) + lines.push(`
`) + lines.push( + `${statusIcon} ${file.filename} — ${coveredCount}/${totalCount} changed lines covered (Lines: ${linePercent}%, Branches: ${branchPercent})`, + ) lines.push('') // Generate code block with coverage annotations @@ -269,7 +398,6 @@ function generateFileSection(file: FileCoverage, fileLines: string[], numberOfSu lines.push('```' + extension) lines.push(generateAnnotatedLines(file.lines, fileLines, numberOfSurroundingLines)) lines.push('```') - lines.push('
') return lines.join('\n') @@ -405,42 +533,41 @@ function generateAnnotatedLines( * Generate the coverage legend explaining the symbols. */ function generateLegend(): string { - return `${COVERAGE_ICONS['not-covered']} Not covered, ${COVERAGE_ICONS.partial} Missing branch coverage, ${COVERAGE_ICONS.covered} Covered` + return `Legend: ${COVERAGE_ICONS['not-covered']} uncovered · ${COVERAGE_ICONS.partial} partial branch · ${COVERAGE_ICONS.covered} covered · ${COVERAGE_ICONS['no-info']} non-executable/blank
Generated by \`coverage-pr-comment\`
` } /** * Generate coverage badges for the overall packages. - * Returns badges for Line, Branch, and Function coverage if data is available. + * Returns badges for Line and Branch coverage if data is available. + * Does NOT include Function/Method coverage badge. */ -function generateCoverageBadges(packages: PackageCoverage[]): string | null { +function generateCoverageBadges(packages: PackageCoverage[]): string { const metrics = calculateReportMetrics(packages) - // Need at least line metrics to generate badges - if (metrics.lineMetrics.total === 0) { - return null - } - const badges: string[] = [] - - // Line coverage badge badges.push(generateBadge('Line Coverage', metrics.lineMetrics)) - - // Branch coverage badge - if (metrics.branchMetrics && metrics.branchMetrics.total > 0) { - badges.push(generateBadge('Branch Coverage', metrics.branchMetrics)) - } - + badges.push(generateBadge('Branch Coverage', metrics.branchMetrics ?? { covered: 0, total: 0 })) return badges.join('\n') } /** * Generate a single shields.io badge markdown. */ -function generateBadge(label: string, metrics: CoverageMetrics): string { - const percent = metrics.total === 0 ? 0 : (metrics.covered / metrics.total) * 100 - const color = getBadgeColor(percent) +function generateBadge(label: string, metrics?: CoverageMetrics): string { const encodedLabel = encodeURIComponent(label) - const encodedValue = encodeURIComponent(`${percent.toFixed(2)}%`) + + // For branch coverage with no data (total === 0), use "n/a" instead of percentage + let value: string + let color: string + if (!metrics || metrics.total === 0) { + value = 'n/a' + color = 'brightgreen' + } else { + const percent = metrics.total === 0 ? 0 : (metrics.covered / metrics.total) * 100 + value = `${percent.toFixed(2)}%` + color = getBadgeColor(percent) + } + const encodedValue = encodeURIComponent(value) return `![${label}](https://img.shields.io/badge/${encodedLabel}-${encodedValue}-${color}.svg?style=flat)` } @@ -496,55 +623,6 @@ function calculateReportMetrics(packages: PackageCoverage[]): { return result } -/** - * Calculate aggregate metrics for a package from its files. - */ -function calculatePackageMetrics(files: FileCoverage[]): { - lineMetrics: CoverageMetrics - branchMetrics?: CoverageMetrics -} { - let lineCovered = 0 - let lineTotal = 0 - let branchCovered = 0 - let branchTotal = 0 - let hasBranchData = false - - for (const file of files) { - lineCovered += file.lineMetrics.covered - lineTotal += file.lineMetrics.total - - if (file.branchMetrics) { - hasBranchData = true - branchCovered += file.branchMetrics.covered - branchTotal += file.branchMetrics.total - } - } - - const result: { - lineMetrics: CoverageMetrics - branchMetrics?: CoverageMetrics - } = { - lineMetrics: { covered: lineCovered, total: lineTotal }, - } - - if (hasBranchData) { - result.branchMetrics = { covered: branchCovered, total: branchTotal } - } - - return result -} - -/** - * Format coverage metrics as a percentage string. - */ -function formatPercent(metrics: CoverageMetrics): string { - if (metrics.total === 0) { - return '0%' - } - const percent = (metrics.covered / metrics.total) * 100 - return `${percent.toFixed(0)}%` -} - /** * Extract file extension for syntax highlighting. */ @@ -579,3 +657,51 @@ function getFileExtension(filename: string): string { return extensionMap[ext] || ext } + +/** + * Sort packages by uncovered lines, partial branches, then alphabetically. + */ +function sortPackages(packages: PackageSectionData[]): void { + packages.sort((a, b) => { + // Sort by total uncovered lines (descending) + if (a.totalUncoveredLines !== b.totalUncoveredLines) { + return b.totalUncoveredLines - a.totalUncoveredLines + } + + // Sort by total partial branches (descending) + if (a.totalPartialBranches !== b.totalPartialBranches) { + return b.totalPartialBranches - a.totalPartialBranches + } + + // Alphabetically by package name (ascending) + return a.packageName.localeCompare(b.packageName) + }) +} + +/** + * Sort files by uncovered lines, partial branches, then alphabetically. + * Fully covered files (0 uncovered) come first, then files with uncovered lines. + */ +function sortFiles(files: FileSectionData[]): void { + files.sort((a, b) => { + // Fully covered files (0 uncovered) come first + const aIsFullyCovered = a.uncoveredLines === 0 + const bIsFullyCovered = b.uncoveredLines === 0 + if (aIsFullyCovered !== bIsFullyCovered) { + return aIsFullyCovered ? -1 : 1 + } + + // For files with uncovered lines, sort by uncovered lines (descending) + if (!aIsFullyCovered && a.uncoveredLines !== b.uncoveredLines) { + return b.uncoveredLines - a.uncoveredLines + } + + // Sort by partial branches (descending) + if (a.partialBranches !== b.partialBranches) { + return b.partialBranches - a.partialBranches + } + + // Alphabetically by filename (ascending) + return a.filename.localeCompare(b.filename) + }) +} diff --git a/tests/core/__snapshots__/basic-coverage.snap.md b/tests/core/__snapshots__/basic-coverage.snap.md index c9aa125..07609c4 100644 --- a/tests/core/__snapshots__/basic-coverage.snap.md +++ b/tests/core/__snapshots__/basic-coverage.snap.md @@ -22,4 +22,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/core/__snapshots__/glob-filter.snap.md b/tests/core/__snapshots__/glob-filter.snap.md index a481fa9..7d058bd 100644 --- a/tests/core/__snapshots__/glob-filter.snap.md +++ b/tests/core/__snapshots__/glob-filter.snap.md @@ -21,4 +21,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/core/__snapshots__/merge-reports.snap.md b/tests/core/__snapshots__/merge-reports.snap.md index 02dffef..4482c66 100644 --- a/tests/core/__snapshots__/merge-reports.snap.md +++ b/tests/core/__snapshots__/merge-reports.snap.md @@ -1,15 +1,15 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) --- ## PR Coverage -4/5 changed lines covered (Lines: 80%, Branches: 100%) +3/5 changed lines covered (Lines: 60%, Branches: 100%) ### shared
-🔴 src/shared.ts — 4/5 changed lines covered (Lines: 80%, Branches: 100%) +🔴 src/shared.ts — 3/5 changed lines covered (Lines: 60%, Branches: 100%) ```typescript 1 🟩 export function sharedOne(): number { @@ -24,4 +24,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/core/__snapshots__/multiple-packages.snap.md b/tests/core/__snapshots__/multiple-packages.snap.md index c762ad4..f45d472 100644 --- a/tests/core/__snapshots__/multiple-packages.snap.md +++ b/tests/core/__snapshots__/multiple-packages.snap.md @@ -20,9 +20,8 @@
### utils - ✓ 🟢 src/utils/format.ts — 3/3 changed lines covered --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md index 3e10451..fa3da87 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- @@ -22,4 +22,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/ellipsis-leading.snap.md b/tests/markdown/__snapshots__/ellipsis-leading.snap.md index 2781320..6245d37 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.00%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- @@ -22,4 +22,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md index 006c755..62b36e5 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-80.00%25-brightgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- @@ -22,4 +22,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md index 2c3fe15..6f1c922 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- @@ -21,4 +21,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/empty-package.snap.md b/tests/markdown/__snapshots__/empty-package.snap.md index 9246f94..3bbcf9a 100644 --- a/tests/markdown/__snapshots__/empty-package.snap.md +++ b/tests/markdown/__snapshots__/empty-package.snap.md @@ -1,5 +1,6 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- @@ -9,4 +10,4 @@ --- -Generated by `coverage-pr-comment` +Generated by `coverage-pr-comment` \ No newline at end of file diff --git a/tests/markdown/__snapshots__/file-not-found.snap.md b/tests/markdown/__snapshots__/file-not-found.snap.md index c9e4f3d..e7560fb 100644 --- a/tests/markdown/__snapshots__/file-not-found.snap.md +++ b/tests/markdown/__snapshots__/file-not-found.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- @@ -19,4 +19,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/gap-1-line.snap.md b/tests/markdown/__snapshots__/gap-1-line.snap.md index 1063082..43fb738 100644 --- a/tests/markdown/__snapshots__/gap-1-line.snap.md +++ b/tests/markdown/__snapshots__/gap-1-line.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- @@ -22,4 +22,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md index 6d65765..220847e 100644 --- a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md +++ b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- @@ -25,4 +25,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md index 37dc8c8..54252ed 100644 --- a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md +++ b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- @@ -21,4 +21,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md index ad31dd8..c388f77 100644 --- a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md +++ b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md @@ -25,4 +25,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md index 2f8f685..9a64d34 100644 --- a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md +++ b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md @@ -8,7 +8,6 @@ 3/5 changed lines covered (Lines: 60%, Branches: 0%) ### MultiFilePackage - ✓ 🟢 src/file1.py — 2/2 changed lines covered
@@ -23,4 +22,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/multiple-packages.snap.md b/tests/markdown/__snapshots__/multiple-packages.snap.md index f01916a..a869898 100644 --- a/tests/markdown/__snapshots__/multiple-packages.snap.md +++ b/tests/markdown/__snapshots__/multiple-packages.snap.md @@ -1,11 +1,11 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- ## PR Coverage -0/2 changed lines covered (Lines: 0%, Branches: n/a) +3/5 changed lines covered (Lines: 60%, Branches: n/a) ### Package.Tests
@@ -23,4 +23,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/simple-report.snap.md b/tests/markdown/__snapshots__/simple-report.snap.md index 87d973d..95729af 100644 --- a/tests/markdown/__snapshots__/simple-report.snap.md +++ b/tests/markdown/__snapshots__/simple-report.snap.md @@ -23,4 +23,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file From 2d51ae2cb323b9cf749c91d0845ecf08af3b6c6b Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 15:28:12 +0100 Subject: [PATCH 03/13] feat: Refactor generator and get truncation working --- src/markdown/generator.ts | 858 ++++++++++-------- .../__snapshots__/multiple-packages.snap.md | 19 +- tests/filter/changed-lines.test.ts | 14 +- .../__snapshots__/multiple-packages.snap.md | 3 + ...report.snap.md => simple-packages.snap.md} | 0 .../__snapshots__/truncation-files.snap.md | 6 +- .../__snapshots__/truncation-minimal.snap.md | 6 +- .../__snapshots__/truncation-none.snap.md | 4 +- .../__snapshots__/truncation-packages.snap.md | 7 +- tests/markdown/generator.test.ts | 94 +- 10 files changed, 560 insertions(+), 451 deletions(-) rename tests/markdown/__snapshots__/{simple-report.snap.md => simple-packages.snap.md} (100%) diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index 259e8f1..82fed75 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -1,5 +1,9 @@ import type { CoverageMetrics, FileCoverage, LineCoverage, PackageCoverage } from '../coverage/model.js' +// ============================================================================= +// CONSTANTS +// ============================================================================= + /** Emoji indicators for line coverage states */ const COVERAGE_ICONS = { covered: '🟩', @@ -22,7 +26,11 @@ export const DEFAULT_MAX_NUMBER_OF_SURROUNDING_LINES = 1 export const DEFAULT_MAX_CHARACTERS = 65536 /** Minimum characters required for meaningful markdown output (badges + legend + notice) */ -export const MINIMUM_CHARACTERS = 500 +export const MINIMUM_CHARACTERS = 900 + +// ============================================================================= +// TYPES +// ============================================================================= /** Options for markdown generation */ export type MarkdownOptions = { @@ -32,13 +40,51 @@ export type MarkdownOptions = { maxCharacters?: number } +/** PR metrics for summary line */ +type PRMetrics = { + coveredLines: number + totalLines: number + linePercent: number + branchPercent: number | null +} + +/** Data structure for a package section with its files */ +type PackageSectionData = { + packageName: string + header: string + files: FileSectionData[] + totalUncoveredLines: number + totalPartialBranches: number +} + +/** Data structure for a file section */ +type FileSectionData = { + filename: string + content: string + uncoveredLines: number + partialBranches: number +} + +/** Classification result for a file's coverage state */ +type FileCoverageClassification = { + hasUncovered: boolean + hasPartial: boolean + uncoveredCount: number + partialCount: number + statusIcon: string +} + +// ============================================================================= +// PUBLIC API +// ============================================================================= + /** - * Generate markdown visualization for coverage packages. + * Generate Markdown visualization for coverage packages. * This is a pure function suitable for snapshot testing. * * @param packages - Array of packages * @param fileContents - Map of resolved (absolute) path to array of line contents - * @param options - Optional configuration for markdown generation + * @param options - Optional configuration for Markdown generation * @returns Markdown string representation * @throws Error if maxCharacters is below MINIMUM_CHARACTERS */ @@ -55,30 +101,21 @@ export function generateMarkdown( throw new Error(`maxCharacters must be at least ${MINIMUM_CHARACTERS}, got ${maxCharacters}`) } - // Generate fixed content (badges and legend) + // Step 1: Generate fixed content (badges and legend) const badges = generateCoverageBadges(packages) const legend = generateLegend() - // Generate all package sections with their file contents - const packageSections: PackageSectionData[] = [] - for (const pkg of packages) { - const sectionData = generatePackageSectionData(pkg, fileContents, numberOfSurroundingLines) - if (sectionData !== null) { - packageSections.push(sectionData) - } - } + // Step 2: Build package section data + const packageSections = buildPackageSections(packages, fileContents, numberOfSurroundingLines) - // Sort packages + // Step 3: Sort packages sortPackages(packageSections) - // Calculate PR summary metrics from packages (only files with uncovered lines) + // Step 4: Calculate PR metrics const prMetrics = calculatePRMetrics(packages) - // Check if all lines are covered (no package sections with uncovered lines) - const hasUncoveredContent = packageSections.length > 0 - - if (!hasUncoveredContent) { - // No uncovered lines - return badges + summary + // Step 5: Render markdown with truncation + if (packageSections.length === 0) { return buildMarkdownForNoUncoveredContent(badges, prMetrics) } @@ -91,119 +128,40 @@ export function generateMarkdown( }) } -function buildMarkdownForNoUncoveredContent(badges: string, prMetrics: PRMetrics): string { - const parts: string[] = [] - - // Repo Coverage section - parts.push('## Repo Coverage') - parts.push(badges) - parts.push('') - parts.push('---') - parts.push('') - - // PR Coverage section - parts.push('## PR Coverage') - parts.push('') - parts.push(generatePRSummaryLine(prMetrics)) - parts.push('') - parts.push('---') - parts.push('') - parts.push(`Generated by \`coverage-pr-comment\``) - - return parts.join('\n') -} - -function buildMarkdownWithinLimitFileLevel(params: { - badges: string - prMetrics: PRMetrics - legend: string - packageSections: PackageSectionData[] - maxCharacters: number -}): string { - const { badges, prMetrics, legend, packageSections, maxCharacters } = params - - // Build output with explicit spacing - let output = '## Repo Coverage\n' - output += badges + '\n' - output += '\n---\n\n' - output += '## PR Coverage\n' - output += generatePRSummaryLine(prMetrics) + '\n\n' - - // We will always include legend at the end, so every "fit" check reserves room for it. - const legendWithSeparator = '\n---\n\n' + legend - const currentLength = output.length + legendWithSeparator.length - if (currentLength > maxCharacters) { - // Should not happen with MINIMUM_CHARACTERS, but be safe. - return (output + legendWithSeparator).slice(0, maxCharacters) - } - - let omittedFiles = 0 - let omittedPackages = 0 - - // Build incrementally: header, then each file - for (let p = 0; p < packageSections.length; p++) { - const pkg = packageSections[p]! - - // Check if we can include this package header - const testOutput = output + pkg.header + '\n' - if (testOutput.length + legendWithSeparator.length > maxCharacters) { - // Omit this package and all remaining - omittedPackages += packageSections.length - p - for (let i = p; i < packageSections.length; i++) omittedFiles += packageSections[i]!.files.length - break - } - - // Include header with empty line after - output += pkg.header + '\n' - - for (let f = 0; f < pkg.files.length; f++) { - const file = pkg.files[f]!.content - - // Check if we can fit this file - // Files are separated by \n\n, last file before --- gets just \n - const isLastFile = f === pkg.files.length - 1 && p === packageSections.length - 1 - const spacingAfter = isLastFile ? '\n' : '\n\n' - const testOutputWithFile = output + file + spacingAfter - if (testOutputWithFile.length + legendWithSeparator.length > maxCharacters) { - // Can't fit this file or remaining files in this package - omittedFiles += pkg.files.length - f - - // Also omit all remaining packages completely - omittedPackages += packageSections.length - (p + 1) - for (let i = p + 1; i < packageSections.length; i++) omittedFiles += packageSections[i]!.files.length - - p = packageSections.length // break outer - break - } +// ============================================================================= +// CLASSIFICATION & METRICS HELPERS +// ============================================================================= - // Add file with spacing - output += file + spacingAfter +/** + * Classify a file's coverage state. + * Returns counts and flags for uncovered/partial lines, plus the appropriate status icon. + */ +function classifyFileCoverage(file: FileCoverage): FileCoverageClassification { + let uncoveredCount = 0 + let partialCount = 0 + + for (const line of file.lines) { + if (line.state === 'not-covered') { + uncoveredCount++ + } else if (line.state === 'partial') { + partialCount++ } } - // Add truncation notice if anything omitted AND it fits - if (omittedFiles > 0 || omittedPackages > 0) { - const notice = generateTruncationNotice(omittedFiles, omittedPackages) - const testOutputWithNotice = output + notice + '\n\n' - if (testOutputWithNotice.length + legendWithSeparator.length <= maxCharacters) { - output += notice + '\n\n' - } - } + const hasUncovered = uncoveredCount > 0 + const hasPartial = partialCount > 0 - // Add horizontal rule and legend - output += legendWithSeparator + // Determine status icon: 🔴 if uncovered, 🟠 if only partial, 🟢 if fully covered + let statusIcon: string + if (hasUncovered) { + statusIcon = FILE_STATUS_ICONS.hasUncovered + } else if (hasPartial) { + statusIcon = FILE_STATUS_ICONS.partialCoverage + } else { + statusIcon = FILE_STATUS_ICONS.fullyCovered + } - // Hard guarantee (should already be true) - if (output.length > maxCharacters) output = output.slice(0, maxCharacters) - return output -} - -/** PR metrics for summary line */ -type PRMetrics = { - coveredLines: number - totalLines: number - linePercent: number - branchPercent: number | null + return { hasUncovered, hasPartial, uncoveredCount, partialCount, statusIcon } } /** @@ -233,104 +191,132 @@ function calculatePRMetrics(packages: PackageCoverage[]): PRMetrics { const linePercent = totalLines === 0 ? 0 : (coveredLines / totalLines) * 100 const branchPercent = hasBranchData && branchTotal > 0 ? (branchCovered / branchTotal) * 100 : null - return { - coveredLines, - totalLines, - linePercent, - branchPercent, - } + return { coveredLines, totalLines, linePercent, branchPercent } } /** - * Generate PR summary line. + * Calculate aggregate metrics for the entire packages (for badges). */ -function generatePRSummaryLine(metrics: PRMetrics): string { - if (metrics.totalLines === 0) { - return '0/0 changed lines covered' +function calculateReportMetrics(packages: PackageCoverage[]): { + lineMetrics: CoverageMetrics + branchMetrics?: CoverageMetrics +} { + let lineCovered = 0 + let lineTotal = 0 + let branchCovered = 0 + let branchTotal = 0 + let hasBranchData = false + + for (const pkg of packages) { + for (const file of pkg.files) { + lineCovered += file.lineMetrics.covered + lineTotal += file.lineMetrics.total + + if (file.branchMetrics) { + hasBranchData = true + branchCovered += file.branchMetrics.covered + branchTotal += file.branchMetrics.total + } + } } - const linePercentStr = `${Math.round(metrics.linePercent)}%` - const branchPercentStr = metrics.branchPercent !== null ? `${Math.round(metrics.branchPercent)}%` : 'n/a' + const result: { lineMetrics: CoverageMetrics; branchMetrics?: CoverageMetrics } = { + lineMetrics: { covered: lineCovered, total: lineTotal }, + } + + if (hasBranchData) { + result.branchMetrics = { covered: branchCovered, total: branchTotal } + } - return `${metrics.coveredLines}/${metrics.totalLines} changed lines covered (Lines: ${linePercentStr}, Branches: ${branchPercentStr})` + return result } -/** Data structure for a package section with its files */ -type PackageSectionData = { - packageName: string - header: string - files: FileSectionData[] - // Metrics for sorting - totalUncoveredLines: number - totalPartialBranches: number +// ============================================================================= +// FORMATTING HELPERS +// ============================================================================= + +/** + * Format a ratio as "covered/total". + */ +function formatRatio(covered: number, total: number): string { + return `${covered}/${total}` } -/** Data structure for a file section */ -type FileSectionData = { - filename: string - content: string - // Metrics for sorting - uncoveredLines: number - partialBranches: number +/** + * Format a percentage rounded to nearest integer. + */ +function formatPercentRounded(covered: number, total: number): string { + if (total === 0) return '0%' + return `${Math.round((covered / total) * 100)}%` } /** - * Generate truncation notice with counts of omitted items. + * Format branch percentage or return "n/a" if no data. */ -function generateTruncationNotice(omittedFiles: number, omittedPackages: number): string { - const parts: string[] = [] - if (omittedFiles > 0) { - parts.push(`${omittedFiles} file(s)`) +function formatBranchPercentOrNA(branchMetrics: CoverageMetrics | undefined): string { + if (!branchMetrics || branchMetrics.total === 0) { + return 'n/a' } - if (omittedPackages > 0) { - parts.push(`${omittedPackages} package(s)`) + return `${Math.round((branchMetrics.covered / branchMetrics.total) * 100)}%` +} + +// ============================================================================= +// SECTION DATA BUILDERS +// ============================================================================= + +/** + * Build all package section data from packages. + * Only includes packages that have files with uncovered/partial lines. + */ +function buildPackageSections( + packages: PackageCoverage[], + fileContents: Map, + numberOfSurroundingLines: number, +): PackageSectionData[] { + const sections: PackageSectionData[] = [] + + for (const pkg of packages) { + const sectionData = buildPackageSectionData(pkg, fileContents, numberOfSurroundingLines) + if (sectionData !== null) { + sections.push(sectionData) + } } - return `... (${parts.join(' and ')} not shown due to size limit)` + + return sections } /** - * Generate package section data without joining into final string. + * Build section data for a single package. * Returns null if the package has no files with uncovered lines. */ -function generatePackageSectionData( +function buildPackageSectionData( pkg: PackageCoverage, fileContents: Map, numberOfSurroundingLines: number, ): PackageSectionData | null { - // Check if package has any files with uncovered lines - const hasUncovered = pkg.files.some(hasUncoveredLines) - - // Skip package entirely if all files are fully covered - if (!hasUncovered) { + if (pkg.files.length === 0) { return null } - - // Package header - simple H3 format const header = `### ${pkg.name}` - - // Generate file sections - need to include all files, not just uncovered const files: FileSectionData[] = [] let totalUncoveredLines = 0 let totalPartialBranches = 0 for (const file of pkg.files) { const content = file.resolvedPath ? (fileContents.get(file.resolvedPath) ?? []) : [] + const classification = classifyFileCoverage(file) - // Calculate metrics for sorting - const uncoveredLines = file.lines.filter((line) => line.state === 'not-covered').length - const partialBranches = file.lines.filter((line) => line.state === 'partial').length - totalUncoveredLines += uncoveredLines - totalPartialBranches += partialBranches + totalUncoveredLines += classification.uncoveredCount + totalPartialBranches += classification.partialCount files.push({ filename: file.filename, - content: generateFileSection(file, content, numberOfSurroundingLines), - uncoveredLines, - partialBranches, + content: renderFileSection(file, content, numberOfSurroundingLines, classification), + uncoveredLines: classification.uncoveredCount, + partialBranches: classification.partialCount, }) } - // Sort files within package sortFiles(files) return { @@ -342,228 +328,222 @@ function generatePackageSectionData( } } +// ============================================================================= +// MARKDOWN RENDERING +// ============================================================================= + /** - * Check if a file has any uncovered or partial lines. + * Build markdown for case when there are no uncovered lines. */ -function hasUncoveredLines(file: FileCoverage): boolean { - return file.lines.some((line) => line.state === 'not-covered' || line.state === 'partial') +function buildMarkdownForNoUncoveredContent(badges: string, prMetrics: PRMetrics): string { + const parts: string[] = [] + + parts.push('## Repo Coverage') + parts.push(badges) + parts.push('') + parts.push('---') + parts.push('') + parts.push('## PR Coverage') + parts.push('') + parts.push(renderPRSummaryLine(prMetrics)) + parts.push('') + parts.push('---') + parts.push('') + parts.push(`Generated by \`coverage-pr-comment\``) + + return parts.join('\n') } /** - * Generate a markdown section for a single file. - * Uses inline format for fully covered files, details block for files with uncovered lines. + * Simple budget tracker for building output within a character limit. + * Reserves space for a required tail (e.g., legend) and tracks what fits. */ -function generateFileSection(file: FileCoverage, fileLines: string[], numberOfSurroundingLines: number): string { - const fileHasUncovered = hasUncoveredLines(file) +class CharBudget { + private parts: string[] = [] + private currentLength = 0 + private readonly reservedTailLength: number + private readonly maxCharacters: number + + constructor(maxCharacters: number, reservedTail: string) { + this.maxCharacters = maxCharacters + this.reservedTailLength = reservedTail.length + } - if (!fileHasUncovered) { - // Fully covered - use inline format - const coveredCount = file.lineMetrics.covered - const totalCount = file.lineMetrics.total - return `✓ ${FILE_STATUS_ICONS.fullyCovered} ${file.filename} — ${coveredCount}/${totalCount} changed lines covered` + /** Current total length including reserved tail. */ + get totalLength(): number { + return this.currentLength + this.reservedTailLength } - // Has uncovered lines - use details block - const lines: string[] = [] + /** Check if adding content would exceed the limit. */ + wouldExceed(content: string): boolean { + return this.currentLength + content.length + this.reservedTailLength > this.maxCharacters + } - // Determine file status icon - // 🔴 if has uncovered lines, 🟠 if only partial branches (no uncovered), 🟢 if fully covered - const fileHasUncoveredLines = file.lines.some((line) => line.state === 'not-covered') - const fileHasPartial = file.lines.some((line) => line.state === 'partial') - const statusIcon = fileHasUncoveredLines - ? FILE_STATUS_ICONS.hasUncovered - : fileHasPartial - ? FILE_STATUS_ICONS.partialCoverage - : FILE_STATUS_ICONS.fullyCovered - - // Calculate file metrics - const coveredCount = file.lineMetrics.covered - const totalCount = file.lineMetrics.total - const linePercent = totalCount === 0 ? 0 : Math.round((coveredCount / totalCount) * 100) - const branchPercent = file.branchMetrics - ? file.branchMetrics.total === 0 - ? 'n/a' - : `${Math.round((file.branchMetrics.covered / file.branchMetrics.total) * 100)}%` - : 'n/a' - - // Start collapsible details section - lines.push(`
`) - lines.push( - `${statusIcon} ${file.filename} — ${coveredCount}/${totalCount} changed lines covered (Lines: ${linePercent}%, Branches: ${branchPercent})`, - ) - lines.push('') + /** Try to append content. Returns true if it fits, false otherwise. */ + tryAppend(content: string): boolean { + if (this.wouldExceed(content)) { + return false + } + this.parts.push(content) + this.currentLength += content.length + return true + } - // Generate code block with coverage annotations - const extension = getFileExtension(file.filename) - lines.push('```' + extension) - lines.push(generateAnnotatedLines(file.lines, fileLines, numberOfSurroundingLines)) - lines.push('```') - lines.push('
') + /** Force append content (used for initial header that must fit). */ + append(content: string): void { + this.parts.push(content) + this.currentLength += content.length + } - return lines.join('\n') + /** Get the current output without the reserved tail. */ + getOutput(): string { + return this.parts.join('') + } } /** - * Generate annotated line content showing coverage state. - * Only shows uncovered/partial lines with configurable context lines around them. - * Uses smart ellipsis handling: shows single lines instead of "..." when gap is small. + * Build markdown with truncation at file level when limit exceeded. */ -function generateAnnotatedLines( - coverageLines: LineCoverage[], - fileLines: string[], - numberOfSurroundingLines: number, -): string { - if (coverageLines.length === 0) { - return '(no coverage data)' - } +function buildMarkdownWithinLimitFileLevel(params: { + badges: string + prMetrics: PRMetrics + legend: string + packageSections: PackageSectionData[] + maxCharacters: number +}): string { + const { badges, prMetrics, legend, packageSections, maxCharacters } = params - const sortedLines = [...coverageLines].sort((a, b) => a.lineNumber - b.lineNumber) + // Legend with separator is always appended at the end + const legendWithSeparator = '\n---\n\n' + legend - // Create a map for quick line lookup - const lineMap = new Map() - for (const line of sortedLines) { - lineMap.set(line.lineNumber, line) - } + // Build the fixed header + const header = + '## Repo Coverage\n' + badges + '\n' + '\n---\n\n' + '## PR Coverage\n' + renderPRSummaryLine(prMetrics) + '\n\n' - // Find all uncovered/partial lines (the "interesting" lines) - const interestingLineNumbers = new Set() - for (const line of sortedLines) { - if (line.state === 'not-covered' || line.state === 'partial') { - interestingLineNumbers.add(line.lineNumber) - } + // Check if even the header + legend exceeds the limit + if (header.length + legendWithSeparator.length > maxCharacters) { + return (header + legendWithSeparator).slice(0, maxCharacters) } - // Expand to include context lines around interesting lines - const linesToShow = new Set() - for (const lineNum of interestingLineNumbers) { - // Add context before and after - for (let i = lineNum - numberOfSurroundingLines; i <= lineNum + numberOfSurroundingLines; i++) { - if (i >= 1 && (i <= fileLines.length || lineMap.has(i))) { - linesToShow.add(i) + const budget = new CharBudget(maxCharacters, legendWithSeparator) + budget.append(header) + + // Track omissions + let omittedFiles = 0 + let omittedPackages = 0 + let truncated = false + + // Process packages and files + packageLoop: for (let p = 0; p < packageSections.length; p++) { + const pkg = packageSections[p]! + + // Try to include package header + if (!budget.tryAppend(pkg.header + '\n')) { + // Can't fit this package header - omit this and all remaining packages + omittedPackages += packageSections.length - p + for (let i = p; i < packageSections.length; i++) { + omittedFiles += packageSections[i]!.files.length } + truncated = true + break } - } - // Convert to sorted array - const linesToShowArray = [...linesToShow].sort((a, b) => a - b) + // Process files in this package + for (let f = 0; f < pkg.files.length; f++) { + const fileContent = pkg.files[f]!.content + const isLastFileOverall = f === pkg.files.length - 1 && p === packageSections.length - 1 + const spacing = isLastFileOverall ? '\n' : '\n\n' - // Helper to get line content from fileLines (1-indexed to 0-indexed) - const getLineContent = (lineNum: number): string => { - const index = lineNum - 1 - if (index >= 0 && index < fileLines.length) { - return fileLines[index] ?? '' + if (!budget.tryAppend(fileContent + spacing)) { + // Can't fit this file - omit remaining files in this package and all remaining packages + omittedFiles += pkg.files.length - f + omittedPackages += packageSections.length - (p + 1) + for (let i = p + 1; i < packageSections.length; i++) { + omittedFiles += packageSections[i]!.files.length + } + truncated = true + break packageLoop + } } - return '' } - const outputLines: string[] = [] + // Add truncation notice if needed and it fits + if (truncated && (omittedFiles > 0 || omittedPackages > 0)) { + const notice = renderTruncationNotice(omittedFiles, omittedPackages) + budget.tryAppend(notice + '\n') + } - // Determine the first and last line numbers in coverage data for ellipsis detection - const minLineInData = sortedLines[0]?.lineNumber ?? 1 - const maxLineInData = sortedLines[sortedLines.length - 1]?.lineNumber ?? 1 - const firstLineToShow = linesToShowArray[0] ?? minLineInData - const lastLineToShow = linesToShowArray[linesToShowArray.length - 1] ?? maxLineInData + // Build final output with legend + let output = budget.getOutput() + legendWithSeparator - // Add leading ellipsis if there's content before the first shown line - // If only 1 line is hidden, show the actual line instead of ellipsis - if (firstLineToShow > 1) { - if (firstLineToShow === 2) { - // Only line 1 is hidden, show it - const gapLine = lineMap.get(1) - const gapIcon = gapLine ? COVERAGE_ICONS[gapLine.state] : COVERAGE_ICONS['no-info'] - const gapLineNumStr = '1'.padStart(3, ' ') - const gapContent = getLineContent(1) - outputLines.push(`${gapLineNumStr} ${gapIcon} ${gapContent}`) - } else { - outputLines.push('...') - } + // Hard guarantee (should already be true) + if (output.length > maxCharacters) { + output = output.slice(0, maxCharacters) } - let prevLineNumber = -1 + return output +} - for (const lineNum of linesToShowArray) { - const line = lineMap.get(lineNum) +/** + * Render PR summary line. + */ +function renderPRSummaryLine(metrics: PRMetrics): string { + if (metrics.totalLines === 0) { + return '0/0 changed lines covered' + } - // Check for gap and handle ellipsis - if (prevLineNumber !== -1 && lineNum > prevLineNumber + 1) { - const gapSize = lineNum - prevLineNumber - 1 - - // If gap is just 1 line, show the actual line - if (gapSize === 1) { - const gapLineNum = prevLineNumber + 1 - const gapLine = lineMap.get(gapLineNum) - const gapIcon = gapLine ? COVERAGE_ICONS[gapLine.state] : COVERAGE_ICONS['no-info'] - const gapLineNumStr = gapLineNum.toString().padStart(3, ' ') - const gapContent = getLineContent(gapLineNum) - outputLines.push(`${gapLineNumStr} ${gapIcon} ${gapContent}`) - } else { - // 2+ line gap: show single ellipsis - outputLines.push('...') - } - } + const linePercentStr = formatPercentRounded(metrics.coveredLines, metrics.totalLines) + const branchPercentStr = metrics.branchPercent !== null ? `${Math.round(metrics.branchPercent)}%` : 'n/a' - const icon = line ? COVERAGE_ICONS[line.state] : COVERAGE_ICONS['no-info'] - const lineNumStr = lineNum.toString().padStart(3, ' ') - const content = getLineContent(lineNum) - outputLines.push(`${lineNumStr} ${icon} ${content}`) + return `${formatRatio(metrics.coveredLines, metrics.totalLines)} changed lines covered (Lines: ${linePercentStr}, Branches: ${branchPercentStr})` +} - prevLineNumber = lineNum +/** + * Render truncation notice with counts of omitted items. + */ +function renderTruncationNotice(omittedFiles: number, omittedPackages: number): string { + const parts: string[] = [] + if (omittedFiles > 0) { + parts.push(`${omittedFiles} file(s)`) } - - // Add trailing ellipsis if there's content after the last shown line - // If only 1 line is hidden, show the actual line instead of ellipsis - if (lastLineToShow < fileLines.length) { - if (lastLineToShow === fileLines.length - 1) { - // Only the last line is hidden, show it - const lastLineNum = fileLines.length - const gapLine = lineMap.get(lastLineNum) - const gapIcon = gapLine ? COVERAGE_ICONS[gapLine.state] : COVERAGE_ICONS['no-info'] - const gapLineNumStr = lastLineNum.toString().padStart(3, ' ') - const gapContent = getLineContent(lastLineNum) - outputLines.push(`${gapLineNumStr} ${gapIcon} ${gapContent}`) - } else { - outputLines.push('...') - } + if (omittedPackages > 0) { + parts.push(`${omittedPackages} package(s)`) } - - return outputLines.join('\n') + return `... (${parts.join(' and ')} not shown due to size limit)` } /** - * Generate the coverage legend explaining the symbols. + * Render the coverage legend explaining the symbols. */ function generateLegend(): string { return `Legend: ${COVERAGE_ICONS['not-covered']} uncovered · ${COVERAGE_ICONS.partial} partial branch · ${COVERAGE_ICONS.covered} covered · ${COVERAGE_ICONS['no-info']} non-executable/blank
Generated by \`coverage-pr-comment\`
` } /** - * Generate coverage badges for the overall packages. - * Returns badges for Line and Branch coverage if data is available. - * Does NOT include Function/Method coverage badge. + * Render coverage badges for the overall packages. */ function generateCoverageBadges(packages: PackageCoverage[]): string { const metrics = calculateReportMetrics(packages) const badges: string[] = [] - badges.push(generateBadge('Line Coverage', metrics.lineMetrics)) - badges.push(generateBadge('Branch Coverage', metrics.branchMetrics ?? { covered: 0, total: 0 })) + badges.push(renderBadge('Line Coverage', metrics.lineMetrics)) + badges.push(renderBadge('Branch Coverage', metrics.branchMetrics ?? { covered: 0, total: 0 })) return badges.join('\n') } /** - * Generate a single shields.io badge markdown. + * Render a single shields.io badge markdown. */ -function generateBadge(label: string, metrics?: CoverageMetrics): string { +function renderBadge(label: string, metrics?: CoverageMetrics): string { const encodedLabel = encodeURIComponent(label) - // For branch coverage with no data (total === 0), use "n/a" instead of percentage let value: string let color: string if (!metrics || metrics.total === 0) { value = 'n/a' color = 'brightgreen' } else { - const percent = metrics.total === 0 ? 0 : (metrics.covered / metrics.total) * 100 + const percent = (metrics.covered / metrics.total) * 100 value = `${percent.toFixed(2)}%` color = getBadgeColor(percent) } @@ -584,43 +564,41 @@ function getBadgeColor(percent: number): string { } /** - * Calculate aggregate metrics for the entire packages. + * Render a markdown section for a single file. + * Uses inline format for fully covered files, details block for files with uncovered lines. */ -function calculateReportMetrics(packages: PackageCoverage[]): { - lineMetrics: CoverageMetrics - branchMetrics?: CoverageMetrics -} { - let lineCovered = 0 - let lineTotal = 0 - let branchCovered = 0 - let branchTotal = 0 - let hasBranchData = false - - for (const pkg of packages) { - for (const file of pkg.files) { - lineCovered += file.lineMetrics.covered - lineTotal += file.lineMetrics.total +function renderFileSection( + file: FileCoverage, + fileLines: string[], + numberOfSurroundingLines: number, + classification: FileCoverageClassification, +): string { + const { hasUncovered, hasPartial, statusIcon } = classification - if (file.branchMetrics) { - hasBranchData = true - branchCovered += file.branchMetrics.covered - branchTotal += file.branchMetrics.total - } - } + // Fully covered - use inline format + if (!hasUncovered && !hasPartial) { + return `✓ ${FILE_STATUS_ICONS.fullyCovered} ${file.filename} — ${formatRatio(file.lineMetrics.covered, file.lineMetrics.total)} changed lines covered` } - const result: { - lineMetrics: CoverageMetrics - branchMetrics?: CoverageMetrics - } = { - lineMetrics: { covered: lineCovered, total: lineTotal }, - } + // Has uncovered/partial lines - use details block + const lines: string[] = [] - if (hasBranchData) { - result.branchMetrics = { covered: branchCovered, total: branchTotal } - } + const linePercent = formatPercentRounded(file.lineMetrics.covered, file.lineMetrics.total) + const branchPercent = formatBranchPercentOrNA(file.branchMetrics) - return result + lines.push(`
`) + lines.push( + `${statusIcon} ${file.filename} — ${formatRatio(file.lineMetrics.covered, file.lineMetrics.total)} changed lines covered (Lines: ${linePercent}, Branches: ${branchPercent})`, + ) + lines.push('') + + const extension = getFileExtension(file.filename) + lines.push('```' + extension) + lines.push(renderAnnotatedLines(file.lines, fileLines, numberOfSurroundingLines)) + lines.push('```') + lines.push('
') + + return lines.join('\n') } /** @@ -635,7 +613,6 @@ function getFileExtension(filename: string): string { const lastPart = parts[parts.length - 1] const ext = lastPart ? lastPart.toLowerCase() : '' - // Map extensions to markdown code block language identifiers const extensionMap: Record = { cs: 'csharp', rs: 'rust', @@ -658,22 +635,147 @@ function getFileExtension(filename: string): string { return extensionMap[ext] || ext } +// ============================================================================= +// LINE ANNOTATION +// ============================================================================= + +/** + * Render annotated line content showing coverage state. + * Only shows uncovered/partial lines with configurable context lines around them. + * Uses smart ellipsis handling: shows single lines instead of "..." when gap is small. + */ +function renderAnnotatedLines( + coverageLines: LineCoverage[], + fileLines: string[], + numberOfSurroundingLines: number, +): string { + if (coverageLines.length === 0) { + return '(no coverage data)' + } + + const sortedLines = [...coverageLines].sort((a, b) => a.lineNumber - b.lineNumber) + + // Create a map for quick line lookup + const lineMap = new Map() + for (const line of sortedLines) { + lineMap.set(line.lineNumber, line) + } + + // Find all uncovered/partial lines (the "interesting" lines) + const interestingLineNumbers = new Set() + for (const line of sortedLines) { + if (line.state === 'not-covered' || line.state === 'partial') { + interestingLineNumbers.add(line.lineNumber) + } + } + + // Expand to include context lines around interesting lines + const linesToShow = new Set() + for (const lineNum of interestingLineNumbers) { + for (let i = lineNum - numberOfSurroundingLines; i <= lineNum + numberOfSurroundingLines; i++) { + if (i >= 1 && (i <= fileLines.length || lineMap.has(i))) { + linesToShow.add(i) + } + } + } + + const linesToShowArray = [...linesToShow].sort((a, b) => a - b) + + // Helper to get line content from fileLines (1-indexed to 0-indexed) + const getLineContent = (lineNum: number): string => { + const index = lineNum - 1 + if (index >= 0 && index < fileLines.length) { + return fileLines[index] ?? '' + } + return '' + } + + // Helper to render a single annotated line + const renderLine = (lineNum: number): string => { + const line = lineMap.get(lineNum) + const icon = line ? COVERAGE_ICONS[line.state] : COVERAGE_ICONS['no-info'] + const lineNumStr = lineNum.toString().padStart(3, ' ') + const content = getLineContent(lineNum) + return `${lineNumStr} ${icon} ${content}` + } + + const outputLines: string[] = [] + + const minLineInData = sortedLines[0]?.lineNumber ?? 1 + const maxLineInData = sortedLines[sortedLines.length - 1]?.lineNumber ?? 1 + const firstLineToShow = linesToShowArray[0] ?? minLineInData + const lastLineToShow = linesToShowArray[linesToShowArray.length - 1] ?? maxLineInData + + // Add leading gap handling + if (firstLineToShow > 1) { + outputLines.push(renderGap(0, firstLineToShow, lineMap, getLineContent)) + } + + let prevLineNumber = -1 + + for (const lineNum of linesToShowArray) { + // Handle gap between consecutive shown lines + if (prevLineNumber !== -1 && lineNum > prevLineNumber + 1) { + outputLines.push(renderGap(prevLineNumber, lineNum, lineMap, getLineContent)) + } + + outputLines.push(renderLine(lineNum)) + prevLineNumber = lineNum + } + + // Add trailing gap handling + if (lastLineToShow < fileLines.length) { + outputLines.push(renderGap(lastLineToShow, fileLines.length + 1, lineMap, getLineContent)) + } + + return outputLines.join('\n') +} + +/** + * Render a gap between two line numbers. + * If gap is just 1 line, renders that line; otherwise renders "...". + * + * @param prevLine - The last shown line (0 if at start) + * @param nextLine - The next line to be shown (fileLines.length + 1 if at end) + * @param lineMap - The map with available line coverage + * @param getLineContent - the callback to get the content based on the line + */ +function renderGap( + prevLine: number, + nextLine: number, + lineMap: Map, + getLineContent: (lineNum: number) => string, +): string { + const gapSize = nextLine - prevLine - 1 + + if (gapSize === 1) { + // Show the single hidden line + const gapLineNum = prevLine + 1 + const gapLine = lineMap.get(gapLineNum) + const gapIcon = gapLine ? COVERAGE_ICONS[gapLine.state] : COVERAGE_ICONS['no-info'] + const gapLineNumStr = gapLineNum.toString().padStart(3, ' ') + const gapContent = getLineContent(gapLineNum) + return `${gapLineNumStr} ${gapIcon} ${gapContent}` + } + + return '...' +} + +// ============================================================================= +// SORTING +// ============================================================================= + /** * Sort packages by uncovered lines, partial branches, then alphabetically. */ function sortPackages(packages: PackageSectionData[]): void { packages.sort((a, b) => { - // Sort by total uncovered lines (descending) if (a.totalUncoveredLines !== b.totalUncoveredLines) { return b.totalUncoveredLines - a.totalUncoveredLines } - - // Sort by total partial branches (descending) if (a.totalPartialBranches !== b.totalPartialBranches) { return b.totalPartialBranches - a.totalPartialBranches } - - // Alphabetically by package name (ascending) return a.packageName.localeCompare(b.packageName) }) } @@ -684,24 +786,20 @@ function sortPackages(packages: PackageSectionData[]): void { */ function sortFiles(files: FileSectionData[]): void { files.sort((a, b) => { - // Fully covered files (0 uncovered) come first const aIsFullyCovered = a.uncoveredLines === 0 const bIsFullyCovered = b.uncoveredLines === 0 if (aIsFullyCovered !== bIsFullyCovered) { return aIsFullyCovered ? -1 : 1 } - // For files with uncovered lines, sort by uncovered lines (descending) if (!aIsFullyCovered && a.uncoveredLines !== b.uncoveredLines) { return b.uncoveredLines - a.uncoveredLines } - // Sort by partial branches (descending) if (a.partialBranches !== b.partialBranches) { return b.partialBranches - a.partialBranches } - // Alphabetically by filename (ascending) return a.filename.localeCompare(b.filename) }) } diff --git a/tests/core/__snapshots__/multiple-packages.snap.md b/tests/core/__snapshots__/multiple-packages.snap.md index f45d472..27c2f71 100644 --- a/tests/core/__snapshots__/multiple-packages.snap.md +++ b/tests/core/__snapshots__/multiple-packages.snap.md @@ -5,11 +5,23 @@ --- ## PR Coverage -5/6 changed lines covered (Lines: 83.33%, Branches: 100%) +3/6 changed lines covered (Lines: 50%, Branches: 100%) + +### utils +
+🔴 src/utils/format.ts — 1/3 changed lines covered (Lines: 33%, Branches: 100%) + +```typescript + 1 🟩 export function formatNumber(n: number): string { + 2 🟥 return n.toFixed(2) + 3 🟥 } + 4 ⬛ +``` +
### core
-🔴 src/core/math.ts — 2/3 changed lines covered (Lines: 67.00%, Branches: 100%) +🔴 src/core/math.ts — 2/3 changed lines covered (Lines: 67%, Branches: 100%) ```typescript 1 🟩 export function add(a: number, b: number): number { @@ -19,9 +31,6 @@ ```
-### utils -✓ 🟢 src/utils/format.ts — 3/3 changed lines covered - --- Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/filter/changed-lines.test.ts b/tests/filter/changed-lines.test.ts index f5fc9b6..c60dc9d 100644 --- a/tests/filter/changed-lines.test.ts +++ b/tests/filter/changed-lines.test.ts @@ -212,7 +212,7 @@ index abc123..def456 100644 line 3 line 4` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(1) expect(result.get('/root/src/file.ts')).toEqual(new Set([2])) @@ -236,7 +236,7 @@ index 111222..333444 100644 +added in file2 line 6` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(2) expect(result.get('/root/src/file1.ts')).toEqual(new Set([2])) @@ -254,7 +254,7 @@ index 0000000..abc123 +line 2 +line 3` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(1) expect(result.get('/root/src/newfile.ts')).toEqual(new Set([1, 2, 3])) @@ -271,7 +271,7 @@ index abc123..0000000 -line 2 -line 3` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(0) }) @@ -292,7 +292,7 @@ index abc123..def456 100644 line 13 line 14` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(1) expect(result.get('/root/src/file.ts')).toEqual(new Set([2, 12])) @@ -301,7 +301,7 @@ index abc123..def456 100644 it('handles empty diff', () => { const diff = '' - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(0) }) @@ -320,7 +320,7 @@ index abc123..def456 100644 line 3 line 4` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(1) expect(result.get('/root/new/path.ts')).toEqual(new Set([2])) diff --git a/tests/markdown/__snapshots__/multiple-packages.snap.md b/tests/markdown/__snapshots__/multiple-packages.snap.md index a869898..fd8e98b 100644 --- a/tests/markdown/__snapshots__/multiple-packages.snap.md +++ b/tests/markdown/__snapshots__/multiple-packages.snap.md @@ -21,6 +21,9 @@ ```
+### Package.Core +✓ 🟢 src/core/utils.cs — 3/3 changed lines covered + --- Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/simple-report.snap.md b/tests/markdown/__snapshots__/simple-packages.snap.md similarity index 100% rename from tests/markdown/__snapshots__/simple-report.snap.md rename to tests/markdown/__snapshots__/simple-packages.snap.md diff --git a/tests/markdown/__snapshots__/truncation-files.snap.md b/tests/markdown/__snapshots__/truncation-files.snap.md index d56b6e2..7f98cea 100644 --- a/tests/markdown/__snapshots__/truncation-files.snap.md +++ b/tests/markdown/__snapshots__/truncation-files.snap.md @@ -1,11 +1,11 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- ## PR Coverage -0/1 changed lines covered (Lines: 0%, Branches: n/a) +0/3 changed lines covered (Lines: 0%, Branches: n/a) ### MultiFilePkg
@@ -30,4 +30,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/truncation-minimal.snap.md b/tests/markdown/__snapshots__/truncation-minimal.snap.md index 90fe21e..c7aefcd 100644 --- a/tests/markdown/__snapshots__/truncation-minimal.snap.md +++ b/tests/markdown/__snapshots__/truncation-minimal.snap.md @@ -1,11 +1,11 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- ## PR Coverage -0/1 changed lines covered (Lines: 0%, Branches: n/a) +0/4 changed lines covered (Lines: 0%, Branches: n/a) ### LargePkg
@@ -30,4 +30,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/truncation-none.snap.md b/tests/markdown/__snapshots__/truncation-none.snap.md index 4f19c82..72c9908 100644 --- a/tests/markdown/__snapshots__/truncation-none.snap.md +++ b/tests/markdown/__snapshots__/truncation-none.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- @@ -19,4 +19,4 @@ --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/truncation-packages.snap.md b/tests/markdown/__snapshots__/truncation-packages.snap.md index 46e5ded..5fbdb62 100644 --- a/tests/markdown/__snapshots__/truncation-packages.snap.md +++ b/tests/markdown/__snapshots__/truncation-packages.snap.md @@ -1,11 +1,11 @@ ## Repo Coverage ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n/a-brightgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- ## PR Coverage -0/1 changed lines covered (Lines: 0%, Branches: n/a) +0/3 changed lines covered (Lines: 0%, Branches: n/a) ### Pkg1
@@ -21,9 +21,8 @@
### Pkg2 - ... (2 file(s) and 1 package(s) not shown due to size limit) --- -Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
+Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/generator.test.ts b/tests/markdown/generator.test.ts index 068775c..14911f8 100644 --- a/tests/markdown/generator.test.ts +++ b/tests/markdown/generator.test.ts @@ -8,7 +8,7 @@ type FakeFileInfo = { } /** - * Generate fake file contents for a coverage report. + * Generate fake file contents for a coverage packages. * Creates mock lines keyed by resolvedPath. */ function createFakeFileContents(fileInfos: FakeFileInfo[]): Map { @@ -25,8 +25,8 @@ function createFakeFileContents(fileInfos: FakeFileInfo[]): Map { - it('generates markdown for a simple report', async () => { - const report: PackageCoverage[] = [ + it('generates markdown for a simple packages', async () => { + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -47,15 +47,15 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/example.ts', numberOfLines: 10 }]), ) - await expect(markdown).toMatchFileSnapshot('./__snapshots__/simple-report.snap.md') + await expect(markdown).toMatchFileSnapshot('./__snapshots__/simple-packages.snap.md') }) it('generates markdown for multiple packages', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'Package.Core', files: [ @@ -88,7 +88,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/src/core/utils.cs', numberOfLines: 15 }, { resolvedPath: '/repo/tests/test_utils.cs', numberOfLines: 10 }, @@ -99,7 +99,7 @@ describe('generateMarkdown', () => { }) it('generates markdown with line gaps shown as ellipsis', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'GapsPackage', files: [ @@ -123,7 +123,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/gaps.rs', numberOfLines: 50 }]), ) @@ -132,20 +132,20 @@ describe('generateMarkdown', () => { }) it('generates markdown for empty package', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'EmptyPackage', files: [], }, ] - const markdown = generateMarkdown(report, createFakeFileContents([])) + const markdown = generateMarkdown(packages, createFakeFileContents([])) await expect(markdown).toMatchFileSnapshot('./__snapshots__/empty-package.snap.md') }) it('generates markdown with multiple files in same package', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'MultiFilePackage', files: [ @@ -174,7 +174,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/src/file1.py', numberOfLines: 3 }, { resolvedPath: '/repo/src/file2.py', numberOfLines: 3 }, @@ -185,7 +185,7 @@ describe('generateMarkdown', () => { }) it('correctly maps file extensions to syntax highlighting', () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'ExtensionTest', files: [ @@ -212,7 +212,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/test.cs', numberOfLines: 1 }, { resolvedPath: '/repo/test.rs', numberOfLines: 2 }, @@ -227,7 +227,7 @@ describe('generateMarkdown', () => { describe('ellipsis handling', () => { it('prepends ellipsis when file has content before first shown line', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -246,7 +246,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 10 }]), ) @@ -254,7 +254,7 @@ describe('generateMarkdown', () => { }) it('appends ellipsis when file has content after last shown line', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -274,7 +274,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 10 }]), ) @@ -282,7 +282,7 @@ describe('generateMarkdown', () => { }) it('shows actual line instead of ellipsis for 1-line gap', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -304,7 +304,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 5 }]), ) @@ -312,7 +312,7 @@ describe('generateMarkdown', () => { }) it('shows single ellipsis for 2+ line gap', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -336,7 +336,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 15 }]), ) @@ -344,7 +344,7 @@ describe('generateMarkdown', () => { }) it('shows actual line 1 instead of leading ellipsis when only 1 line hidden at start', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -365,7 +365,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 6 }]), ) @@ -373,7 +373,7 @@ describe('generateMarkdown', () => { }) it('shows actual last line instead of trailing ellipsis when only 1 line hidden at end', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -394,7 +394,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 5 }]), ) @@ -404,7 +404,7 @@ describe('generateMarkdown', () => { describe('missing file content handling', () => { it('renders empty content when file is not found', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -424,13 +424,13 @@ describe('generateMarkdown', () => { // Empty map - file not found on disk const fileContents = new Map() - const markdown = generateMarkdown(report, fileContents) + const markdown = generateMarkdown(packages, fileContents) await expect(markdown).toMatchFileSnapshot('./__snapshots__/file-not-found.snap.md') }) it('renders empty content when line number exceeds file length', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -450,7 +450,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/short.ts', numberOfLines: 3 }]), ) @@ -460,7 +460,7 @@ describe('generateMarkdown', () => { describe('character limit and truncation', () => { it('throws error when maxCharacters is below minimum', () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'Pkg', files: [ @@ -475,14 +475,14 @@ describe('generateMarkdown', () => { ] expect(() => - generateMarkdown(report, createFakeFileContents([{ resolvedPath: '/repo/a.ts', numberOfLines: 1 }]), { + generateMarkdown(packages, createFakeFileContents([{ resolvedPath: '/repo/a.ts', numberOfLines: 1 }]), { maxCharacters: 100, }), ).toThrow(`maxCharacters must be at least ${MINIMUM_CHARACTERS}`) }) it('does not truncate when content fits within limit', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'SmallPkg', files: [ @@ -497,7 +497,7 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/small.ts', numberOfLines: 2 }]), { maxCharacters: 2000, @@ -509,7 +509,7 @@ describe('generateMarkdown', () => { }) it('truncates files from end when limit exceeded', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'MultiFilePkg', files: [ @@ -536,13 +536,13 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/file1.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file2.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file3.ts', numberOfLines: 2 }, ]), - { maxCharacters: 550 }, + { maxCharacters: 1000 }, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-files.snap.md') @@ -551,7 +551,7 @@ describe('generateMarkdown', () => { }) it('truncates entire packages from end when limit exceeded', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'Pkg1', files: [ @@ -591,13 +591,13 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/pkg1/a.ts', numberOfLines: 5 }, { resolvedPath: '/repo/pkg2/b.ts', numberOfLines: 2 }, { resolvedPath: '/repo/pkg3/c.ts', numberOfLines: 2 }, ]), - { maxCharacters: 500 }, + { maxCharacters: 900 }, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-packages.snap.md') @@ -607,7 +607,7 @@ describe('generateMarkdown', () => { it('shows minimal output with badges and legend when severely limited', async () => { // Create multiple files to force truncation at minimum limit - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'LargePkg', files: [ @@ -640,26 +640,26 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/file1.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file2.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file3.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file4.ts', numberOfLines: 2 }, ]), - { maxCharacters: 500 }, + { maxCharacters: 1000 }, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-minimal.snap.md') // Should always have badges and legend expect(markdown).toContain('img.shields.io') - expect(markdown).toContain('Not covered') + expect(markdown).toContain('uncovered') // Should show truncation notice expect(markdown).toContain('not shown due to size limit') }) it('uses default maxCharacters of 65536 when not specified', () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'Pkg', files: [ @@ -675,7 +675,7 @@ describe('generateMarkdown', () => { // Should not throw and should not truncate small content const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/test.ts', numberOfLines: 2 }]), ) From bd769f783c7ed3c0d254b05364e027aeb3e0c8de Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 15:51:21 +0100 Subject: [PATCH 04/13] feat: Add logging information to markdown generation --- src/core/process-coverage.ts | 2 +- src/markdown/generator.ts | 40 +++++++++++++++----------- tests/markdown/generator.test.ts | 48 ++++++++++++++++++++++++++++---- 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/src/core/process-coverage.ts b/src/core/process-coverage.ts index 845843d..36db01a 100644 --- a/src/core/process-coverage.ts +++ b/src/core/process-coverage.ts @@ -152,7 +152,7 @@ export async function processCoverage( } } // Generate Markdown from filtered report - const markdown = generateMarkdown(filteredPackages, fileContents) + const markdown = generateMarkdown(filteredPackages, fileContents, {}, logger) // Calculate overall metrics for outputs (from original merged report for accuracy) const metrics = calculateOverallMetrics(mergedPackages) diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index 82fed75..edb7f77 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -1,4 +1,5 @@ import type { CoverageMetrics, FileCoverage, LineCoverage, PackageCoverage } from '../coverage/model.js' +import type { Logger } from '../core/index.js' // ============================================================================= // CONSTANTS @@ -85,6 +86,7 @@ type FileCoverageClassification = { * @param packages - Array of packages * @param fileContents - Map of resolved (absolute) path to array of line contents * @param options - Optional configuration for Markdown generation + * @param logger - Optional logger for emitting log messages * @returns Markdown string representation * @throws Error if maxCharacters is below MINIMUM_CHARACTERS */ @@ -92,6 +94,7 @@ export function generateMarkdown( packages: PackageCoverage[], fileContents: Map, options: MarkdownOptions = {}, + logger: Logger, ): string { const numberOfSurroundingLines = options.numberOfSurroundingLines ?? DEFAULT_MAX_NUMBER_OF_SURROUNDING_LINES const maxCharacters = options.maxCharacters ?? DEFAULT_MAX_CHARACTERS @@ -116,16 +119,14 @@ export function generateMarkdown( // Step 5: Render markdown with truncation if (packageSections.length === 0) { - return buildMarkdownForNoUncoveredContent(badges, prMetrics) + const markdown = buildMarkdownForNoUncoveredContent(badges, prMetrics) + logger.info(`Generated Markdown with ${markdown.length} characters`) + return markdown } - return buildMarkdownWithinLimitFileLevel({ - badges, - prMetrics, - legend, - packageSections, - maxCharacters, - }) + const markdown = buildMarkdownWithinLimitFileLevel(badges, prMetrics, legend, packageSections, maxCharacters, logger) + logger.info(`Generated Markdown with ${markdown.length} characters`) + return markdown } // ============================================================================= @@ -404,15 +405,14 @@ class CharBudget { /** * Build markdown with truncation at file level when limit exceeded. */ -function buildMarkdownWithinLimitFileLevel(params: { - badges: string - prMetrics: PRMetrics - legend: string - packageSections: PackageSectionData[] - maxCharacters: number -}): string { - const { badges, prMetrics, legend, packageSections, maxCharacters } = params - +function buildMarkdownWithinLimitFileLevel( + badges: string, + prMetrics: PRMetrics, + legend: string, + packageSections: PackageSectionData[], + maxCharacters: number, + logger?: Logger, +): string { // Legend with separator is always appended at the end const legendWithSeparator = '\n---\n\n' + legend @@ -471,6 +471,12 @@ function buildMarkdownWithinLimitFileLevel(params: { if (truncated && (omittedFiles > 0 || omittedPackages > 0)) { const notice = renderTruncationNotice(omittedFiles, omittedPackages) budget.tryAppend(notice + '\n') + + if (omittedFiles > 0) { + logger?.warning( + `Truncated ${omittedPackages} packages, ${omittedFiles} files to fit into size constraint of ${maxCharacters} characters`, + ) + } } // Build final output with legend diff --git a/tests/markdown/generator.test.ts b/tests/markdown/generator.test.ts index 14911f8..187d1bb 100644 --- a/tests/markdown/generator.test.ts +++ b/tests/markdown/generator.test.ts @@ -1,12 +1,15 @@ import { describe, expect, it } from 'vitest' import type { PackageCoverage } from '../../src/coverage/model.js' import { generateMarkdown, MINIMUM_CHARACTERS } from '../../src/markdown/index.js' +import { createCliLogger } from '../../src/core/process-coverage.js' type FakeFileInfo = { resolvedPath: string numberOfLines: number } +const logger = createCliLogger(true) + /** * Generate fake file contents for a coverage packages. * Creates mock lines keyed by resolvedPath. @@ -49,6 +52,8 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/example.ts', numberOfLines: 10 }]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/simple-packages.snap.md') @@ -93,6 +98,8 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/src/core/utils.cs', numberOfLines: 15 }, { resolvedPath: '/repo/tests/test_utils.cs', numberOfLines: 10 }, ]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/multiple-packages.snap.md') @@ -125,6 +132,8 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/gaps.rs', numberOfLines: 50 }]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/line-gaps-ellipsis.snap.md') @@ -139,7 +148,7 @@ describe('generateMarkdown', () => { }, ] - const markdown = generateMarkdown(packages, createFakeFileContents([])) + const markdown = generateMarkdown(packages, createFakeFileContents([]), {}, logger) await expect(markdown).toMatchFileSnapshot('./__snapshots__/empty-package.snap.md') }) @@ -179,6 +188,8 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/src/file1.py', numberOfLines: 3 }, { resolvedPath: '/repo/src/file2.py', numberOfLines: 3 }, ]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/multiple-files-same-package.snap.md') @@ -218,6 +229,8 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/test.rs', numberOfLines: 2 }, { resolvedPath: '/repo/test.tsx', numberOfLines: 3 }, ]), + {}, + logger, ) expect(markdown).toContain('```csharp') @@ -248,6 +261,8 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 10 }]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/ellipsis-leading.snap.md') @@ -276,6 +291,8 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 10 }]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/ellipsis-trailing.snap.md') @@ -306,6 +323,8 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 5 }]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/gap-1-line.snap.md') @@ -338,6 +357,8 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 15 }]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/gap-2-plus-lines.snap.md') @@ -367,6 +388,8 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 6 }]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/ellipsis-leading-1-line.snap.md') @@ -396,6 +419,8 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 5 }]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/ellipsis-trailing-1-line.snap.md') @@ -424,7 +449,7 @@ describe('generateMarkdown', () => { // Empty map - file not found on disk const fileContents = new Map() - const markdown = generateMarkdown(packages, fileContents) + const markdown = generateMarkdown(packages, fileContents, {}, logger) await expect(markdown).toMatchFileSnapshot('./__snapshots__/file-not-found.snap.md') }) @@ -452,6 +477,8 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/short.ts', numberOfLines: 3 }]), + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/line-exceeds-file-length.snap.md') @@ -475,9 +502,14 @@ describe('generateMarkdown', () => { ] expect(() => - generateMarkdown(packages, createFakeFileContents([{ resolvedPath: '/repo/a.ts', numberOfLines: 1 }]), { - maxCharacters: 100, - }), + generateMarkdown( + packages, + createFakeFileContents([{ resolvedPath: '/repo/a.ts', numberOfLines: 1 }]), + { + maxCharacters: 100, + }, + logger, + ), ).toThrow(`maxCharacters must be at least ${MINIMUM_CHARACTERS}`) }) @@ -502,6 +534,7 @@ describe('generateMarkdown', () => { { maxCharacters: 2000, }, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-none.snap.md') @@ -543,6 +576,7 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/file3.ts', numberOfLines: 2 }, ]), { maxCharacters: 1000 }, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-files.snap.md') @@ -598,6 +632,7 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/pkg3/c.ts', numberOfLines: 2 }, ]), { maxCharacters: 900 }, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-packages.snap.md') @@ -648,6 +683,7 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/file4.ts', numberOfLines: 2 }, ]), { maxCharacters: 1000 }, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-minimal.snap.md') @@ -677,6 +713,8 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/test.ts', numberOfLines: 2 }]), + {}, + logger, ) expect(markdown).not.toContain('not shown due to size limit') From 59c8ab7bb100a173ab4b49dbcfbed33f9e68ffe6 Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 15:54:04 +0100 Subject: [PATCH 05/13] chore: Small cleanup --- src/markdown/generator.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index edb7f77..66e1e10 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -370,11 +370,6 @@ class CharBudget { this.reservedTailLength = reservedTail.length } - /** Current total length including reserved tail. */ - get totalLength(): number { - return this.currentLength + this.reservedTailLength - } - /** Check if adding content would exceed the limit. */ wouldExceed(content: string): boolean { return this.currentLength + content.length + this.reservedTailLength > this.maxCharacters From f600c6b4e8759dbcc18cd877b184516c779ad9c1 Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 16:04:37 +0100 Subject: [PATCH 06/13] fix: Comment on open PRs only --- src/github.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/github.ts b/src/github.ts index fd3ca62..254d669 100644 --- a/src/github.ts +++ b/src/github.ts @@ -38,12 +38,18 @@ const getEnv = (name: string): string => { /** * Find the pull request number associated with the current context. + * Only returns PRs that are still open (not merged or closed). */ export async function findPullRequestNumber(octokit: Octokit, context: Context): Promise { // Check if we're already in a pull_request event if ('pull_request' in context.payload) { - const prPayload = context.payload as { pull_request?: { number: number } } + const prPayload = context.payload as { pull_request?: { number: number; state?: string } } if (prPayload.pull_request?.number) { + // For pull_request events, check if the PR is still open + // If it's closed/merged, we shouldn't update comments + if (prPayload.pull_request.state !== 'open') { + return null + } return prPayload.pull_request.number } } @@ -55,7 +61,21 @@ export async function findPullRequestNumber(octokit: Octokit, context: Context): commit_sha: context.sha, }) - return pulls[0]?.number ?? null + // Filter to only open PRs and return the first one + // If state is not available in the response, fetch PR details to check + for (const pull of pulls) { + const prNumber = pull.number + if (!prNumber) continue + if ('state' in pull) { + // Skip closed/merged PRs immediately + if (pull.state !== 'open') { + continue + } + return prNumber + } + } + + return null } /** From 6f64e162b4798653f08411733f9da3fdb5da47cf Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 17:35:18 +0100 Subject: [PATCH 07/13] fix: Consolidate percentage formatting --- src/markdown/generator.ts | 30 +++++++++++++------ .../core/__snapshots__/basic-coverage.snap.md | 4 +-- tests/core/__snapshots__/glob-filter.snap.md | 4 +-- .../core/__snapshots__/merge-reports.snap.md | 4 +-- .../__snapshots__/multiple-packages.snap.md | 8 ++--- .../ellipsis-leading-1-line.snap.md | 2 +- .../__snapshots__/ellipsis-leading.snap.md | 2 +- .../ellipsis-trailing-1-line.snap.md | 2 +- .../__snapshots__/ellipsis-trailing.snap.md | 4 +-- .../__snapshots__/file-not-found.snap.md | 2 +- .../markdown/__snapshots__/gap-1-line.snap.md | 2 +- .../__snapshots__/gap-2-plus-lines.snap.md | 4 +-- .../line-exceeds-file-length.snap.md | 4 +-- .../__snapshots__/line-gaps-ellipsis.snap.md | 4 +-- .../multiple-files-same-package.snap.md | 6 ++-- .../__snapshots__/multiple-packages.snap.md | 2 +- .../__snapshots__/simple-packages.snap.md | 4 +-- .../__snapshots__/truncation-files.snap.md | 2 +- .../__snapshots__/truncation-minimal.snap.md | 2 +- .../__snapshots__/truncation-none.snap.md | 2 +- .../__snapshots__/truncation-packages.snap.md | 2 +- 21 files changed, 54 insertions(+), 42 deletions(-) diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index 66e1e10..7d0d315 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -37,7 +37,7 @@ export const MINIMUM_CHARACTERS = 900 export type MarkdownOptions = { /** Number of lines to show before and after uncovered lines (default: 1) */ numberOfSurroundingLines?: number - /** Maximum number of characters in the output (default: 65536, minimum: 500) */ + /** Maximum number of characters in the output (default: 65536, minimum: 900) */ maxCharacters?: number } @@ -244,11 +244,23 @@ function formatRatio(covered: number, total: number): string { } /** - * Format a percentage rounded to nearest integer. + * Format a percentage value (0-100) to up to 2 decimal places. + * Removes trailing zeros for cleaner output. + * Examples: 100 → "100%", 99.99 → "99.99%", 20.5 → "20.5%" */ -function formatPercentRounded(covered: number, total: number): string { +function formatPercent(value: number): string { + const formatted = value.toFixed(2) + // Remove trailing zeros and decimal point if not needed + return formatted.replace(/\.?0+$/, '') + '%' +} + +/** + * Calculate and format percentage from covered/total ratio. + * Returns "0%" if total is 0. + */ +function formatPercentFromRatio(covered: number, total: number): string { if (total === 0) return '0%' - return `${Math.round((covered / total) * 100)}%` + return formatPercent((covered / total) * 100) } /** @@ -258,7 +270,7 @@ function formatBranchPercentOrNA(branchMetrics: CoverageMetrics | undefined): st if (!branchMetrics || branchMetrics.total === 0) { return 'n/a' } - return `${Math.round((branchMetrics.covered / branchMetrics.total) * 100)}%` + return formatPercentFromRatio(branchMetrics.covered, branchMetrics.total) } // ============================================================================= @@ -493,8 +505,8 @@ function renderPRSummaryLine(metrics: PRMetrics): string { return '0/0 changed lines covered' } - const linePercentStr = formatPercentRounded(metrics.coveredLines, metrics.totalLines) - const branchPercentStr = metrics.branchPercent !== null ? `${Math.round(metrics.branchPercent)}%` : 'n/a' + const linePercentStr = formatPercentFromRatio(metrics.coveredLines, metrics.totalLines) + const branchPercentStr = metrics.branchPercent !== null ? formatPercent(metrics.branchPercent) : 'n/a' return `${formatRatio(metrics.coveredLines, metrics.totalLines)} changed lines covered (Lines: ${linePercentStr}, Branches: ${branchPercentStr})` } @@ -545,7 +557,7 @@ function renderBadge(label: string, metrics?: CoverageMetrics): string { color = 'brightgreen' } else { const percent = (metrics.covered / metrics.total) * 100 - value = `${percent.toFixed(2)}%` + value = formatPercent(percent) color = getBadgeColor(percent) } const encodedValue = encodeURIComponent(value) @@ -584,7 +596,7 @@ function renderFileSection( // Has uncovered/partial lines - use details block const lines: string[] = [] - const linePercent = formatPercentRounded(file.lineMetrics.covered, file.lineMetrics.total) + const linePercent = formatPercentFromRatio(file.lineMetrics.covered, file.lineMetrics.total) const branchPercent = formatBranchPercentOrNA(file.branchMetrics) lines.push(`
`) diff --git a/tests/core/__snapshots__/basic-coverage.snap.md b/tests/core/__snapshots__/basic-coverage.snap.md index 07609c4..305c6c0 100644 --- a/tests/core/__snapshots__/basic-coverage.snap.md +++ b/tests/core/__snapshots__/basic-coverage.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) --- diff --git a/tests/core/__snapshots__/glob-filter.snap.md b/tests/core/__snapshots__/glob-filter.snap.md index 7d058bd..95abfca 100644 --- a/tests/core/__snapshots__/glob-filter.snap.md +++ b/tests/core/__snapshots__/glob-filter.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.00%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) --- diff --git a/tests/core/__snapshots__/merge-reports.snap.md b/tests/core/__snapshots__/merge-reports.snap.md index 4482c66..dfa1fc3 100644 --- a/tests/core/__snapshots__/merge-reports.snap.md +++ b/tests/core/__snapshots__/merge-reports.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) --- diff --git a/tests/core/__snapshots__/multiple-packages.snap.md b/tests/core/__snapshots__/multiple-packages.snap.md index 27c2f71..3300ac2 100644 --- a/tests/core/__snapshots__/multiple-packages.snap.md +++ b/tests/core/__snapshots__/multiple-packages.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.00%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) --- @@ -9,7 +9,7 @@ ### utils
-🔴 src/utils/format.ts — 1/3 changed lines covered (Lines: 33%, Branches: 100%) +🔴 src/utils/format.ts — 1/3 changed lines covered (Lines: 33.33%, Branches: 100%) ```typescript 1 🟩 export function formatNumber(n: number): string { @@ -21,7 +21,7 @@ ### core
-🔴 src/core/math.ts — 2/3 changed lines covered (Lines: 67%, Branches: 100%) +🔴 src/core/math.ts — 2/3 changed lines covered (Lines: 66.67%, Branches: 100%) ```typescript 1 🟩 export function add(a: number, b: number): number { diff --git a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md index fa3da87..472527f 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75.00%25-green.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/ellipsis-leading.snap.md b/tests/markdown/__snapshots__/ellipsis-leading.snap.md index 6245d37..e51ec5b 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.00%25-yellowgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md index 62b36e5..d6177e9 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-80.00%25-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-80%25-brightgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md index 6f1c922..38a0671 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md @@ -5,11 +5,11 @@ --- ## PR Coverage -2/3 changed lines covered (Lines: 67%, Branches: n/a) +2/3 changed lines covered (Lines: 66.67%, Branches: n/a) ### TestPackage
-🔴 src/test.ts — 2/3 changed lines covered (Lines: 67%, Branches: n/a) +🔴 src/test.ts — 2/3 changed lines covered (Lines: 66.67%, Branches: n/a) ```typescript 1 🟩 some content in line 1 diff --git a/tests/markdown/__snapshots__/file-not-found.snap.md b/tests/markdown/__snapshots__/file-not-found.snap.md index e7560fb..179cc7e 100644 --- a/tests/markdown/__snapshots__/file-not-found.snap.md +++ b/tests/markdown/__snapshots__/file-not-found.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/gap-1-line.snap.md b/tests/markdown/__snapshots__/gap-1-line.snap.md index 43fb738..100bf2f 100644 --- a/tests/markdown/__snapshots__/gap-1-line.snap.md +++ b/tests/markdown/__snapshots__/gap-1-line.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md index 220847e..c748b6b 100644 --- a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md +++ b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md @@ -5,11 +5,11 @@ --- ## PR Coverage -4/6 changed lines covered (Lines: 67%, Branches: n/a) +4/6 changed lines covered (Lines: 66.67%, Branches: n/a) ### TestPackage
-🔴 src/test.ts — 4/6 changed lines covered (Lines: 67%, Branches: n/a) +🔴 src/test.ts — 4/6 changed lines covered (Lines: 66.67%, Branches: n/a) ```typescript 1 🟩 some content in line 1 diff --git a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md index 54252ed..0ce6b1e 100644 --- a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md +++ b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md @@ -5,11 +5,11 @@ --- ## PR Coverage -2/3 changed lines covered (Lines: 67%, Branches: n/a) +2/3 changed lines covered (Lines: 66.67%, Branches: n/a) ### TestPackage
-🔴 src/short.ts — 2/3 changed lines covered (Lines: 67%, Branches: n/a) +🔴 src/short.ts — 2/3 changed lines covered (Lines: 66.67%, Branches: n/a) ```typescript ... diff --git a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md index c388f77..5edb3e3 100644 --- a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md +++ b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-40.00%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0.00%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-40%25-yellowgreen.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md index 9a64d34..3cae376 100644 --- a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md +++ b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0.00%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) --- @@ -11,7 +11,7 @@ ✓ 🟢 src/file1.py — 2/2 changed lines covered
-🔴 src/file2.py — 1/3 changed lines covered (Lines: 33%, Branches: 0%) +🔴 src/file2.py — 1/3 changed lines covered (Lines: 33.33%, Branches: 0%) ```python 1 🟥 some content in line 1 diff --git a/tests/markdown/__snapshots__/multiple-packages.snap.md b/tests/markdown/__snapshots__/multiple-packages.snap.md index fd8e98b..6795409 100644 --- a/tests/markdown/__snapshots__/multiple-packages.snap.md +++ b/tests/markdown/__snapshots__/multiple-packages.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/simple-packages.snap.md b/tests/markdown/__snapshots__/simple-packages.snap.md index 95729af..a28dc77 100644 --- a/tests/markdown/__snapshots__/simple-packages.snap.md +++ b/tests/markdown/__snapshots__/simple-packages.snap.md @@ -1,6 +1,6 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0.00%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-files.snap.md b/tests/markdown/__snapshots__/truncation-files.snap.md index 7f98cea..e510923 100644 --- a/tests/markdown/__snapshots__/truncation-files.snap.md +++ b/tests/markdown/__snapshots__/truncation-files.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-minimal.snap.md b/tests/markdown/__snapshots__/truncation-minimal.snap.md index c7aefcd..2417431 100644 --- a/tests/markdown/__snapshots__/truncation-minimal.snap.md +++ b/tests/markdown/__snapshots__/truncation-minimal.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-none.snap.md b/tests/markdown/__snapshots__/truncation-none.snap.md index 72c9908..a44712c 100644 --- a/tests/markdown/__snapshots__/truncation-none.snap.md +++ b/tests/markdown/__snapshots__/truncation-none.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-packages.snap.md b/tests/markdown/__snapshots__/truncation-packages.snap.md index 5fbdb62..9c5bfa1 100644 --- a/tests/markdown/__snapshots__/truncation-packages.snap.md +++ b/tests/markdown/__snapshots__/truncation-packages.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- From a5b203271e7d7dd9293607d0b290239afb6b2f5a Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 17:36:42 +0100 Subject: [PATCH 08/13] chore: logger is not required consistently --- src/markdown/generator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index 7d0d315..611f2d3 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -418,7 +418,7 @@ function buildMarkdownWithinLimitFileLevel( legend: string, packageSections: PackageSectionData[], maxCharacters: number, - logger?: Logger, + logger: Logger, ): string { // Legend with separator is always appended at the end const legendWithSeparator = '\n---\n\n' + legend @@ -480,7 +480,7 @@ function buildMarkdownWithinLimitFileLevel( budget.tryAppend(notice + '\n') if (omittedFiles > 0) { - logger?.warning( + logger.warning( `Truncated ${omittedPackages} packages, ${omittedFiles} files to fit into size constraint of ${maxCharacters} characters`, ) } From aac4021067692fd5b7b20a984d9c9c5a81cae6c0 Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 17:42:39 +0100 Subject: [PATCH 09/13] fix: Spacing between badges to ensure they are rendered in a single line --- src/markdown/generator.ts | 2 +- tests/core/__snapshots__/basic-coverage.snap.md | 3 +-- tests/core/__snapshots__/glob-filter.snap.md | 3 +-- tests/core/__snapshots__/merge-reports.snap.md | 3 +-- tests/core/__snapshots__/multiple-packages.snap.md | 3 +-- tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md | 3 +-- tests/markdown/__snapshots__/ellipsis-leading.snap.md | 3 +-- tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md | 3 +-- tests/markdown/__snapshots__/ellipsis-trailing.snap.md | 3 +-- tests/markdown/__snapshots__/empty-package.snap.md | 3 +-- tests/markdown/__snapshots__/file-not-found.snap.md | 3 +-- tests/markdown/__snapshots__/gap-1-line.snap.md | 3 +-- tests/markdown/__snapshots__/gap-2-plus-lines.snap.md | 3 +-- tests/markdown/__snapshots__/line-exceeds-file-length.snap.md | 3 +-- tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md | 3 +-- .../markdown/__snapshots__/multiple-files-same-package.snap.md | 3 +-- tests/markdown/__snapshots__/multiple-packages.snap.md | 3 +-- tests/markdown/__snapshots__/simple-packages.snap.md | 3 +-- tests/markdown/__snapshots__/truncation-files.snap.md | 3 +-- tests/markdown/__snapshots__/truncation-minimal.snap.md | 3 +-- tests/markdown/__snapshots__/truncation-none.snap.md | 3 +-- tests/markdown/__snapshots__/truncation-packages.snap.md | 3 +-- 22 files changed, 22 insertions(+), 43 deletions(-) diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index 611f2d3..19d7843 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -541,7 +541,7 @@ function generateCoverageBadges(packages: PackageCoverage[]): string { const badges: string[] = [] badges.push(renderBadge('Line Coverage', metrics.lineMetrics)) badges.push(renderBadge('Branch Coverage', metrics.branchMetrics ?? { covered: 0, total: 0 })) - return badges.join('\n') + return badges.join(' ') } /** diff --git a/tests/core/__snapshots__/basic-coverage.snap.md b/tests/core/__snapshots__/basic-coverage.snap.md index 305c6c0..bb0ad49 100644 --- a/tests/core/__snapshots__/basic-coverage.snap.md +++ b/tests/core/__snapshots__/basic-coverage.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) --- diff --git a/tests/core/__snapshots__/glob-filter.snap.md b/tests/core/__snapshots__/glob-filter.snap.md index 95abfca..72966a1 100644 --- a/tests/core/__snapshots__/glob-filter.snap.md +++ b/tests/core/__snapshots__/glob-filter.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) --- diff --git a/tests/core/__snapshots__/merge-reports.snap.md b/tests/core/__snapshots__/merge-reports.snap.md index dfa1fc3..eb43afc 100644 --- a/tests/core/__snapshots__/merge-reports.snap.md +++ b/tests/core/__snapshots__/merge-reports.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) --- diff --git a/tests/core/__snapshots__/multiple-packages.snap.md b/tests/core/__snapshots__/multiple-packages.snap.md index 3300ac2..21d2ab9 100644 --- a/tests/core/__snapshots__/multiple-packages.snap.md +++ b/tests/core/__snapshots__/multiple-packages.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md index 472527f..bd0ddcb 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/ellipsis-leading.snap.md b/tests/markdown/__snapshots__/ellipsis-leading.snap.md index e51ec5b..e926644 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md index d6177e9..968e39f 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-80%25-brightgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-80%25-brightgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md index 38a0671..312f3d3 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/empty-package.snap.md b/tests/markdown/__snapshots__/empty-package.snap.md index 3bbcf9a..7c76009 100644 --- a/tests/markdown/__snapshots__/empty-package.snap.md +++ b/tests/markdown/__snapshots__/empty-package.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-n%2Fa-brightgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-n%2Fa-brightgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/file-not-found.snap.md b/tests/markdown/__snapshots__/file-not-found.snap.md index 179cc7e..c8499db 100644 --- a/tests/markdown/__snapshots__/file-not-found.snap.md +++ b/tests/markdown/__snapshots__/file-not-found.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/gap-1-line.snap.md b/tests/markdown/__snapshots__/gap-1-line.snap.md index 100bf2f..291070b 100644 --- a/tests/markdown/__snapshots__/gap-1-line.snap.md +++ b/tests/markdown/__snapshots__/gap-1-line.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md index c748b6b..b12c6cb 100644 --- a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md +++ b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md index 0ce6b1e..570323a 100644 --- a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md +++ b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md index 5edb3e3..787d641 100644 --- a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md +++ b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-40%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-40%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md index 3cae376..05f5f66 100644 --- a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md +++ b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/multiple-packages.snap.md b/tests/markdown/__snapshots__/multiple-packages.snap.md index 6795409..6c6c21b 100644 --- a/tests/markdown/__snapshots__/multiple-packages.snap.md +++ b/tests/markdown/__snapshots__/multiple-packages.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/simple-packages.snap.md b/tests/markdown/__snapshots__/simple-packages.snap.md index a28dc77..ce8ae7a 100644 --- a/tests/markdown/__snapshots__/simple-packages.snap.md +++ b/tests/markdown/__snapshots__/simple-packages.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-files.snap.md b/tests/markdown/__snapshots__/truncation-files.snap.md index e510923..c470889 100644 --- a/tests/markdown/__snapshots__/truncation-files.snap.md +++ b/tests/markdown/__snapshots__/truncation-files.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-minimal.snap.md b/tests/markdown/__snapshots__/truncation-minimal.snap.md index 2417431..9992d39 100644 --- a/tests/markdown/__snapshots__/truncation-minimal.snap.md +++ b/tests/markdown/__snapshots__/truncation-minimal.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-none.snap.md b/tests/markdown/__snapshots__/truncation-none.snap.md index a44712c..67ab223 100644 --- a/tests/markdown/__snapshots__/truncation-none.snap.md +++ b/tests/markdown/__snapshots__/truncation-none.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-packages.snap.md b/tests/markdown/__snapshots__/truncation-packages.snap.md index 9c5bfa1..810cee0 100644 --- a/tests/markdown/__snapshots__/truncation-packages.snap.md +++ b/tests/markdown/__snapshots__/truncation-packages.snap.md @@ -1,6 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- From 5b73acd34d45e28c6904e9806c93c67eb5801c32 Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 18:21:07 +0100 Subject: [PATCH 10/13] fix: Calculate repo wide coverage correctly --- action.yaml | 4 ++ src/action.ts | 6 +- src/core/process-coverage.ts | 31 +++++++--- src/coverage/model.ts | 5 ++ src/markdown/generator.ts | 62 +++++-------------- tests/core/__snapshots__/glob-filter.snap.md | 13 ++-- .../core/process-coverage.integration.test.ts | 3 + .../ellipsis-leading-1-line.snap.md | 2 +- .../__snapshots__/ellipsis-leading.snap.md | 2 +- .../ellipsis-trailing-1-line.snap.md | 2 +- .../__snapshots__/ellipsis-trailing.snap.md | 2 +- .../__snapshots__/empty-package.snap.md | 2 +- .../__snapshots__/file-not-found.snap.md | 2 +- .../markdown/__snapshots__/gap-1-line.snap.md | 2 +- .../__snapshots__/gap-2-plus-lines.snap.md | 2 +- .../line-exceeds-file-length.snap.md | 2 +- .../__snapshots__/line-gaps-ellipsis.snap.md | 2 +- .../multiple-files-same-package.snap.md | 2 +- .../__snapshots__/multiple-packages.snap.md | 2 +- .../__snapshots__/simple-packages.snap.md | 2 +- .../__snapshots__/truncation-files.snap.md | 2 +- .../__snapshots__/truncation-minimal.snap.md | 2 +- .../__snapshots__/truncation-none.snap.md | 2 +- .../__snapshots__/truncation-packages.snap.md | 2 +- tests/markdown/generator.test.ts | 28 ++++++++- .../integration/glob-filter/coverage.xml | 12 ++-- .../glob-filter/src/exclude/ignored.ts | 4 ++ 27 files changed, 115 insertions(+), 87 deletions(-) diff --git a/action.yaml b/action.yaml index d0b413c..b8ca620 100644 --- a/action.yaml +++ b/action.yaml @@ -31,6 +31,10 @@ outputs: description: The overall line coverage percentage branch-coverage: description: The overall branch coverage percentage + pr-line-coverage: + description: The coverage of the changes of this PR. Equal to the overall line coverage, if not executed on a PR + pr-branch-coverage: + description: The overall branch coverage percentage. Equal to the overall line coverage, if not executed on a PR runs: using: 'node24' diff --git a/src/action.ts b/src/action.ts index 9aa6952..b1a2ce4 100644 --- a/src/action.ts +++ b/src/action.ts @@ -39,11 +39,13 @@ export const run = async (inputs: Inputs, octokit: Octokit, context: Context): P return } - const { markdown, lineCoverage, branchCoverage } = result + const { markdown, lineCoverage, branchCoverage, lineCoveragePr, branchCoveragePr } = result // Set GitHub Actions outputs core.setOutput('line-coverage', lineCoverage.toFixed(2)) - core.setOutput('branch-coverage', branchCoverage.toFixed(2)) + core.setOutput('branch-coverage', branchCoverage?.toFixed(2)) + core.setOutput('pr-line-coverage', lineCoveragePr.toFixed(2)) + core.setOutput('pr-branch-coverage', branchCoveragePr?.toFixed(2)) // Find the pull request (needed for posting) const pullNumber = await findPullRequestNumber(octokit, context) diff --git a/src/core/process-coverage.ts b/src/core/process-coverage.ts index 36db01a..8d99b9e 100644 --- a/src/core/process-coverage.ts +++ b/src/core/process-coverage.ts @@ -11,6 +11,7 @@ import { } from '../coverage/index.js' import { filterByChangedLines, filterByGlob, getChangedLinesFromGit, type ChangedLinesMap } from '../filter/index.js' import { generateMarkdown } from '../markdown/index.js' +import { PercentageCoverageMetrics } from '../coverage/model.js' /** * Logger interface for dependency injection. @@ -53,7 +54,6 @@ export type ProcessCoverageInputs = { /** Explicit head commit SHA for comparison */ headSha?: string | undefined } - /** * Result of coverage processing. */ @@ -61,7 +61,9 @@ export type ProcessCoverageResult = { /** Generated markdown report */ markdown: string lineCoverage: number - branchCoverage: number + branchCoverage: number | undefined + lineCoveragePr: number + branchCoveragePr: number | undefined } /** @@ -151,13 +153,26 @@ export async function processCoverage( logger.debug?.(`Generating markdown for ${file.resolvedPath} with ${file.lines.length} changed lines`) } } - // Generate Markdown from filtered report - const markdown = generateMarkdown(filteredPackages, fileContents, {}, logger) - // Calculate overall metrics for outputs (from original merged report for accuracy) - const metrics = calculateOverallMetrics(mergedPackages) + const overallMetrics = calculateOverallMetrics(mergedPackages) + const prMetrics = calculateOverallMetrics(filteredPackages) + logger.info( + `Calculated overall metrics (LineCoverage: ${overallMetrics.lineCoverage}, BranchCoverage: ${overallMetrics.branchCoverage})`, + ) + logger.info( + `Calculated overall metrics (LineCoverage: ${prMetrics.lineCoverage}, BranchCoverage: ${prMetrics.branchCoverage})`, + ) + + // Generate Markdown from filtered report + const markdown = generateMarkdown(filteredPackages, fileContents, overallMetrics, {}, logger) - return { markdown, lineCoverage: metrics.lineCoverage, branchCoverage: metrics.branchCoverage } + return { + markdown, + lineCoverage: overallMetrics.lineCoverage, + branchCoverage: overallMetrics.branchCoverage, + lineCoveragePr: prMetrics.lineCoverage, + branchCoveragePr: prMetrics.branchCoverage, + } } async function firstExistingDirectory(paths: readonly string[]): Promise { @@ -272,7 +287,7 @@ async function mergeReportAndResolveSources( /** * Calculate overall coverage metrics from a merged packages. */ -function calculateOverallMetrics(packages: PackageCoverage[]): { lineCoverage: number; branchCoverage: number } { +function calculateOverallMetrics(packages: PackageCoverage[]): PercentageCoverageMetrics { let lineCovered = 0 let lineTotal = 0 let branchCovered = 0 diff --git a/src/coverage/model.ts b/src/coverage/model.ts index d25c1e8..6acfc9f 100644 --- a/src/coverage/model.ts +++ b/src/coverage/model.ts @@ -14,6 +14,11 @@ export type CoverageMetrics = { total: number } +export type PercentageCoverageMetrics = { + lineCoverage: number + branchCoverage?: number | undefined +} + export type FileCoverage = { /** Display path (relative to source root, for markdown output) */ filename: string diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index 19d7843..e6fe896 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -1,4 +1,10 @@ -import type { CoverageMetrics, FileCoverage, LineCoverage, PackageCoverage } from '../coverage/model.js' +import { + CoverageMetrics, + FileCoverage, + LineCoverage, + PackageCoverage, + PercentageCoverageMetrics, +} from '../coverage/model.js' import type { Logger } from '../core/index.js' // ============================================================================= @@ -85,6 +91,7 @@ type FileCoverageClassification = { * * @param packages - Array of packages * @param fileContents - Map of resolved (absolute) path to array of line contents + * @param overallMetrics - The overall percentage metrics * @param options - Optional configuration for Markdown generation * @param logger - Optional logger for emitting log messages * @returns Markdown string representation @@ -93,6 +100,7 @@ type FileCoverageClassification = { export function generateMarkdown( packages: PackageCoverage[], fileContents: Map, + overallMetrics: PercentageCoverageMetrics, options: MarkdownOptions = {}, logger: Logger, ): string { @@ -105,7 +113,7 @@ export function generateMarkdown( } // Step 1: Generate fixed content (badges and legend) - const badges = generateCoverageBadges(packages) + const badges = generateCoverageBadges(overallMetrics) const legend = generateLegend() // Step 2: Build package section data @@ -195,43 +203,6 @@ function calculatePRMetrics(packages: PackageCoverage[]): PRMetrics { return { coveredLines, totalLines, linePercent, branchPercent } } -/** - * Calculate aggregate metrics for the entire packages (for badges). - */ -function calculateReportMetrics(packages: PackageCoverage[]): { - lineMetrics: CoverageMetrics - branchMetrics?: CoverageMetrics -} { - let lineCovered = 0 - let lineTotal = 0 - let branchCovered = 0 - let branchTotal = 0 - let hasBranchData = false - - for (const pkg of packages) { - for (const file of pkg.files) { - lineCovered += file.lineMetrics.covered - lineTotal += file.lineMetrics.total - - if (file.branchMetrics) { - hasBranchData = true - branchCovered += file.branchMetrics.covered - branchTotal += file.branchMetrics.total - } - } - } - - const result: { lineMetrics: CoverageMetrics; branchMetrics?: CoverageMetrics } = { - lineMetrics: { covered: lineCovered, total: lineTotal }, - } - - if (hasBranchData) { - result.branchMetrics = { covered: branchCovered, total: branchTotal } - } - - return result -} - // ============================================================================= // FORMATTING HELPERS // ============================================================================= @@ -535,28 +506,25 @@ function generateLegend(): string { /** * Render coverage badges for the overall packages. */ -function generateCoverageBadges(packages: PackageCoverage[]): string { - const metrics = calculateReportMetrics(packages) - +function generateCoverageBadges(overallMetrics: PercentageCoverageMetrics): string { const badges: string[] = [] - badges.push(renderBadge('Line Coverage', metrics.lineMetrics)) - badges.push(renderBadge('Branch Coverage', metrics.branchMetrics ?? { covered: 0, total: 0 })) + badges.push(renderBadge('Line Coverage', overallMetrics.lineCoverage)) + badges.push(renderBadge('Branch Coverage', overallMetrics.branchCoverage)) return badges.join(' ') } /** * Render a single shields.io badge markdown. */ -function renderBadge(label: string, metrics?: CoverageMetrics): string { +function renderBadge(label: string, percent: number | undefined): string { const encodedLabel = encodeURIComponent(label) let value: string let color: string - if (!metrics || metrics.total === 0) { + if (!percent) { value = 'n/a' color = 'brightgreen' } else { - const percent = (metrics.covered / metrics.total) * 100 value = formatPercent(percent) color = getBadgeColor(percent) } diff --git a/tests/core/__snapshots__/glob-filter.snap.md b/tests/core/__snapshots__/glob-filter.snap.md index 72966a1..16cf3d0 100644 --- a/tests/core/__snapshots__/glob-filter.snap.md +++ b/tests/core/__snapshots__/glob-filter.snap.md @@ -1,20 +1,19 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-75%25-green.svg?style=flat) --- ## PR Coverage -1/2 changed lines covered (Lines: 50%, Branches: 100%) +2/2 changed lines covered (Lines: 100%, Branches: 50%) ### filtered
-🔴 src/include/wanted.ts — 1/2 changed lines covered (Lines: 50%, Branches: 100%) +🟠 src/include/wanted.ts — 2/2 changed lines covered (Lines: 100%, Branches: 50%) ```typescript - 1 🟩 export function wanted(): string { - 2 🟥 return 'I am wanted' - 3 ⬛ } - 4 ⬛ + 1 🟨 export function wanted(): string { + 2 🟩 return 'I am wanted' +... ```
diff --git a/tests/core/process-coverage.integration.test.ts b/tests/core/process-coverage.integration.test.ts index 10cb1f9..931f29f 100644 --- a/tests/core/process-coverage.integration.test.ts +++ b/tests/core/process-coverage.integration.test.ts @@ -50,6 +50,9 @@ describe('processCoverage integration', () => { ) expect(result).not.toBeNull() + // Outputs must be computed from the already-filtered data (same as markdown) + expect(result!.lineCoverage).toBeCloseTo(50, 5) + expect(result!.branchCoverage).toBeCloseTo(75, 5) expect(result!.markdown).toContain('wanted.ts') expect(result!.markdown).not.toContain('ignored.ts') await expect(result!.markdown).toMatchFileSnapshot('./__snapshots__/glob-filter.snap.md') diff --git a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md index bd0ddcb..c7d649c 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-70%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-70%25-green.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/ellipsis-leading.snap.md b/tests/markdown/__snapshots__/ellipsis-leading.snap.md index e926644..b4f3126 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-30%25-yellow.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-30%25-yellow.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md index 968e39f..ce7d179 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-80%25-brightgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-80%25-brightgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-80%25-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md index 312f3d3..abc1d45 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-40%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-40%25-yellowgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/empty-package.snap.md b/tests/markdown/__snapshots__/empty-package.snap.md index 7c76009..ffa54c0 100644 --- a/tests/markdown/__snapshots__/empty-package.snap.md +++ b/tests/markdown/__snapshots__/empty-package.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-n%2Fa-brightgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/file-not-found.snap.md b/tests/markdown/__snapshots__/file-not-found.snap.md index c8499db..1d4bb7e 100644 --- a/tests/markdown/__snapshots__/file-not-found.snap.md +++ b/tests/markdown/__snapshots__/file-not-found.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/gap-1-line.snap.md b/tests/markdown/__snapshots__/gap-1-line.snap.md index 291070b..e89414a 100644 --- a/tests/markdown/__snapshots__/gap-1-line.snap.md +++ b/tests/markdown/__snapshots__/gap-1-line.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md index b12c6cb..b7564c6 100644 --- a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md +++ b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-60%25-green.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md index 570323a..6cc4b52 100644 --- a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md +++ b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-90%25-brightgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-90%25-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md index 787d641..0001280 100644 --- a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md +++ b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-40%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md index 05f5f66..ec8be18 100644 --- a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md +++ b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-10%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-10%25-red.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/multiple-packages.snap.md b/tests/markdown/__snapshots__/multiple-packages.snap.md index 6c6c21b..4d2a251 100644 --- a/tests/markdown/__snapshots__/multiple-packages.snap.md +++ b/tests/markdown/__snapshots__/multiple-packages.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/simple-packages.snap.md b/tests/markdown/__snapshots__/simple-packages.snap.md index ce8ae7a..f6b2c7c 100644 --- a/tests/markdown/__snapshots__/simple-packages.snap.md +++ b/tests/markdown/__snapshots__/simple-packages.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0%25-red.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-files.snap.md b/tests/markdown/__snapshots__/truncation-files.snap.md index c470889..dc403c4 100644 --- a/tests/markdown/__snapshots__/truncation-files.snap.md +++ b/tests/markdown/__snapshots__/truncation-files.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-minimal.snap.md b/tests/markdown/__snapshots__/truncation-minimal.snap.md index 9992d39..3b3899c 100644 --- a/tests/markdown/__snapshots__/truncation-minimal.snap.md +++ b/tests/markdown/__snapshots__/truncation-minimal.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-none.snap.md b/tests/markdown/__snapshots__/truncation-none.snap.md index 67ab223..ad5a05f 100644 --- a/tests/markdown/__snapshots__/truncation-none.snap.md +++ b/tests/markdown/__snapshots__/truncation-none.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) --- diff --git a/tests/markdown/__snapshots__/truncation-packages.snap.md b/tests/markdown/__snapshots__/truncation-packages.snap.md index 810cee0..951fb6e 100644 --- a/tests/markdown/__snapshots__/truncation-packages.snap.md +++ b/tests/markdown/__snapshots__/truncation-packages.snap.md @@ -1,5 +1,5 @@ ## Repo Coverage -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) --- diff --git a/tests/markdown/generator.test.ts b/tests/markdown/generator.test.ts index 187d1bb..7ce3fdf 100644 --- a/tests/markdown/generator.test.ts +++ b/tests/markdown/generator.test.ts @@ -52,6 +52,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/example.ts', numberOfLines: 10 }]), + { lineCoverage: 42.24, branchCoverage: undefined }, {}, logger, ) @@ -98,6 +99,7 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/src/core/utils.cs', numberOfLines: 15 }, { resolvedPath: '/repo/tests/test_utils.cs', numberOfLines: 10 }, ]), + { lineCoverage: 42.24, branchCoverage: undefined }, {}, logger, ) @@ -132,6 +134,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/gaps.rs', numberOfLines: 50 }]), + { lineCoverage: 42.24, branchCoverage: undefined }, {}, logger, ) @@ -148,7 +151,13 @@ describe('generateMarkdown', () => { }, ] - const markdown = generateMarkdown(packages, createFakeFileContents([]), {}, logger) + const markdown = generateMarkdown( + packages, + createFakeFileContents([]), + { lineCoverage: 42.24, branchCoverage: undefined }, + {}, + logger, + ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/empty-package.snap.md') }) @@ -188,6 +197,7 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/src/file1.py', numberOfLines: 3 }, { resolvedPath: '/repo/src/file2.py', numberOfLines: 3 }, ]), + { lineCoverage: 10, branchCoverage: 10 }, {}, logger, ) @@ -229,6 +239,7 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/test.rs', numberOfLines: 2 }, { resolvedPath: '/repo/test.tsx', numberOfLines: 3 }, ]), + { lineCoverage: 20, branchCoverage: 20 }, {}, logger, ) @@ -261,6 +272,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 10 }]), + { lineCoverage: 30, branchCoverage: 30 }, {}, logger, ) @@ -291,6 +303,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 10 }]), + { lineCoverage: 40, branchCoverage: 40 }, {}, logger, ) @@ -323,6 +336,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 5 }]), + { lineCoverage: 50, branchCoverage: 50 }, {}, logger, ) @@ -357,6 +371,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 15 }]), + { lineCoverage: 60, branchCoverage: 60 }, {}, logger, ) @@ -388,6 +403,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 6 }]), + { lineCoverage: 70, branchCoverage: 70 }, {}, logger, ) @@ -419,6 +435,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 5 }]), + { lineCoverage: 80, branchCoverage: 80 }, {}, logger, ) @@ -449,7 +466,7 @@ describe('generateMarkdown', () => { // Empty map - file not found on disk const fileContents = new Map() - const markdown = generateMarkdown(packages, fileContents, {}, logger) + const markdown = generateMarkdown(packages, fileContents, { lineCoverage: 42.24, branchCoverage: 50 }, {}, logger) await expect(markdown).toMatchFileSnapshot('./__snapshots__/file-not-found.snap.md') }) @@ -477,6 +494,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/src/short.ts', numberOfLines: 3 }]), + { lineCoverage: 90, branchCoverage: 90 }, {}, logger, ) @@ -505,6 +523,7 @@ describe('generateMarkdown', () => { generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/a.ts', numberOfLines: 1 }]), + { lineCoverage: 100, branchCoverage: 100 }, { maxCharacters: 100, }, @@ -531,6 +550,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/small.ts', numberOfLines: 2 }]), + { lineCoverage: 42.24, branchCoverage: 50 }, { maxCharacters: 2000, }, @@ -575,6 +595,7 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/file2.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file3.ts', numberOfLines: 2 }, ]), + { lineCoverage: 42.24, branchCoverage: 50 }, { maxCharacters: 1000 }, logger, ) @@ -631,6 +652,7 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/pkg2/b.ts', numberOfLines: 2 }, { resolvedPath: '/repo/pkg3/c.ts', numberOfLines: 2 }, ]), + { lineCoverage: 42.24, branchCoverage: 50 }, { maxCharacters: 900 }, logger, ) @@ -682,6 +704,7 @@ describe('generateMarkdown', () => { { resolvedPath: '/repo/file3.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file4.ts', numberOfLines: 2 }, ]), + { lineCoverage: 42.24, branchCoverage: 50 }, { maxCharacters: 1000 }, logger, ) @@ -713,6 +736,7 @@ describe('generateMarkdown', () => { const markdown = generateMarkdown( packages, createFakeFileContents([{ resolvedPath: '/repo/test.ts', numberOfLines: 2 }]), + { lineCoverage: 42.24, branchCoverage: 50 }, {}, logger, ) diff --git a/tests/resources/integration/glob-filter/coverage.xml b/tests/resources/integration/glob-filter/coverage.xml index 46ec905..d495728 100644 --- a/tests/resources/integration/glob-filter/coverage.xml +++ b/tests/resources/integration/glob-filter/coverage.xml @@ -5,14 +5,18 @@ - - + + - - + + + + + + diff --git a/tests/resources/integration/glob-filter/src/exclude/ignored.ts b/tests/resources/integration/glob-filter/src/exclude/ignored.ts index e2a6853..16283c8 100644 --- a/tests/resources/integration/glob-filter/src/exclude/ignored.ts +++ b/tests/resources/integration/glob-filter/src/exclude/ignored.ts @@ -1,3 +1,7 @@ export function ignored(): string { return 'I am ignored' } + +// filler +// filler +// filler From 5eed09b6287238de6f4e6f6aabddcd0d313a2582 Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 18:27:46 +0100 Subject: [PATCH 11/13] chore: Address review feedback --- src/cli.ts | 8 +++--- src/github.ts | 52 ++++++++++++++++++++++----------------- src/markdown/generator.ts | 2 +- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 726a943..792d4dd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -121,7 +121,7 @@ async function main(): Promise { process.exit(1) } - const { markdown, lineCoverage, branchCoverage } = result + const { markdown, lineCoverage, branchCoverage, lineCoveragePr, branchCoveragePr } = result // Output results if (values.output) { @@ -133,8 +133,10 @@ async function main(): Promise { // Print metrics summary console.log('\n--- Coverage Summary ---') - console.log(`Line Coverage: ${lineCoverage.toFixed(2)}%`) - console.log(`Branch Coverage: ${branchCoverage.toFixed(2)}%`) + console.log(`Line Coverage: ${lineCoverage.toFixed(2)}%`) + console.log(`Branch Coverage: ${branchCoverage?.toFixed(2)}%`) + console.log(`Line Coverage PR: ${lineCoveragePr.toFixed(2)}%`) + console.log(`Branch Coverage PR: ${branchCoveragePr?.toFixed(2)}%`) } main().catch((error) => { diff --git a/src/github.ts b/src/github.ts index 254d669..c74d5b4 100644 --- a/src/github.ts +++ b/src/github.ts @@ -41,38 +41,44 @@ const getEnv = (name: string): string => { * Only returns PRs that are still open (not merged or closed). */ export async function findPullRequestNumber(octokit: Octokit, context: Context): Promise { - // Check if we're already in a pull_request event + // Fast path: pull_request event payload if ('pull_request' in context.payload) { - const prPayload = context.payload as { pull_request?: { number: number; state?: string } } - if (prPayload.pull_request?.number) { - // For pull_request events, check if the PR is still open - // If it's closed/merged, we shouldn't update comments - if (prPayload.pull_request.state !== 'open') { - return null - } - return prPayload.pull_request.number + const pr = (context.payload as { pull_request?: { number: number; state?: string } }).pull_request + if (pr?.number) { + if (pr.state !== 'open') return null + return pr.number } } // Otherwise, find PRs associated with this commit - const { data: pulls } = await octokit.rest.repos.listPullRequestsAssociatedWithCommit({ - owner: context.repo.owner, - repo: context.repo.repo, - commit_sha: context.sha, - }) + let pulls + try { + const res = await octokit.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.sha, + }) + pulls = res.data + } catch { + return null + } - // Filter to only open PRs and return the first one - // If state is not available in the response, fetch PR details to check for (const pull of pulls) { const prNumber = pull.number if (!prNumber) continue - if ('state' in pull) { - // Skip closed/merged PRs immediately - if (pull.state !== 'open') { - continue - } - return prNumber - } + + // Prefer using state if present + if (pull.state === 'open') return prNumber + if (pull.state && pull.state !== 'open') continue + + // Fallback: fetch PR details if state is missing/unknown + const { data: pr } = await octokit.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }) + + if (pr.state === 'open') return prNumber } return null diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index e6fe896..169bb3e 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -93,7 +93,7 @@ type FileCoverageClassification = { * @param fileContents - Map of resolved (absolute) path to array of line contents * @param overallMetrics - The overall percentage metrics * @param options - Optional configuration for Markdown generation - * @param logger - Optional logger for emitting log messages + * @param logger - Logger for emitting log messages * @returns Markdown string representation * @throws Error if maxCharacters is below MINIMUM_CHARACTERS */ From 1e0a17c62333e43bd59b81eb92593db6e783f4f1 Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 18:51:36 +0100 Subject: [PATCH 12/13] chore: Fix some of the code fixers --- src/action.ts | 4 +- src/cli.ts | 6 +-- src/core/index.ts | 2 +- src/core/process-coverage.ts | 14 +++---- src/filter/filter.ts | 2 +- src/github.ts | 2 +- src/index.ts | 2 +- src/markdown/generator.ts | 19 ++++----- src/markdown/index.ts | 2 +- .../core/process-coverage.integration.test.ts | 18 ++++---- tests/filter/filter.test.ts | 42 ++++++++++--------- tests/markdown/generator.test.ts | 2 +- 12 files changed, 59 insertions(+), 56 deletions(-) diff --git a/src/action.ts b/src/action.ts index b1a2ce4..485df1c 100644 --- a/src/action.ts +++ b/src/action.ts @@ -1,7 +1,7 @@ import * as core from '@actions/core' import type { Octokit } from '@octokit/action' -import { processCoverage, type Logger } from './core/index.js' -import { findPullRequestNumber, getComparisonShas, postComment, type Context } from './github.js' +import { type Logger, processCoverage } from './core/index.js' +import { type Context, findPullRequestNumber, getComparisonShas, postComment } from './github.js' export type Inputs = { files: string diff --git a/src/cli.ts b/src/cli.ts index 792d4dd..f31da00 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,10 +1,10 @@ #!/usr/bin/env node -import * as fs from 'node:fs/promises' import { execFile } from 'node:child_process' +import * as fs from 'node:fs/promises' import { parseArgs, promisify } from 'node:util' import { processCoverage } from './core/index.js' -import { isValidGitRef } from './filter/changed-lines.js' import { createCliLogger } from './core/process-coverage.js' +import { isValidGitRef } from './filter/changed-lines.js' const execFileAsync = promisify(execFile) @@ -128,7 +128,7 @@ async function main(): Promise { await fs.writeFile(values.output, markdown, 'utf-8') console.log(`\nReport written to: ${values.output}`) } else { - console.log('\n' + markdown) + console.log(`\n${markdown}`) } // Print metrics summary diff --git a/src/core/index.ts b/src/core/index.ts index 8adcafc..d357f6f 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,6 +1,6 @@ export { - processCoverage, type Logger, type ProcessCoverageInputs, type ProcessCoverageResult, + processCoverage, } from './process-coverage.js' diff --git a/src/core/process-coverage.ts b/src/core/process-coverage.ts index 8d99b9e..e53fea4 100644 --- a/src/core/process-coverage.ts +++ b/src/core/process-coverage.ts @@ -2,16 +2,16 @@ import * as fs from 'node:fs/promises' import * as path from 'node:path' import { CoberturaCoverageParser, - CoverageMetrics, + type CoverageMetrics, type CoverageReport, - FileCoverage, - LineCoverage, - LineCoverageState, - PackageCoverage, + type FileCoverage, + type LineCoverage, + type LineCoverageState, + type PackageCoverage, } from '../coverage/index.js' -import { filterByChangedLines, filterByGlob, getChangedLinesFromGit, type ChangedLinesMap } from '../filter/index.js' +import type { PercentageCoverageMetrics } from '../coverage/model.js' +import { type ChangedLinesMap, filterByChangedLines, filterByGlob, getChangedLinesFromGit } from '../filter/index.js' import { generateMarkdown } from '../markdown/index.js' -import { PercentageCoverageMetrics } from '../coverage/model.js' /** * Logger interface for dependency injection. diff --git a/src/filter/filter.ts b/src/filter/filter.ts index a437a7b..d87923d 100644 --- a/src/filter/filter.ts +++ b/src/filter/filter.ts @@ -1,6 +1,6 @@ import * as path from 'node:path' -import { FileCoverage, PackageCoverage } from '../coverage/model.js' import type { Logger } from '../core/index.js' +import type { FileCoverage, PackageCoverage } from '../coverage/model.js' import type { ChangedLinesMap } from './model.js' /** diff --git a/src/github.ts b/src/github.ts index c74d5b4..739f8ea 100644 --- a/src/github.ts +++ b/src/github.ts @@ -51,7 +51,7 @@ export async function findPullRequestNumber(octokit: Octokit, context: Context): } // Otherwise, find PRs associated with this commit - let pulls + let pulls: { number: number; state?: string }[] try { const res = await octokit.rest.repos.listPullRequestsAssociatedWithCommit({ owner: context.repo.owner, diff --git a/src/index.ts b/src/index.ts index 79ab5e3..773a0cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import * as core from '@actions/core' -import { getContext, getOctokit } from './github.js' import { run } from './action.js' +import { getContext, getOctokit } from './github.js' try { await run( diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index 169bb3e..3e0626b 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -1,11 +1,11 @@ -import { +import type { Logger } from '../core/index.js' +import type { CoverageMetrics, FileCoverage, LineCoverage, PackageCoverage, PercentageCoverageMetrics, } from '../coverage/model.js' -import type { Logger } from '../core/index.js' // ============================================================================= // CONSTANTS @@ -222,7 +222,7 @@ function formatRatio(covered: number, total: number): string { function formatPercent(value: number): string { const formatted = value.toFixed(2) // Remove trailing zeros and decimal point if not needed - return formatted.replace(/\.?0+$/, '') + '%' + return `${formatted.replace(/\.?0+$/, '')}%` } /** @@ -392,11 +392,10 @@ function buildMarkdownWithinLimitFileLevel( logger: Logger, ): string { // Legend with separator is always appended at the end - const legendWithSeparator = '\n---\n\n' + legend + const legendWithSeparator = `\n---\n\n${legend}` // Build the fixed header - const header = - '## Repo Coverage\n' + badges + '\n' + '\n---\n\n' + '## PR Coverage\n' + renderPRSummaryLine(prMetrics) + '\n\n' + const header = `## Repo Coverage\n${badges}\n\n---\n\n## PR Coverage\n${renderPRSummaryLine(prMetrics)}\n\n` // Check if even the header + legend exceeds the limit if (header.length + legendWithSeparator.length > maxCharacters) { @@ -416,7 +415,7 @@ function buildMarkdownWithinLimitFileLevel( const pkg = packageSections[p]! // Try to include package header - if (!budget.tryAppend(pkg.header + '\n')) { + if (!budget.tryAppend(`${pkg.header}\n`)) { // Can't fit this package header - omit this and all remaining packages omittedPackages += packageSections.length - p for (let i = p; i < packageSections.length; i++) { @@ -428,7 +427,7 @@ function buildMarkdownWithinLimitFileLevel( // Process files in this package for (let f = 0; f < pkg.files.length; f++) { - const fileContent = pkg.files[f]!.content + const fileContent = pkg.files[f]?.content const isLastFileOverall = f === pkg.files.length - 1 && p === packageSections.length - 1 const spacing = isLastFileOverall ? '\n' : '\n\n' @@ -448,7 +447,7 @@ function buildMarkdownWithinLimitFileLevel( // Add truncation notice if needed and it fits if (truncated && (omittedFiles > 0 || omittedPackages > 0)) { const notice = renderTruncationNotice(omittedFiles, omittedPackages) - budget.tryAppend(notice + '\n') + budget.tryAppend(`${notice}\n`) if (omittedFiles > 0) { logger.warning( @@ -574,7 +573,7 @@ function renderFileSection( lines.push('') const extension = getFileExtension(file.filename) - lines.push('```' + extension) + lines.push(`\`\`\`${extension}`) lines.push(renderAnnotatedLines(file.lines, fileLines, numberOfSurroundingLines)) lines.push('```') lines.push('
') diff --git a/src/markdown/index.ts b/src/markdown/index.ts index a30590a..93b71fc 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -1,6 +1,6 @@ export { + DEFAULT_MAX_CHARACTERS, generateMarkdown, type MarkdownOptions, - DEFAULT_MAX_CHARACTERS, MINIMUM_CHARACTERS, } from './generator.js' diff --git a/tests/core/process-coverage.integration.test.ts b/tests/core/process-coverage.integration.test.ts index 931f29f..9b2b982 100644 --- a/tests/core/process-coverage.integration.test.ts +++ b/tests/core/process-coverage.integration.test.ts @@ -1,6 +1,6 @@ import * as path from 'node:path' import { describe, expect, it } from 'vitest' -import { processCoverage, createCliLogger } from '../../src/core/process-coverage.js' +import { createCliLogger, processCoverage } from '../../src/core/process-coverage.js' const FIXTURES_DIR = path.join(__dirname, '../resources/integration') @@ -18,7 +18,7 @@ describe('processCoverage integration', () => { ) expect(result).not.toBeNull() - await expect(result!.markdown).toMatchFileSnapshot('./__snapshots__/basic-coverage.snap.md') + await expect(result?.markdown).toMatchFileSnapshot('./__snapshots__/basic-coverage.snap.md') }) it('generates markdown for multiple packages', async () => { @@ -34,7 +34,7 @@ describe('processCoverage integration', () => { ) expect(result).not.toBeNull() - await expect(result!.markdown).toMatchFileSnapshot('./__snapshots__/multiple-packages.snap.md') + await expect(result?.markdown).toMatchFileSnapshot('./__snapshots__/multiple-packages.snap.md') }) it('filters files by glob pattern', async () => { @@ -51,11 +51,11 @@ describe('processCoverage integration', () => { expect(result).not.toBeNull() // Outputs must be computed from the already-filtered data (same as markdown) - expect(result!.lineCoverage).toBeCloseTo(50, 5) - expect(result!.branchCoverage).toBeCloseTo(75, 5) - expect(result!.markdown).toContain('wanted.ts') - expect(result!.markdown).not.toContain('ignored.ts') - await expect(result!.markdown).toMatchFileSnapshot('./__snapshots__/glob-filter.snap.md') + expect(result?.lineCoverage).toBeCloseTo(50, 5) + expect(result?.branchCoverage).toBeCloseTo(75, 5) + expect(result?.markdown).toContain('wanted.ts') + expect(result?.markdown).not.toContain('ignored.ts') + await expect(result?.markdown).toMatchFileSnapshot('./__snapshots__/glob-filter.snap.md') }) it('merges multiple coverage files', async () => { @@ -71,7 +71,7 @@ describe('processCoverage integration', () => { ) expect(result).not.toBeNull() - await expect(result!.markdown).toMatchFileSnapshot('./__snapshots__/merge-reports.snap.md') + await expect(result?.markdown).toMatchFileSnapshot('./__snapshots__/merge-reports.snap.md') }) it('returns null when no files match pattern', async () => { diff --git a/tests/filter/filter.test.ts b/tests/filter/filter.test.ts index 8c87915..b2af7ba 100644 --- a/tests/filter/filter.test.ts +++ b/tests/filter/filter.test.ts @@ -1,6 +1,7 @@ +import assert from 'node:assert' import { describe, expect, it } from 'vitest' +import { createCliLogger, type Logger } from '../../src/core/process-coverage.js' import type { PackageCoverage } from '../../src/coverage/model.js' -import { createCliLogger, Logger } from '../../src/core/process-coverage.js' import { filterByChangedLines, filterByGlob } from '../../src/filter/index.js' import type { ChangedLinesMap } from '../../src/filter/model.js' @@ -54,28 +55,28 @@ describe('filterByGlob', () => { it('matches all files with ** pattern', () => { const result = filterByGlob(sampleReport, '**', mockLogger) expect(result).toHaveLength(2) - expect(result[0]!.files).toHaveLength(3) - expect(result[1]!.files).toHaveLength(1) + expect(result[0]?.files).toHaveLength(3) + expect(result[1]?.files).toHaveLength(1) }) it('filters files by directory pattern', () => { const result = filterByGlob(sampleReport, '**/src/**', mockLogger) expect(result).toHaveLength(1) - expect(result[0]!.name).toBe('Package1') - expect(result[0]!.files).toHaveLength(2) + expect(result[0]?.name).toBe('Package1') + expect(result[0]?.files).toHaveLength(2) }) it('filters files by extension pattern', () => { const result = filterByGlob(sampleReport, '**/*.tsx', mockLogger) expect(result).toHaveLength(1) - expect(result[0]!.files).toHaveLength(1) - expect(result[0]!.files[0]!.filename).toBe('src/components/Button.tsx') + expect(result[0]?.files).toHaveLength(1) + expect(result[0]?.files[0]?.filename).toBe('src/components/Button.tsx') }) it('removes empty packages after filtering', () => { const result = filterByGlob(sampleReport, '**/lib/**', mockLogger) expect(result).toHaveLength(1) - expect(result[0]!.name).toBe('Package2') + expect(result[0]?.name).toBe('Package2') }) it('returns empty packages array when nothing matches', () => { @@ -86,8 +87,8 @@ describe('filterByGlob', () => { it('supports specific file matching', () => { const result = filterByGlob(sampleReport, '**/helper.ts', mockLogger) expect(result).toHaveLength(1) - expect(result[0]!.files).toHaveLength(1) - expect(result[0]!.files[0]!.filename).toBe('src/utils/helper.ts') + expect(result[0]?.files).toHaveLength(1) + expect(result[0]?.files[0]?.filename).toBe('src/utils/helper.ts') }) }) @@ -128,9 +129,10 @@ describe('filterByChangedLines', () => { const result = filterByChangedLines(sampleReport, changedLines, mockLogger) expect(result).toHaveLength(1) - expect(result[0]!.files).toHaveLength(1) + expect(result[0]?.files).toHaveLength(1) - const file = result[0]!.files[0]! + const file = result[0]?.files[0] + assert(file) expect(file.filename).toBe('src/file1.ts') expect(file.lines).toHaveLength(2) expect(file.lines.map((l) => l.lineNumber)).toEqual([2, 4]) @@ -141,7 +143,8 @@ describe('filterByChangedLines', () => { const result = filterByChangedLines(sampleReport, changedLines, mockLogger) - const file = result[0]!.files[0]! + const file = result[0]?.files[0] + assert(file) expect(file.lineMetrics.covered).toBe(2) // lines 1 and 3 are covered expect(file.lineMetrics.total).toBe(3) // 3 lines total }) @@ -151,7 +154,8 @@ describe('filterByChangedLines', () => { const result = filterByChangedLines(sampleReport, changedLines, mockLogger) - const file = result[0]!.files[0]! + const file = result[0]?.files[0] + assert(file) expect(file.branchMetrics).toEqual({ covered: 1, total: 2 }) }) @@ -160,8 +164,8 @@ describe('filterByChangedLines', () => { const result = filterByChangedLines(sampleReport, changedLines, mockLogger) - expect(result[0]!.files).toHaveLength(1) - expect(result[0]!.files[0]!.filename).toBe('src/file1.ts') + expect(result[0]?.files).toHaveLength(1) + expect(result[0]?.files[0]?.filename).toBe('src/file1.ts') }) it('excludes files with empty changed lines set', () => { @@ -172,7 +176,7 @@ describe('filterByChangedLines', () => { const result = filterByChangedLines(sampleReport, changedLines, mockLogger) - expect(result[0]!.files).toHaveLength(1) + expect(result[0]?.files).toHaveLength(1) }) it('removes packages with no files after filtering', () => { @@ -215,7 +219,7 @@ describe('filterByChangedLines', () => { // File without resolvedPath should be included with all its lines expect(result).toHaveLength(1) - expect(result[0]!.files).toHaveLength(1) - expect(result[0]!.files[0]!.lines).toHaveLength(2) + expect(result[0]?.files).toHaveLength(1) + expect(result[0]?.files[0]?.lines).toHaveLength(2) }) }) diff --git a/tests/markdown/generator.test.ts b/tests/markdown/generator.test.ts index 7ce3fdf..7b837bc 100644 --- a/tests/markdown/generator.test.ts +++ b/tests/markdown/generator.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' +import { createCliLogger } from '../../src/core/process-coverage.js' import type { PackageCoverage } from '../../src/coverage/model.js' import { generateMarkdown, MINIMUM_CHARACTERS } from '../../src/markdown/index.js' -import { createCliLogger } from '../../src/core/process-coverage.js' type FakeFileInfo = { resolvedPath: string From 039ea1457460729d9b12b78d08fd6e8523d1213e Mon Sep 17 00:00:00 2001 From: Ole Rosskamp Date: Sun, 4 Jan 2026 18:54:43 +0100 Subject: [PATCH 13/13] chore: Address some more review issues --- action.yaml | 4 ++-- src/core/process-coverage.ts | 2 +- src/markdown/generator.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/action.yaml b/action.yaml index b8ca620..f7b0669 100644 --- a/action.yaml +++ b/action.yaml @@ -32,9 +32,9 @@ outputs: branch-coverage: description: The overall branch coverage percentage pr-line-coverage: - description: The coverage of the changes of this PR. Equal to the overall line coverage, if not executed on a PR + description: The line coverage of the changes in this PR. Equal to the overall line coverage, if not executed on a PR pr-branch-coverage: - description: The overall branch coverage percentage. Equal to the overall line coverage, if not executed on a PR + description: The branch coverage of the changes in this PR. Equal to the overall line coverage, if not executed on a PR runs: using: 'node24' diff --git a/src/core/process-coverage.ts b/src/core/process-coverage.ts index e53fea4..d292ffa 100644 --- a/src/core/process-coverage.ts +++ b/src/core/process-coverage.ts @@ -160,7 +160,7 @@ export async function processCoverage( `Calculated overall metrics (LineCoverage: ${overallMetrics.lineCoverage}, BranchCoverage: ${overallMetrics.branchCoverage})`, ) logger.info( - `Calculated overall metrics (LineCoverage: ${prMetrics.lineCoverage}, BranchCoverage: ${prMetrics.branchCoverage})`, + `Calculated PR metrics (LineCoverage: ${prMetrics.lineCoverage}, BranchCoverage: ${prMetrics.branchCoverage})`, ) // Generate Markdown from filtered report diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index 3e0626b..795ff7b 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -520,7 +520,7 @@ function renderBadge(label: string, percent: number | undefined): string { let value: string let color: string - if (!percent) { + if (percent === undefined) { value = 'n/a' color = 'brightgreen' } else {