Compare commits

..

12 Commits

Author SHA1 Message Date
Jozef Izso
8fd5fc58ca Add missing golang-json reporter to action.yml
The golang-json reporter has been fully implemented since earlier versions
but was missing from the action.yml documentation. This made it undiscoverable
for users looking for Go test support.

Changes:
- Added golang-json to the list of supported reporters in action.yml

This aligns the action.yml with:
- The actual implementation in src/main.ts (lines 264-265)
- The README.md documentation (line 145)
- The existing parser and tests

Fixes #689

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 17:47:11 +01:00
Jozef Izso
3b9dad208e Merge pull request #681 from phactum-mnestler/main
Update sax.js to fix large XML file parsing #681
2025-11-15 11:24:15 +01:00
Jozef Izso
7c636a991c Merge pull request #643 from micmarc/feature/python-support 2025-11-15 11:12:45 +01:00
Michael Nestler
cfce4bda71 Add saxjs to version overrides 2025-11-15 11:07:56 +01:00
Michael Marcus
fe87682515 Improve testing with robust schema for unittest report 2025-11-14 21:59:25 -05:00
Michael Marcus
9b8d3b002e Python support
Add python-xunit-parser.ts with associated case statement
Add python-xunit to reporter docs in action.yml
Add tests
Update README

Resolves #244
Resolves #633
2025-11-14 16:29:58 -05:00
Jozef Izso
e2f0ff6339 Merge pull request #645 from micmarc/fix/report-title-short-summary 2025-11-14 20:00:35 +01:00
Jozef Izso
bc8c29617e test-reporter release v2.2.0
Merge pull request #679 from dorny/release/v2.2.0
2025-11-14 18:46:03 +01:00
Michael Marcus
9aef9d168f Remove info log 2025-11-14 12:01:42 -05:00
Michael Marcus
6b64465c34 Rebuild index.js after rebase from main 2025-11-14 11:59:46 -05:00
Michael Marcus
6617053f9c Fix short summary formatting when a report title is present 2025-11-14 11:58:16 -05:00
Michael Nestler
43a747d94c Update sax.js to fix large XML file parsing 2025-11-14 16:06:35 +01:00
14 changed files with 888 additions and 430 deletions

View File

@@ -19,6 +19,7 @@ This [Github Action](https://github.com/features/actions) displays test results
- Go / [go test](https://pkg.go.dev/testing)
- Java / [JUnit](https://junit.org/)
- JavaScript / [JEST](https://jestjs.io/) / [Mocha](https://mochajs.org/)
- Python / [pytest](https://docs.pytest.org/en/stable/) / [unittest](https://docs.python.org/3/library/unittest.html)
- Swift / xUnit
For more information see [Supported formats](#supported-formats) section.
@@ -145,7 +146,9 @@ jobs:
# java-junit
# jest-junit
# mocha-json
# python-xunit
# rspec-json
# swift-xunit
reporter: ''
# Allows you to generate only the summary.
@@ -349,6 +352,16 @@ Before version [v9.1.0](https://github.com/mochajs/mocha/releases/tag/v9.1.0), M
Please update Mocha to version [v9.1.0](https://github.com/mochajs/mocha/releases/tag/v9.1.0) or above if you encounter this issue.
</details>
<details>
<summary>python-xunit (Experimental)</summary>
Support for Python test results in xUnit format is experimental - should work but it was not extensively tested.
For pytest support, configure [JUnit XML output](https://docs.pytest.org/en/stable/how-to/output.html#creating-junitxml-format-files) and run with the `--junit-xml` option, which also lets you specify the output path for test results.
For unittest support, use a test runner that outputs the JUnit report format, such as [unittest-xml-reporting](https://pypi.org/project/unittest-xml-reporting/).
</details>
<details>
<summary>swift-xunit (Experimental)</summary>

View File

@@ -0,0 +1,23 @@
![Tests failed](https://img.shields.io/badge/tests-4%20passed%2C%202%20failed%2C%202%20skipped-critical)
|Report|Passed|Failed|Skipped|Time|
|:---|---:|---:|---:|---:|
|[fixtures/python-xunit-unittest.xml](#user-content-r0)|4 ✅|2 ❌|2 ⚪|1ms|
## ❌ <a id="user-content-r0" href="#user-content-r0">fixtures/python-xunit-unittest.xml</a>
**8** tests were completed in **1ms** with **4** passed, **2** failed and **2** skipped.
|Test suite|Passed|Failed|Skipped|Time|
|:---|---:|---:|---:|---:|
|[TestAcme-20251114214921](#user-content-r0s0)|4 ✅|2 ❌|2 ⚪|1ms|
### ❌ <a id="user-content-r0s0" href="#user-content-r0s0">TestAcme-20251114214921</a>
```
TestAcme
✅ test_always_pass
✅ test_parameterized_0_param1
✅ test_parameterized_1_param2
✅ test_with_subtests
❌ test_always_fail
AssertionError: failed
❌ test_error
Exception: error
⚪ test_always_skip
⚪ test_expected_failure
```

View File

@@ -0,0 +1,87 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`python-xunit unittest report report from python test results matches snapshot 1`] = `
TestRunResult {
"path": "fixtures/python-xunit-unittest.xml",
"suites": [
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "TestAcme",
"tests": [
TestCaseResult {
"error": undefined,
"name": "test_always_pass",
"result": "success",
"time": 0,
},
TestCaseResult {
"error": undefined,
"name": "test_parameterized_0_param1",
"result": "success",
"time": 1,
},
TestCaseResult {
"error": undefined,
"name": "test_parameterized_1_param2",
"result": "success",
"time": 0,
},
TestCaseResult {
"error": undefined,
"name": "test_with_subtests",
"result": "success",
"time": 0,
},
TestCaseResult {
"error": {
"details": "Traceback (most recent call last):
File "/Users/foo/Projects/python-test/tests/test_lib.py", line 24, in test_always_fail
self.fail("failed")
AssertionError: failed
",
"line": undefined,
"message": "AssertionError: failed",
"path": undefined,
},
"name": "test_always_fail",
"result": "failed",
"time": 0,
},
TestCaseResult {
"error": {
"details": "Traceback (most recent call last):
File "/Users/foo/Projects/python-test/tests/test_lib.py", line 31, in test_error
raise Exception("error")
Exception: error
",
"line": undefined,
"message": "Exception: error",
"path": undefined,
},
"name": "test_error",
"result": "failed",
"time": 0,
},
TestCaseResult {
"error": undefined,
"name": "test_always_skip",
"result": "skipped",
"time": 0,
},
TestCaseResult {
"error": undefined,
"name": "test_expected_failure",
"result": "skipped",
"time": 0,
},
],
},
],
"name": "TestAcme-20251114214921",
"totalTime": 1,
},
],
"totalTime": 1,
}
`;

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="TestAcme-20251114214921" tests="8" file=".py" time="0.001" timestamp="2025-11-14T21:49:22" failures="1" errors="1" skipped="2">
<testcase classname="TestAcme" name="test_always_pass" time="0.000" timestamp="2025-11-14T21:49:22" file="tests/test_lib.py" line="8"/>
<testcase classname="TestAcme" name="test_parameterized_0_param1" time="0.001" timestamp="2025-11-14T21:49:22" file="tests/test_lib.py" line="618"/>
<testcase classname="TestAcme" name="test_parameterized_1_param2" time="0.000" timestamp="2025-11-14T21:49:22" file="tests/test_lib.py" line="618"/>
<testcase classname="TestAcme" name="test_with_subtests" time="0.000" timestamp="2025-11-14T21:49:22" file="tests/test_lib.py" line="11"/>
<testcase classname="TestAcme" name="test_always_fail" time="0.000" timestamp="2025-11-14T21:49:22" file="tests/test_lib.py" line="23">
<failure type="AssertionError" message="failed"><![CDATA[Traceback (most recent call last):
File "/Users/foo/Projects/python-test/tests/test_lib.py", line 24, in test_always_fail
self.fail("failed")
AssertionError: failed
]]></failure>
</testcase>
<testcase classname="TestAcme" name="test_error" time="0.000" timestamp="2025-11-14T21:49:22" file="tests/test_lib.py" line="30">
<error type="Exception" message="error"><![CDATA[Traceback (most recent call last):
File "/Users/foo/Projects/python-test/tests/test_lib.py", line 31, in test_error
raise Exception("error")
Exception: error
]]></error>
</testcase>
<testcase classname="TestAcme" name="test_always_skip" time="0.000" timestamp="2025-11-14T21:49:22" file="tests/test_lib.py" line="20">
<skipped type="skip" message="skipped"/>
</testcase>
<testcase classname="TestAcme" name="test_expected_failure" time="0.000" timestamp="2025-11-14T21:49:22" file="tests/test_lib.py" line="26">
<skipped type="XFAIL" message="expected failure: (&lt;class 'AssertionError'&gt;, AssertionError('expected failure'), &lt;traceback object at 0x100c125c0&gt;)"/>
</testcase>
</testsuite>

View File

@@ -303,4 +303,47 @@ describe('jest-junit tests', () => {
expect(report).not.toContain('<details><summary>Expand for details</summary>')
expect(report).not.toContain('</details>')
})
it('report includes the short summary', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'jest-junit.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const opts: ParseOptions = {
parseErrors: true,
trackedFiles: []
}
const parser = new JestJunitParser(opts)
const result = await parser.parse(filePath, fileContent)
const shortSummary = '1 passed, 4 failed and 1 skipped'
const report = getReport([result], DEFAULT_OPTIONS, shortSummary)
// Report should have the title as the first line
expect(report).toMatch(/^## 1 passed, 4 failed and 1 skipped\n/)
})
it('report includes a custom report title and short summary', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'jest-junit.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const opts: ParseOptions = {
parseErrors: true,
trackedFiles: []
}
const parser = new JestJunitParser(opts)
const result = await parser.parse(filePath, fileContent)
const shortSummary = '1 passed, 4 failed and 1 skipped'
const report = getReport(
[result],
{
...DEFAULT_OPTIONS,
reportTitle: 'My Custom Title'
},
shortSummary
)
// Report should have the title as the first line
expect(report).toMatch(/^# My Custom Title\n## 1 passed, 4 failed and 1 skipped\n/)
})
})

View File

@@ -0,0 +1,70 @@
import * as fs from 'fs'
import * as path from 'path'
import {PythonXunitParser} from '../src/parsers/python-xunit/python-xunit-parser'
import {ParseOptions} from '../src/test-parser'
import {DEFAULT_OPTIONS, getReport} from '../src/report/get-report'
import {normalizeFilePath} from '../src/utils/path-utils'
const defaultOpts: ParseOptions = {
parseErrors: true,
trackedFiles: []
}
describe('python-xunit unittest report', () => {
const fixturePath = path.join(__dirname, 'fixtures', 'python-xunit-unittest.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
it('report from python test results matches snapshot', async () => {
const outputPath = path.join(__dirname, '__outputs__', 'python-xunit.md')
const trackedFiles = ['tests/test_lib.py']
const opts: ParseOptions = {
...defaultOpts,
trackedFiles
}
const parser = new PythonXunitParser(opts)
const result = await parser.parse(filePath, fileContent)
expect(result).toMatchSnapshot()
const report = getReport([result])
fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, report)
})
it('report does not include a title by default', async () => {
const parser = new PythonXunitParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const report = getReport([result])
// Report should have the badge as the first line
expect(report).toMatch(/^!\[Tests failed]/)
})
it.each([
['empty string', ''],
['space', ' '],
['tab', '\t'],
['newline', '\n']
])('report does not include a title when configured value is %s', async (_, reportTitle) => {
const parser = new PythonXunitParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const report = getReport([result], {
...DEFAULT_OPTIONS,
reportTitle
})
// Report should have the badge as the first line
expect(report).toMatch(/^!\[Tests failed]/)
})
it('report includes a custom report title', async () => {
const parser = new PythonXunitParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const report = getReport([result], {
...DEFAULT_OPTIONS,
reportTitle: 'My Custom Title'
})
// Report should have the title as the first line
expect(report).toMatch(/^# My Custom Title\n/)
})
})

View File

@@ -29,9 +29,11 @@ inputs:
- dotnet-nunit
- dotnet-trx
- flutter-json
- golang-json
- java-junit
- jest-junit
- mocha-json
- python-xunit
- rspec-json
- swift-xunit
required: true

908
dist/index.js generated vendored

File diff suppressed because it is too large Load Diff

80
dist/licenses.txt generated vendored
View File

@@ -1350,48 +1350,62 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
sax
ISC
The ISC License
BlueOak-1.0.0
# Blue Oak Model License
Copyright (c) Isaac Z. Schlueter and Contributors
Version 1.0.0
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
## Purpose
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
This license gives everyone as much permission to work with
this software as possible, while protecting contributors
from liability.
====
## Acceptance
`String.fromCodePoint` by Mathias Bynens used according to terms of MIT
License, as follows:
In order to receive this license, you must agree to its
rules. The rules of this license are both obligations
under that agreement and conditions to your license.
You must not do anything with this software that triggers
a rule that you cannot or will not follow.
Copyright Mathias Bynens <https://mathiasbynens.be/>
## Copyright
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
Each contributor licenses you to do everything with this
software that would otherwise infringe that contributor's
copyright in it.
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
## Notices
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
You must ensure that everyone who gets a copy of
any part of this software from you, with or without
changes, also gets the text of this license or a link to
<https://blueoakcouncil.org/license/1.0.0>.
## Excuse
If anyone notifies you in writing that you have not
complied with [Notices](#notices), you can keep your
license by taking all practical steps to comply within 30
days after the notice. If you do not do so, your license
ends immediately.
## Patent
Each contributor licenses you to do everything with this
software that would otherwise infringe any patent claims
they can license or become able to license.
## Reliability
No contributor can revoke this license.
## No Liability
***As far as the law allows, this software comes as is,
without any warranty or condition, and no contributor
will be liable to anyone for any damages related to this
software or this license, under any kind of legal claim.***
to-regex-range

7
package-lock.json generated
View File

@@ -7580,9 +7580,10 @@
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
"license": "BlueOak-1.0.0"
},
"node_modules/semver": {
"version": "7.7.3",

View File

@@ -69,6 +69,9 @@
"ts-jest": "^29.4.5",
"typescript": "^5.9.3"
},
"overrides": {
"sax": "^1.4.3"
},
"jest-junit": {
"suiteName": "jest tests",
"outputDirectory": "__tests__/__results__",

View File

@@ -17,9 +17,9 @@ import {GolangJsonParser} from './parsers/golang-json/golang-json-parser'
import {JavaJunitParser} from './parsers/java-junit/java-junit-parser'
import {JestJunitParser} from './parsers/jest-junit/jest-junit-parser'
import {MochaJsonParser} from './parsers/mocha-json/mocha-json-parser'
import {PythonXunitParser} from './parsers/python-xunit/python-xunit-parser'
import {RspecJsonParser} from './parsers/rspec-json/rspec-json-parser'
import {SwiftXunitParser} from './parsers/swift-xunit/swift-xunit-parser'
import {normalizeDirPath, normalizeFilePath} from './utils/path-utils'
import {getCheckRunContext} from './utils/github-utils'
@@ -181,20 +181,23 @@ class TestReporter {
let baseUrl = ''
if (this.useActionsSummary) {
const summary = getReport(results, {
listSuites,
listTests,
baseUrl,
onlySummary,
useActionsSummary,
badgeTitle,
reportTitle,
collapsed
})
const summary = getReport(
results,
{
listSuites,
listTests,
baseUrl,
onlySummary,
useActionsSummary,
badgeTitle,
reportTitle,
collapsed
},
shortSummary
)
core.info('Summary content:')
core.info(summary)
core.summary.addRaw(`# ${shortSummary}`)
await core.summary.addRaw(summary).write()
} else {
core.info(`Creating check run ${name}`)
@@ -268,6 +271,8 @@ class TestReporter {
return new JestJunitParser(options)
case 'mocha-json':
return new MochaJsonParser(options)
case 'python-xunit':
return new PythonXunitParser(options)
case 'rspec-json':
return new RspecJsonParser(options)
case 'swift-xunit':

View File

@@ -0,0 +1,8 @@
import {ParseOptions} from '../../test-parser'
import {JavaJunitParser} from '../java-junit/java-junit-parser'
export class PythonXunitParser extends JavaJunitParser {
constructor(readonly options: ParseOptions) {
super(options)
}
}

View File

@@ -30,13 +30,15 @@ export const DEFAULT_OPTIONS: ReportOptions = {
collapsed: 'auto'
}
export function getReport(results: TestRunResult[], options: ReportOptions = DEFAULT_OPTIONS): string {
core.info('Generating check run summary')
export function getReport(
results: TestRunResult[],
options: ReportOptions = DEFAULT_OPTIONS,
shortSummary = ''
): string {
applySort(results)
const opts = {...options}
let lines = renderReport(results, opts)
let lines = renderReport(results, opts, shortSummary)
let report = lines.join('\n')
if (getByteLength(report) <= getMaxReportLength(options)) {
@@ -46,7 +48,7 @@ export function getReport(results: TestRunResult[], options: ReportOptions = DEF
if (opts.listTests === 'all') {
core.info("Test report summary is too big - setting 'listTests' to 'failed'")
opts.listTests = 'failed'
lines = renderReport(results, opts)
lines = renderReport(results, opts, shortSummary)
report = lines.join('\n')
if (getByteLength(report) <= getMaxReportLength(options)) {
return report
@@ -103,7 +105,7 @@ function getByteLength(text: string): number {
return Buffer.byteLength(text, 'utf8')
}
function renderReport(results: TestRunResult[], options: ReportOptions): string[] {
function renderReport(results: TestRunResult[], options: ReportOptions, shortSummary: string): string[] {
const sections: string[] = []
const reportTitle: string = options.reportTitle.trim()
@@ -111,6 +113,10 @@ function renderReport(results: TestRunResult[], options: ReportOptions): string[
sections.push(`# ${reportTitle}`)
}
if (shortSummary) {
sections.push(`## ${shortSummary}`)
}
const badge = getReportBadge(results, options)
sections.push(badge)