Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import require$$6$1 from 'string_decoder';
import require$$0$f from 'diagnostics_channel';
import require$$2$6 from 'child_process';
import require$$6$2 from 'timers';
import require$$0$g, { createHash } from 'node:crypto';
import * as fs$1 from 'node:fs/promises';
import fs__default$1 from 'node:fs/promises';
import * as os from 'node:os';
Expand All @@ -40,7 +41,6 @@ import require$$1$8 from 'node:http';
import require$$2$8 from 'node:https';
import require$$3$2 from 'node:zlib';
import require$$1$9 from 'tty';
import require$$0$g from 'node:crypto';
import require$$2$9 from 'node:buffer';
import require$$1$a from 'fs/promises';
import require$$0$h from 'constants';
Expand Down Expand Up @@ -33143,7 +33143,8 @@ class ReportFormatter {
lines.push("| File | Patch % | Lines |");
lines.push("|------|---------|-------|");
for (const file of patchFilesWithMissing) {
const fileName = this.getFileName(file.path);
const filePath = this.normalizeFilePath(file.path);
const fileCell = this.formatFileCell(filePath, options.githubContext);
const missingCount = file.missedLines.length;
const partialCount = file.partialLines.length;
let linesText = "";
Expand All @@ -33156,7 +33157,7 @@ class ReportFormatter {
else if (partialCount > 0) {
linesText = `:warning: ${partialCount} partials`;
}
lines.push(`| \`${fileName}\` | ${file.percentage.toFixed(2)}% | ${linesText} |`);
lines.push(`| ${fileCell} | ${file.percentage.toFixed(2)}% | ${linesText} |`);
}
lines.push("");
lines.push("</details>");
Expand All @@ -33174,7 +33175,12 @@ class ReportFormatter {
lines.push("| File | Coverage % | Lines |");
lines.push("|------|------------|-------|");
for (const file of filesWithMissing) {
const fileName = this.getFileName(file.path);
const filePath = this.normalizeFilePath(file.path);
// Don't generate links for project-wide paths since they may be
// absolute (from coverage parsers) and won't produce valid GitHub
// diff anchors. Links are only reliable for patch breakdown paths
// which come directly from the git diff.
const fileCell = this.formatFileCell(filePath);
const missingCount = file.missingLines?.length || 0;
const partialCount = file.partialLines?.length || 0;
let linesText = "";
Expand All @@ -33187,7 +33193,7 @@ class ReportFormatter {
else if (partialCount > 0) {
linesText = `:warning: ${partialCount} partials`;
}
lines.push(`| \`${fileName}\` | ${file.lineRate.toFixed(2)}% | ${linesText} |`);
lines.push(`| ${fileCell} | ${file.lineRate.toFixed(2)}% | ${linesText} |`);
}
lines.push("");
lines.push("</details>");
Expand Down Expand Up @@ -33320,10 +33326,23 @@ class ReportFormatter {
lines.push("");
}
/**
* Get just the filename from a path
* Format a file path as a markdown table cell, optionally linked to the
* GitHub PR diff view when context is available.
*/
getFileName(path) {
return path.split("/").pop() || path;
formatFileCell(filePath, context) {
if (context) {
const url = this.buildPrDiffUrl(filePath, context);
return `[${filePath}](${url})`;
}
return `\`${filePath}\``;
}
/**
* Build a URL to a file in the GitHub PR diff view.
* GitHub anchors each file diff with #diff-{sha256hex(filepath)}.
*/
buildPrDiffUrl(filePath, context) {
const hash = createHash("sha256").update(filePath).digest("hex");
return `${context.serverUrl}/${context.owner}/${context.repo}/pull/${context.prNumber}/files#diff-${hash}`;
}
/**
* Format delta value with sign only (no emoji)
Expand Down Expand Up @@ -231452,13 +231471,24 @@ async function run() {
const effectiveFilesMode = coverageConfig.config.files !== "none" && !githubClient.isPullRequest()
? "all"
: coverageConfig.config.files;
// Build GitHub context for linking files to PR diff view
const prNumber = githubClient.getPullRequestNumber();
const githubContext = prNumber
? {
owner: contextInfo.owner,
repo: contextInfo.repo,
prNumber,
serverUrl: process.env.GITHUB_SERVER_URL || "https://github.com",
}
: undefined;
const reportOptions = {
filesMode: effectiveFilesMode,
changedFiles: effectiveFilesMode === "changed"
? patchCoverage?.changedFiles || []
: undefined,
patchTarget: patchTargetForFormatter,
patchFileBreakdown: patchCoverage?.fileBreakdown,
githubContext,
};
const summaryReportBody = formatter.formatReport(aggregatedTestResults || undefined, aggregatedCoverageResults || undefined, reportOptions);
// Write Job Summary (always)
Expand Down
200 changes: 178 additions & 22 deletions src/__tests__/report-formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,9 +351,10 @@ describe("ReportFormatter", () => {
);

expect(comment).toContain("Files with missing lines (3)");
expect(comment).toContain("`changed-file.ts`");
expect(comment).toContain("`unchanged-file.ts`");
expect(comment).toContain("`dot-prefixed.ts`");
// Full paths should be shown (not just filenames)
expect(comment).toContain("`src/changed-file.ts`");
expect(comment).toContain("`src/unchanged-file.ts`");
expect(comment).toContain("`src/dot-prefixed.ts`");
});

it("should only show changed files when filesMode is changed", () => {
Expand All @@ -367,9 +368,9 @@ describe("ReportFormatter", () => {
);

expect(comment).toContain("Files with missing lines (2)");
expect(comment).toContain("`changed-file.ts`");
expect(comment).toContain("`dot-prefixed.ts`");
expect(comment).not.toContain("`unchanged-file.ts`");
expect(comment).toContain("src/changed-file.ts");
expect(comment).toContain("src/dot-prefixed.ts");
expect(comment).not.toContain("unchanged-file.ts");
});

it("should match absolute coverage paths when filesMode is changed", () => {
Expand All @@ -394,8 +395,11 @@ describe("ReportFormatter", () => {
);

expect(comment).toContain("Files with missing lines (1)");
expect(comment).toContain("`changed-file.ts`");
expect(comment).not.toContain("`unchanged-file.ts`");
// Absolute paths are normalized — leading / and segments before repo root are stripped
expect(comment).toContain(
"home/runner/work/repo/repo/src/changed-file.ts",
);
expect(comment).not.toContain("unchanged-file.ts");
});

it("should match when diff paths are absolute and coverage paths are relative", () => {
Expand All @@ -409,8 +413,8 @@ describe("ReportFormatter", () => {
);

expect(comment).toContain("Files with missing lines (1)");
expect(comment).toContain("`changed-file.ts`");
expect(comment).not.toContain("`unchanged-file.ts`");
expect(comment).toContain("src/changed-file.ts");
expect(comment).not.toContain("unchanged-file.ts");
});

it("should only show changed files by default (filesMode defaults to changed)", () => {
Expand All @@ -423,9 +427,9 @@ describe("ReportFormatter", () => {
);

expect(comment).toContain("Files with missing lines (1)");
expect(comment).toContain("`changed-file.ts`");
expect(comment).not.toContain("`unchanged-file.ts`");
expect(comment).not.toContain("`dot-prefixed.ts`");
expect(comment).toContain("src/changed-file.ts");
expect(comment).not.toContain("unchanged-file.ts");
expect(comment).not.toContain("dot-prefixed.ts");
});

it("should hide file table when filesMode is none", () => {
Expand All @@ -438,7 +442,7 @@ describe("ReportFormatter", () => {
);

expect(comment).not.toContain("Files with missing lines");
expect(comment).not.toContain("`changed-file.ts`");
expect(comment).not.toContain("changed-file.ts");
});

it("should hide file table when filesMode is changed and changedFiles is empty", () => {
Expand Down Expand Up @@ -502,12 +506,13 @@ describe("ReportFormatter", () => {

// Should show only files with missing or partial lines in the patch
expect(comment).toContain("Files with missing lines (2)");
expect(comment).toContain("`args.rs`");
// Full paths should be shown
expect(comment).toContain("`src/args.rs`");
expect(comment).toContain("2 Missing");
expect(comment).toContain("`bytes.rs`");
expect(comment).toContain("`src/types/bytes.rs`");
expect(comment).toContain("1 partials");
// clean-file.rs has no missing or partial lines in the patch, should be excluded
expect(comment).not.toContain("`clean-file.rs`");
expect(comment).not.toContain("clean-file.rs");
});

it("should use patch percentage instead of project lineRate", () => {
Expand Down Expand Up @@ -607,8 +612,8 @@ describe("ReportFormatter", () => {
},
);

const manyIdx = comment.indexOf("`many-missing.rs`");
const fewIdx = comment.indexOf("`few-missing.rs`");
const manyIdx = comment.indexOf("src/many-missing.rs");
const fewIdx = comment.indexOf("src/few-missing.rs");
// many-missing.rs (4 total) should appear before few-missing.rs (1 total)
expect(manyIdx).toBeLessThan(fewIdx);
});
Expand All @@ -625,9 +630,9 @@ describe("ReportFormatter", () => {
);

expect(comment).toContain("Files with missing lines (3)");
expect(comment).toContain("`changed-file.ts`");
expect(comment).toContain("`unchanged-file.ts`");
expect(comment).toContain("`dot-prefixed.ts`");
expect(comment).toContain("`src/changed-file.ts`");
expect(comment).toContain("`src/unchanged-file.ts`");
expect(comment).toContain("`src/dot-prefixed.ts`");
});

it("should still show project uncovered lines in summary when patchFileBreakdown is provided", () => {
Expand All @@ -643,5 +648,156 @@ describe("ReportFormatter", () => {
expect(comment).toContain("Project has **20** uncovered lines.");
});
});

describe("File links with githubContext", () => {
const githubContext = {
owner: "pydantic",
repo: "monty",
prNumber: 290,
serverUrl: "https://github.com",
};

it("should render file names as links to PR diff when githubContext is provided", () => {
const patchBreakdown: PatchFileCoverage[] = [
{
path: "src/changed-file.ts",
coveredLines: [1, 2],
missedLines: [3],
partialLines: [],
percentage: 66.67,
},
{
path: "src/unchanged-file.ts",
coveredLines: [1],
missedLines: [2],
partialLines: [],
percentage: 50,
},
];

const comment = formatter.formatReport(
undefined,
coverageWithMissingFiles,
{
patchFileBreakdown: patchBreakdown,
githubContext,
},
);

// Should contain markdown links, not backtick-formatted names
expect(comment).toContain(
"[src/changed-file.ts](https://github.com/pydantic/monty/pull/290/files#diff-",
);
expect(comment).toContain(
"[src/unchanged-file.ts](https://github.com/pydantic/monty/pull/290/files#diff-",
);
// Should NOT contain backtick-only file references in the table
expect(comment).not.toContain("`src/changed-file.ts`");
});

it("should render patch breakdown files as links when githubContext is provided", () => {
const patchBreakdown: PatchFileCoverage[] = [
{
path: "crates/monty/src/types/mod.rs",
coveredLines: [1, 2],
missedLines: [3],
partialLines: [],
percentage: 66.67,
},
{
path: "crates/monty/src/builtins/mod.rs",
coveredLines: [1],
missedLines: [2],
partialLines: [],
percentage: 50,
},
];

const comment = formatter.formatReport(
undefined,
coverageWithMissingFiles,
{
patchFileBreakdown: patchBreakdown,
githubContext,
},
);

// Both files should show full paths (disambiguating the two mod.rs files)
expect(comment).toContain("crates/monty/src/types/mod.rs");
expect(comment).toContain("crates/monty/src/builtins/mod.rs");
// Both should be links
expect(comment).toContain(
"[crates/monty/src/types/mod.rs](https://github.com/pydantic/monty/pull/290/files#diff-",
);
expect(comment).toContain(
"[crates/monty/src/builtins/mod.rs](https://github.com/pydantic/monty/pull/290/files#diff-",
);
});

it("should use plain backtick paths without githubContext", () => {
const comment = formatter.formatReport(
undefined,
coverageWithMissingFiles,
{
filesMode: "all",
// No githubContext
},
);

// Should use backtick formatting with full paths
expect(comment).toContain("`src/changed-file.ts`");
expect(comment).not.toContain("[src/changed-file.ts]");
});

it("should use plain backtick paths in fallback even with githubContext", () => {
// When there's no patchFileBreakdown (fallback path), coverage parser
// paths may be absolute and won't produce valid GitHub diff anchors.
const comment = formatter.formatReport(
undefined,
coverageWithMissingFiles,
{
filesMode: "all",
githubContext,
// No patchFileBreakdown — hits fallback path
},
);

// Fallback path should NOT generate links
expect(comment).toContain("`src/changed-file.ts`");
expect(comment).not.toContain("[src/changed-file.ts]");
});

it("should support custom server URL for GHES", () => {
const ghesContext = {
owner: "org",
repo: "repo",
prNumber: 42,
serverUrl: "https://github.example.com",
};

const patchBreakdown: PatchFileCoverage[] = [
{
path: "src/changed-file.ts",
coveredLines: [1, 2],
missedLines: [3],
partialLines: [],
percentage: 66.67,
},
];

const comment = formatter.formatReport(
undefined,
coverageWithMissingFiles,
{
patchFileBreakdown: patchBreakdown,
githubContext: ghesContext,
},
);

expect(comment).toContain(
"[src/changed-file.ts](https://github.example.com/org/repo/pull/42/files#diff-",
);
});
});
});
});
Loading
Loading