Skip to content
Draft
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ Powered by [Job Summaries](https://github.blog/2022-05-09-supercharging-github-a
# stderr: standard error output of `go test` subprocess
# Optional. No default
omit:

# Generate coverage profile and display coverage percentage.
# Specify the coverage file path (e.g., "coverage.out").
# When set, coverage percentages will be displayed in the summary table.
# Optional. No default
coverprofile:
```

## Screenshots
Expand Down Expand Up @@ -97,6 +103,15 @@ jobs:
fromJSONFile: /path/to/test2json.json
```

### With coverage

```yaml
- name: Test
uses: robherley/go-test-action@v0
with:
coverprofile: coverage.out
```

### Omitting elements

See [Inputs](#inputs) above for valid options
Expand Down
14 changes: 14 additions & 0 deletions __tests__/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,18 @@ describe('events', () => {
const otherTestEvents = parseTestEvents(otherStdout)
expect(otherTestEvents[0]).toHaveProperty('isCached', false)
})

it('correctly parses coverage percentage', () => {
const coverageStdout =
'{"Time":"2025-12-13T04:41:53.511365498Z","Action":"output","Package":"example.com/test","Output":"coverage: 50.0% of statements\\n"}'

const coverageEvents = parseTestEvents(coverageStdout)
expect(coverageEvents[0]).toHaveProperty('coverage', 50.0)

const noCoverageStdout =
'{"Time":"2022-07-10T22:42:11.931552-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Output":"ok \\tgithub.com/robherley/go-test-example/success\\n"}'

const noCoverageEvents = parseTestEvents(noCoverageStdout)
expect(noCoverageEvents[0]).toHaveProperty('coverage', undefined)
})
})
9 changes: 9 additions & 0 deletions __tests__/fixtures/gotestoutput_coverage.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{"Time":"2025-12-13T04:41:53.509319636Z","Action":"start","Package":"example.com/test"}
{"Time":"2025-12-13T04:41:53.510906756Z","Action":"run","Package":"example.com/test","Test":"TestAdd"}
{"Time":"2025-12-13T04:41:53.510923518Z","Action":"output","Package":"example.com/test","Test":"TestAdd","Output":"=== RUN TestAdd\n"}
{"Time":"2025-12-13T04:41:53.510972229Z","Action":"output","Package":"example.com/test","Test":"TestAdd","Output":"--- PASS: TestAdd (0.00s)\n"}
{"Time":"2025-12-13T04:41:53.510985845Z","Action":"pass","Package":"example.com/test","Test":"TestAdd","Elapsed":0}
{"Time":"2025-12-13T04:41:53.510994671Z","Action":"output","Package":"example.com/test","Output":"PASS\n"}
{"Time":"2025-12-13T04:41:53.511365498Z","Action":"output","Package":"example.com/test","Output":"coverage: 50.0% of statements\n"}
{"Time":"2025-12-13T04:41:53.511714866Z","Action":"output","Package":"example.com/test","Output":"ok \texample.com/test\t0.002s\tcoverage: 50.0% of statements\n"}
{"Time":"2025-12-13T04:41:53.511732649Z","Action":"pass","Package":"example.com/test","Elapsed":0.002}
8 changes: 8 additions & 0 deletions __tests__/inputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('renderer', () => {
testArguments: ['./...'],
fromJSONFile: null,
omit: new Set(),
coverprofile: null,
})
})

Expand All @@ -50,6 +51,13 @@ describe('renderer', () => {
expect(inputs.fromJSONFile).toEqual('foo.json')
})

it('parses coverprofile', () => {
mockInput('coverprofile', 'coverage.out')
const inputs = getInputs()

expect(inputs.coverprofile).toEqual('coverage.out')
})

it('parses omit', () => {
mockInput(
'omit',
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ inputs:
description: 'Omit any packages from summary output that are successful'
required: false
default: 'false'
coverprofile:
description: 'Generate coverage profile and display coverage percentage. Specify the coverage file path (e.g., "coverage.out")'
required: false
runs:
using: 'node20'
main: 'dist/index.js'
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface TestEvent {
isPackageLevel: boolean
isConclusive: boolean
isCached: boolean
coverage?: number // percentage (e.g., 50.0 for 50.0%)
}

/**
Expand All @@ -50,6 +51,16 @@ export function parseTestEvents(stdout: string): TestEvent[] {
for (let line of lines) {
try {
const json = JSON.parse(line)

// Parse coverage percentage from output
let coverage: number | undefined
if (json.Output) {
const coverageMatch = json.Output.match(/coverage:\s*(\d+(?:\.\d+)?)%\s+of\s+statements/)
if (coverageMatch) {
coverage = parseFloat(coverageMatch[1])
}
}

events.push({
time: json.Time && new Date(json.Time),
action: json.Action as TestEventAction,
Expand All @@ -61,6 +72,7 @@ export function parseTestEvents(stdout: string): TestEvent[] {
isSubtest: json.Test?.includes('/') || false, // afaik there isn't a better indicator in test2json
isPackageLevel: typeof json.Test === 'undefined',
isConclusive: conclusiveTestEvents.includes(json.Action),
coverage,
})
} catch {
core.debug(`unable to parse line: ${line}`)
Expand Down
7 changes: 7 additions & 0 deletions src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface Inputs {
testArguments: string[]
fromJSONFile: string | null
omit: Set<OmitOption>
coverprofile: string | null
}

export enum OmitOption {
Expand All @@ -27,6 +28,7 @@ export const defaultInputs = (): Inputs => ({
testArguments: ['./...'],
fromJSONFile: null,
omit: new Set(),
coverprofile: null,
})

/**
Expand Down Expand Up @@ -65,6 +67,11 @@ export function getInputs(): Inputs {
.forEach(option => inputs.omit.add(option as OmitOption))
}

const coverprofile = core.getInput('coverprofile')
if (coverprofile) {
inputs.coverprofile = coverprofile
}

return inputs
}

Expand Down
43 changes: 34 additions & 9 deletions src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Renderer {
stderr: string
omit: Set<OmitOption>
packageResults: PackageResult[]
private hasCoverage: boolean = false
headers: SummaryTableRow = [
{ data: '📦 Package', header: true },
{ data: '🟢 Passed', header: true },
Expand All @@ -42,6 +43,7 @@ class Renderer {
this.stderr = stderr
this.omit = omit
this.packageResults = this.calculatePackageResults()
this.setupHeaders()
}

/**
Expand Down Expand Up @@ -73,6 +75,20 @@ class Renderer {
.write()
}

/**
* Setup table headers based on whether coverage data is available
*/
private setupHeaders() {
// Check if any package has coverage data
this.hasCoverage = this.packageResults.some(
result => result.coverage !== undefined
)

if (this.hasCoverage) {
this.headers.push({ data: '📊 Coverage', header: true })
}
}

/**
* Filters package results based on omit options
* @returns filtered package results to render
Expand Down Expand Up @@ -211,18 +227,27 @@ class Renderer {
packageResult.packageEvent.package === this.moduleName ? ' (main)' : ''
}</code>`

const packageRows: SummaryTableRow[] = [
[
pkgName,
packageResult.conclusions.pass.toString(),
packageResult.conclusions.fail.toString(),
packageResult.conclusions.skip.toString(),
`${(packageResult.packageEvent.elapsed || 0) * 1000}ms`,
],
const baseRow = [
pkgName,
packageResult.conclusions.pass.toString(),
packageResult.conclusions.fail.toString(),
packageResult.conclusions.skip.toString(),
`${(packageResult.packageEvent.elapsed || 0) * 1000}ms`,
]

if (this.hasCoverage) {
baseRow.push(
packageResult.coverage !== undefined
? `${packageResult.coverage.toFixed(1)}%`
: 'N/A'
)
}

const packageRows: SummaryTableRow[] = [baseRow]

if (details) {
packageRows.push([{ data: details, colspan: '5' }])
const colspan = this.hasCoverage ? '6' : '5'
packageRows.push([{ data: details, colspan }])
}

return packageRows
Expand Down
6 changes: 6 additions & 0 deletions src/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class PackageResult {
fail: 0,
skip: 0,
}
coverage?: number

constructor(packageEvent: TestEvent, events: TestEvent[]) {
this.packageEvent = packageEvent
Expand All @@ -25,6 +26,11 @@ class PackageResult {
)

this.eventsToResults()

// Extract coverage from package-level event if present
if (this.packageEvent.coverage !== undefined) {
this.coverage = this.packageEvent.coverage
}
}

public testCount(): number {
Expand Down
10 changes: 9 additions & 1 deletion src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,17 @@ class Runner {
},
})

const testArgs = ['-json']

if (this.inputs.coverprofile) {
testArgs.push('-coverprofile', this.inputs.coverprofile)
}

testArgs.push(...this.inputs.testArguments)

const retCode = await exec(
'go',
['test', '-json', ...this.inputs.testArguments],
['test', ...testArgs],
{
cwd: this.inputs.moduleDirectory,
ignoreReturnCode: true,
Expand Down