Skip to content

Commit 13a3b0b

Browse files
committed
fix: Respect SourcePath config
and add additional parameter
1 parent c7af8cb commit 13a3b0b

9 files changed

Lines changed: 253 additions & 15 deletions

File tree

action.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ inputs:
1717
description: Select subdirectories to show coverage data for
1818
required: false
1919
default: '**'
20+
source:
21+
description: Source directory for resolving file paths from coverage files
22+
required: false
23+
default: ${{ github.workspace }}
2024
token:
2125
description: GitHub token for API access
2226
required: true

src/action.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type Inputs = {
88
updateComment: boolean
99
showChangedLinesOnly: boolean
1010
showGlobOnly: string
11+
sourceDir: string
1112
}
1213

1314
/**
@@ -26,6 +27,7 @@ export const run = async (inputs: Inputs, octokit: Octokit, context: Context): P
2627
const result = await processCoverage(
2728
{
2829
files: inputs.files,
30+
sourceDir: inputs.sourceDir,
2931
showChangedLinesOnly: inputs.showChangedLinesOnly,
3032
showGlobOnly: inputs.showGlobOnly,
3133
baseRef,

src/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ USAGE:
2424
OPTIONS:
2525
--files <patterns> Coverage file patterns (required, comma or newline separated)
2626
--output <path> Output file path (optional, prints to stdout if not specified)
27+
--source <path> Source directory for resolving file paths (default: current directory)
2728
--base-ref <ref> Git ref to compare against (e.g., origin/main, HEAD~1)
2829
--show-changed-only Filter to show only changed lines (requires --base-ref)
2930
--show-glob <pattern> Glob pattern to filter which files to show (default: **)
@@ -49,6 +50,7 @@ async function main(): Promise<void> {
4950
options: {
5051
files: { type: 'string', short: 'f' },
5152
output: { type: 'string', short: 'o' },
53+
source: { type: 'string', short: 's' },
5254
'base-ref': { type: 'string', short: 'b' },
5355
'show-changed-only': { type: 'boolean', default: false },
5456
'show-glob': { type: 'string', default: '**' },
@@ -81,6 +83,7 @@ async function main(): Promise<void> {
8183
const result = await processCoverage(
8284
{
8385
files: values.files,
86+
sourceDir: values.source ?? process.cwd(),
8487
showChangedLinesOnly: values['show-changed-only'] ?? false,
8588
showGlobOnly: values['show-glob'] ?? '**',
8689
...(values['base-ref'] && { baseRef: values['base-ref'] }),

src/core/process-coverage.ts

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { glob } from 'glob'
33
import { CoberturaCoverageParser, type CoverageReport } from '../coverage/index.js'
44
import { applyFilters, getChangedLinesFromGit, type ChangedLinesMap, type FilterContext } from '../filter/index.js'
55
import { generateMarkdown } from '../markdown/index.js'
6+
import { resolveFilePaths, type PathResolutionContext } from '../path/index.js'
67

78
/**
89
* Logger interface for dependency injection.
@@ -19,6 +20,8 @@ export type Logger = {
1920
export type ProcessCoverageInputs = {
2021
/** Coverage file patterns (newline or comma separated) */
2122
files: string
23+
/** Source directory for resolving file paths from coverage files */
24+
sourceDir: string
2225
/** Whether to filter to show only changed lines */
2326
showChangedLinesOnly: boolean
2427
/** Glob pattern to filter which files to show */
@@ -86,9 +89,37 @@ export async function processCoverage(
8689
reports.push(report)
8790
}
8891

89-
// Merge all reports into one
92+
// Merge all reports into one (including sources)
9093
const mergedReport = mergeReports(reports)
9194

95+
// Resolve file paths to get display paths and absolute paths
96+
const allFilenames: string[] = []
97+
for (const pkg of mergedReport.packages) {
98+
for (const file of pkg.files) {
99+
allFilenames.push(file.filename)
100+
}
101+
}
102+
103+
const pathContext: PathResolutionContext = {
104+
sources: mergedReport.sources ?? [],
105+
sourceDir: inputs.sourceDir,
106+
logger,
107+
}
108+
109+
logger.info(`Resolving file paths (sourceDir: ${inputs.sourceDir})...`)
110+
const resolvedPaths = await resolveFilePaths(allFilenames, pathContext)
111+
112+
// Update file objects with resolved paths
113+
for (const pkg of mergedReport.packages) {
114+
for (const file of pkg.files) {
115+
const resolution = resolvedPaths.get(file.filename)
116+
if (resolution) {
117+
file.resolvedPath = resolution.absolutePath
118+
file.filename = resolution.displayPath
119+
}
120+
}
121+
}
122+
92123
// Get changed lines using git if filtering is enabled and we have a base ref
93124
let changedLines: ChangedLinesMap | undefined
94125
if (inputs.showChangedLinesOnly && inputs.baseRef) {
@@ -118,16 +149,18 @@ export async function processCoverage(
118149
logger.info('Coverage report filtered based on configuration')
119150
}
120151

121-
// Collect all unique file paths from the filtered report
122-
const filePaths = new Set<string>()
152+
// Collect all unique resolved file paths from the filtered report
153+
// Map from display path (filename) to resolved path for reading
154+
const filePathMap = new Map<string, string>()
123155
for (const pkg of filteredReport.packages) {
124156
for (const file of pkg.files) {
125-
filePaths.add(file.filename)
157+
// Use resolvedPath for reading, fall back to filename if not set
158+
filePathMap.set(file.filename, file.resolvedPath ?? file.filename)
126159
}
127160
}
128161

129-
// Read file contents from disk
130-
const fileContents = await readFileContents([...filePaths])
162+
// Read file contents from disk using resolved paths
163+
const fileContents = await readFileContents(filePathMap)
131164

132165
// Generate markdown from filtered report
133166
const markdown = generateMarkdown(filteredReport, fileContents)
@@ -140,11 +173,20 @@ export async function processCoverage(
140173

141174
/**
142175
* Merge multiple coverage reports into one.
176+
* Also merges sources from all reports.
143177
*/
144178
function mergeReports(reports: CoverageReport[]): CoverageReport {
145179
const packageMap = new Map<string, CoverageReport['packages'][0]>()
180+
const allSources = new Set<string>()
146181

147182
for (const report of reports) {
183+
// Collect sources from all reports
184+
if (report.sources) {
185+
for (const source of report.sources) {
186+
allSources.add(source)
187+
}
188+
}
189+
148190
for (const pkg of report.packages) {
149191
if (packageMap.has(pkg.name)) {
150192
// Merge files into existing package
@@ -156,7 +198,11 @@ function mergeReports(reports: CoverageReport[]): CoverageReport {
156198
}
157199
}
158200

159-
return { packages: Array.from(packageMap.values()) }
201+
const result: CoverageReport = { packages: Array.from(packageMap.values()) }
202+
if (allSources.size > 0) {
203+
result.sources = Array.from(allSources)
204+
}
205+
return result
160206
}
161207

162208
/**
@@ -195,20 +241,22 @@ function calculateOverallMetrics(report: CoverageReport): CoverageMetrics {
195241
}
196242

197243
/**
198-
* Read file contents from disk for a list of file paths.
199-
* Returns a map of filepath -> lines array.
244+
* Read file contents from disk for a map of display paths to resolved paths.
245+
* Returns a map of display path -> lines array.
200246
* Files that don't exist return empty arrays.
247+
*
248+
* @param pathMap - Map of display path to resolved (absolute) path
201249
*/
202-
async function readFileContents(filepaths: string[]): Promise<Map<string, string[]>> {
250+
async function readFileContents(pathMap: Map<string, string>): Promise<Map<string, string[]>> {
203251
const contents = new Map<string, string[]>()
204252

205-
for (const filepath of filepaths) {
253+
for (const [displayPath, resolvedPath] of pathMap) {
206254
try {
207-
const content = await fs.readFile(filepath, 'utf-8')
208-
contents.set(filepath, content.split('\n'))
255+
const content = await fs.readFile(resolvedPath, 'utf-8')
256+
contents.set(displayPath, content.split('\n'))
209257
} catch {
210258
// File doesn't exist or can't be read - use empty array
211-
contents.set(filepath, [])
259+
contents.set(displayPath, [])
212260
}
213261
}
214262

src/coverage/model.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ export type CoverageMetrics = {
1515
}
1616

1717
export type FileCoverage = {
18+
/** Display path (relative to source root) */
1819
filename: string
20+
/** Absolute path for reading file contents (set after path resolution) */
21+
resolvedPath?: string
1922
lines: LineCoverage[]
2023
lineMetrics: CoverageMetrics
2124
branchMetrics?: CoverageMetrics
@@ -29,4 +32,6 @@ export type PackageCoverage = {
2932

3033
export type CoverageReport = {
3134
packages: PackageCoverage[]
35+
/** Source paths from Cobertura XML <sources> element */
36+
sources?: string[]
3237
}

src/coverage/parsers/cobertura.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ type CoberturaRoot = {
4949
'@_lines-valid': string
5050
'@_branches-covered': string
5151
'@_branches-valid': string
52+
sources?: {
53+
source?: string | string[]
54+
}
5255
packages?: {
5356
package?: CoberturaPackage | CoberturaPackage[]
5457
}
@@ -79,8 +82,20 @@ export class CoberturaCoverageParser implements CoverageParser {
7982
const coverage = parsed.coverage
8083

8184
const packages = this.parsePackages(coverage.packages?.package)
85+
const sources = this.parseSources(coverage.sources?.source)
86+
87+
const report: CoverageReport = { packages }
88+
if (sources.length > 0) {
89+
report.sources = sources
90+
}
91+
return report
92+
}
8293

83-
return { packages }
94+
private parseSources(sources: string | string[] | undefined): string[] {
95+
if (!sources) return []
96+
const sourceList = Array.isArray(sources) ? sources : [sources]
97+
// Normalize path separators (Windows backslashes to forward slashes)
98+
return sourceList.map((s) => s.replace(/\\/g, '/'))
8499
}
85100

86101
private parsePackages(packages: CoberturaPackage | CoberturaPackage[] | undefined): PackageCoverage[] {

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ try {
99
updateComment: core.getBooleanInput('update-comment'),
1010
showChangedLinesOnly: core.getBooleanInput('show-changed-lines-only'),
1111
showGlobOnly: core.getInput('show-glob-only') || '**',
12+
sourceDir: core.getInput('source') || process.env['GITHUB_WORKSPACE'] || process.cwd(),
1213
},
1314
getOctokit(),
1415
await getContext(),

src/path/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export {
2+
resolveFilePath,
3+
resolveFilePaths,
4+
type PathLogger,
5+
type PathResolutionContext,
6+
type PathResolutionResult,
7+
} from './resolve.js'

0 commit comments

Comments
 (0)