Compare commits

..

14 commits

Author SHA1 Message Date
Jozef Izso
29672c52a8
Merge pull request #772 from dorny/codex/summary-output 2026-04-25 14:21:30 +02:00
Jozef Izso
373689ed6b
chore: rebuild dist folder 2026-04-25 14:20:38 +02:00
Jozef Izso
bd45c7a559
Refactor summary output to use a temporary file 2026-04-25 14:20:21 +02:00
Jozef Izso
49ad12d837
Add changelog for summary output 2026-04-25 14:03:52 +02:00
Jozef Izso
f610967bd1
Add generated summary action output 2026-04-25 14:03:52 +02:00
Jozef Izso
1ab1766274
Merge pull request #773 from dorny/codex/list-files-input 2026-04-25 13:34:59 +02:00
Jozef Izso
528bb1a4e4
Add changelog for list-files input 2026-04-25 13:31:01 +02:00
Jozef Izso
e05b863b43
Suppress empty list-files summary table 2026-04-25 13:17:09 +02:00
Jozef Izso
d7f15447e6
Tighten list-files report tests 2026-04-25 13:04:58 +02:00
Jozef Izso
0af87079d7
Merge pull request #771 from dorny/dependabot/npm_and_yarn/jest-junit-17.0.0 2026-04-25 12:13:48 +02:00
Jozef Izso
45c8d46681
Resolve list-files conflicts on current main 2026-04-25 12:13:19 +02:00
Felix Chapman
ba6f1d11e2
Correct README to indicate list-files is not automatically adjusted 2026-04-25 12:12:06 +02:00
Felix Chapman
a48134be3e
Add new parameter list-files for when there are multiple files in a report 2026-04-25 12:12:06 +02:00
dependabot[bot]
97ba6567fc
Bump jest-junit from 16.0.0 to 17.0.0
Bumps [jest-junit](https://github.com/jest-community/jest-junit) from 16.0.0 to 17.0.0.
- [Release notes](https://github.com/jest-community/jest-junit/releases)
- [Commits](https://github.com/jest-community/jest-junit/compare/v16.0.0...v17.0.0)

---
updated-dependencies:
- dependency-name: jest-junit
  dependency-version: 17.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-24 23:52:36 +00:00
9 changed files with 278 additions and 105 deletions

View file

@ -1,5 +1,9 @@
# Changelog
## 3.1.0
* Feature: Add `list-files` input to control test report file listing https://github.com/dorny/test-reporter/pull/773
* Feature: Add `summary_file` output with the path to the generated summary in Markdown format https://github.com/dorny/test-reporter/pull/772
## 3.0.0
* Feature: Use NodeJS 24 LTS as default runtime https://github.com/dorny/test-reporter/pull/738

View file

@ -184,6 +184,12 @@ jobs:
# none
list-tests: 'all'
# Limits which test result files are listed:
# all
# failed
# none
list-files: 'all'
# Limits number of created annotations with error message and stack trace captured during test execution.
# Must be less or equal to 50.
max-annotations: '10'
@ -219,6 +225,7 @@ jobs:
| time | Test execution time [ms] |
| url | Check run URL |
| url_html | Check run URL HTML |
| summary_file | Path to a file containing the generated test report summary in Markdown format |
| slug_prefix| Random anchor links slug prefix generated for the summary headers |
## Supported formats

View file

@ -134,26 +134,145 @@ describe('getBadge', () => {
})
describe('getReport', () => {
it('sorts suites by descending time when configured', () => {
// Helper function to create test results
function createTestResult(path: string, passed: number, failed: number, skipped: number): TestRunResult {
const tests: TestCaseResult[] = []
for (let i = 0; i < passed; i++) {
tests.push(new TestCaseResult(`passed-test-${i}`, 'success', 100))
}
for (let i = 0; i < failed; i++) {
tests.push(
new TestCaseResult(`failed-test-${i}`, 'failed', 100, {
details: 'Test failed',
message: 'Assertion error'
})
)
}
for (let i = 0; i < skipped; i++) {
tests.push(new TestCaseResult(`skipped-test-${i}`, 'skipped', 0))
}
const group = new TestGroupResult('test-group', tests)
const suite = new TestSuiteResult('test-suite', [group])
return new TestRunResult(path, [suite])
}
describe('list-files parameter', () => {
const results = [
new TestRunResult('report.xml', [
createSuite('unit', 10),
createSuite('integration', 30),
createSuite('smoke', 20)
])
createTestResult('passing-file.spec.ts', 5, 0, 0),
createTestResult('failing-file.spec.ts', 3, 2, 1),
createTestResult('passing-with-skipped-file.spec.ts', 8, 0, 2)
]
const report = getReport(results, {
...DEFAULT_OPTIONS,
sortSuites: 'time-desc',
listTests: 'none'
it('shows all files when list-files is "all"', () => {
const report = getReport(results, {
...DEFAULT_OPTIONS,
listFiles: 'all',
listSuites: 'none',
listTests: 'none'
})
expect(report).toContain('|Report|Passed|Failed|Skipped|Time|')
expect(report).toContain('passing-file.spec.ts')
expect(report).toContain('failing-file.spec.ts')
expect(report).toContain('passing-with-skipped-file.spec.ts')
})
expect(report.indexOf('integration')).toBeLessThan(report.indexOf('smoke'))
expect(report.indexOf('smoke')).toBeLessThan(report.indexOf('unit'))
it('shows only failed files when list-files is "failed"', () => {
const report = getReport(results, {
...DEFAULT_OPTIONS,
listFiles: 'failed',
listSuites: 'none',
listTests: 'none'
})
expect(report).toContain('|Report|Passed|Failed|Skipped|Time|')
expect(report).not.toContain('passing-file.spec.ts')
expect(report).toContain('failing-file.spec.ts')
expect(report).not.toContain('passing-with-skipped-file.spec.ts')
})
it('shows no file details when list-files is "none"', () => {
const report = getReport(results, {
...DEFAULT_OPTIONS,
listFiles: 'none',
listSuites: 'none',
listTests: 'none'
})
expect(report).toContain('![')
expect(report).not.toContain('|Report|Passed|Failed|Skipped|Time|')
expect(report).not.toContain('passing-file.spec.ts')
expect(report).not.toContain('failing-file.spec.ts')
expect(report).not.toContain('passing-with-skipped-file.spec.ts')
})
it('does not show an empty summary table when list-files is "none" and only-summary is enabled', () => {
const report = getReport(results, {
...DEFAULT_OPTIONS,
listFiles: 'none',
listSuites: 'all',
onlySummary: true,
listTests: 'none'
})
expect(report).toContain('![')
expect(report).not.toContain('|Report|Passed|Failed|Skipped|Time|')
expect(report).not.toContain('passing-file.spec.ts')
expect(report).not.toContain('failing-file.spec.ts')
expect(report).not.toContain('passing-with-skipped-file.spec.ts')
})
it('works correctly with list-suites and list-tests when list-files is "failed"', () => {
const report = getReport(results, {
...DEFAULT_OPTIONS,
listFiles: 'failed',
listSuites: 'all',
listTests: 'all'
})
expect(report).toContain('|Report|Passed|Failed|Skipped|Time|')
expect(report).not.toContain('passing-file.spec.ts')
expect(report).toContain('failing-file.spec.ts')
expect(report).not.toContain('passing-with-skipped-file.spec.ts')
// Should show suite details for the failed file
expect(report).toContain('test-suite')
})
it('filters correctly when all files pass and list-files is "failed"', () => {
const allPassingResults = [
createTestResult('passing-file-1.spec.ts', 5, 0, 0),
createTestResult('passing-file-2.spec.ts', 8, 0, 2)
]
const report = getReport(allPassingResults, {
...DEFAULT_OPTIONS,
listFiles: 'failed',
listSuites: 'all',
listTests: 'none'
})
expect(report).not.toContain('passing-file-1.spec.ts')
expect(report).not.toContain('passing-file-2.spec.ts')
expect(report).toContain('![')
expect(report).not.toContain('|Report|Passed|Failed|Skipped|Time|')
})
it('filters correctly when all files fail and list-files is "failed"', () => {
const allFailingResults = [
createTestResult('failing-file-1.spec.ts', 0, 5, 0),
createTestResult('failing-file-2.spec.ts', 1, 2, 1)
]
const report = getReport(allFailingResults, {
...DEFAULT_OPTIONS,
listFiles: 'failed',
listSuites: 'all',
listTests: 'none'
})
expect(report).toContain('failing-file-1.spec.ts')
expect(report).toContain('failing-file-2.spec.ts')
})
})
})
function createSuite(name: string, time: number): TestSuiteResult {
return new TestSuiteResult(name, [new TestGroupResult(name, [new TestCaseResult(`${name}-test`, 'success', time)])])
}

View file

@ -46,13 +46,6 @@ inputs:
- none
required: false
default: 'all'
sort-suites:
description: |
Sort order for test suites. Supported options:
- name: Sort alphabetically by name (default)
- time-desc: Sort by execution time, slowest first
required: false
default: 'name'
list-tests:
description: |
Limits which test cases are listed. Supported options:
@ -61,6 +54,14 @@ inputs:
- none
required: false
default: 'all'
list-files:
description: |
Limits which test result files are listed. Supported options:
- all
- failed
- none
required: false
default: 'all'
max-annotations:
description: |
Limits number of created annotations with error message and stack trace captured during test execution.
@ -129,6 +130,8 @@ outputs:
description: Check run URL
url_html:
description: Check run URL HTML
summary_file:
description: Path to a file containing the generated test report summary in Markdown format
slug_prefix:
description: Random prefix added to generated report anchor slugs for this action run
runs:

95
dist/index.js generated vendored
View file

@ -16015,7 +16015,7 @@ module.exports.fetch = async function fetch (init, options = undefined) {
}
module.exports.Headers = __nccwpck_require__(660).Headers
module.exports.Response = __nccwpck_require__(9051).Response
module.exports.Request = __nccwpck_require__(2348).Request
module.exports.Request = __nccwpck_require__(9967).Request
module.exports.FormData = __nccwpck_require__(5910).FormData
module.exports.File = globalThis.File ?? (__nccwpck_require__(4573).File)
module.exports.FileReader = __nccwpck_require__(8355).FileReader
@ -27430,7 +27430,7 @@ const { urlEquals, getFieldValues } = __nccwpck_require__(6798)
const { kEnumerableProperty, isDisturbed } = __nccwpck_require__(3440)
const { webidl } = __nccwpck_require__(5893)
const { Response, cloneResponse, fromInnerResponse } = __nccwpck_require__(9051)
const { Request, fromInnerRequest } = __nccwpck_require__(2348)
const { Request, fromInnerRequest } = __nccwpck_require__(9967)
const { kState } = __nccwpck_require__(3627)
const { fetching } = __nccwpck_require__(4398)
const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = __nccwpck_require__(3168)
@ -29744,7 +29744,7 @@ module.exports = {
const { pipeline } = __nccwpck_require__(7075)
const { fetching } = __nccwpck_require__(4398)
const { makeRequest } = __nccwpck_require__(2348)
const { makeRequest } = __nccwpck_require__(9967)
const { webidl } = __nccwpck_require__(5893)
const { EventSourceStream } = __nccwpck_require__(4031)
const { parseMIMEType } = __nccwpck_require__(1900)
@ -33368,7 +33368,7 @@ const {
fromInnerResponse
} = __nccwpck_require__(9051)
const { HeadersList } = __nccwpck_require__(660)
const { Request, cloneRequest } = __nccwpck_require__(2348)
const { Request, cloneRequest } = __nccwpck_require__(9967)
const zlib = __nccwpck_require__(8522)
const {
bytesMatch,
@ -35632,7 +35632,7 @@ module.exports = {
/***/ }),
/***/ 2348:
/***/ 9967:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
/* globals AbortController */
@ -40814,7 +40814,7 @@ const {
const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished, parseExtensions } = __nccwpck_require__(8625)
const { channels } = __nccwpck_require__(2414)
const { CloseEvent } = __nccwpck_require__(5188)
const { makeRequest } = __nccwpck_require__(2348)
const { makeRequest } = __nccwpck_require__(9967)
const { fetching } = __nccwpck_require__(4398)
const { Headers, getHeadersList } = __nccwpck_require__(660)
const { getDecodeSplit } = __nccwpck_require__(3168)
@ -56445,6 +56445,12 @@ function getOctokit(token, options, ...additionalPlugins) {
//# sourceMappingURL=github.js.map
// EXTERNAL MODULE: external "node:crypto"
var external_node_crypto_ = __nccwpck_require__(7598);
;// CONCATENATED MODULE: external "node:fs"
const external_node_fs_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:fs");
;// CONCATENATED MODULE: external "node:os"
const external_node_os_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:os");
;// CONCATENATED MODULE: external "node:path"
const external_node_path_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:path");
// EXTERNAL MODULE: ./node_modules/adm-zip/adm-zip.js
var adm_zip = __nccwpck_require__(1316);
// EXTERNAL MODULE: ./node_modules/picomatch/index.js
@ -56954,7 +56960,7 @@ const DEFAULT_OPTIONS = {
listSuites: 'all',
listTests: 'all',
slugPrefix: '',
sortSuites: 'name',
listFiles: 'all',
baseUrl: '',
onlySummary: false,
useActionsSummary: true,
@ -56963,7 +56969,7 @@ const DEFAULT_OPTIONS = {
collapsed: 'auto'
};
function getReport(results, options = DEFAULT_OPTIONS, shortSummary = '') {
applySort(results, options);
applySort(results);
const opts = { ...options };
let lines = renderReport(results, opts, shortSummary);
let report = lines.join('\n');
@ -57011,15 +57017,10 @@ function trimReport(lines, options) {
reportLines.push(errorMsg);
return reportLines.join('\n');
}
function applySort(results, options) {
function applySort(results) {
results.sort((a, b) => a.path.localeCompare(b.path, DEFAULT_LOCALE));
for (const res of results) {
if (options.sortSuites === 'time-desc') {
res.suites.sort((a, b) => b.time - a.time);
}
else {
res.suites.sort((a, b) => a.name.localeCompare(b.name, DEFAULT_LOCALE));
}
res.suites.sort((a, b) => a.name.localeCompare(b.name, DEFAULT_LOCALE));
}
}
function getByteLength(text) {
@ -57080,25 +57081,31 @@ function getTestRunsReport(testRuns, options) {
sections.push(`<details><summary>Expand for details</summary>`);
sections.push(` `);
}
if (testRuns.length > 0 || options.onlySummary) {
const tableData = testRuns
.map((tr, originalIndex) => ({ tr, originalIndex }))
.filter(({ tr }) => tr.passed > 0 || tr.failed > 0 || tr.skipped > 0)
.map(({ tr, originalIndex }) => {
const time = formatTime(tr.time);
const name = tr.path;
const addr = options.baseUrl + makeRunSlug(originalIndex, options).link;
const nameLink = markdown_utils_link(name, addr);
const passed = tr.passed > 0 ? `${tr.passed} ${Icon.success}` : '';
const failed = tr.failed > 0 ? `${tr.failed} ${Icon.fail}` : '';
const skipped = tr.skipped > 0 ? `${tr.skipped} ${Icon.skip}` : '';
return [nameLink, passed, failed, skipped, time];
});
// Filter test runs based on list-files option
const filteredTestRuns = options.listFiles === 'failed'
? testRuns.filter(tr => tr.result === 'failed')
: options.listFiles === 'none'
? []
: testRuns;
const tableData = filteredTestRuns
.map((tr, originalIndex) => ({ tr, originalIndex }))
.filter(({ tr }) => tr.passed > 0 || tr.failed > 0 || tr.skipped > 0)
.map(({ tr, originalIndex }) => {
const time = formatTime(tr.time);
const name = tr.path;
const addr = options.baseUrl + makeRunSlug(originalIndex, options).link;
const nameLink = markdown_utils_link(name, addr);
const passed = tr.passed > 0 ? `${tr.passed} ${Icon.success}` : '';
const failed = tr.failed > 0 ? `${tr.failed} ${Icon.fail}` : '';
const skipped = tr.skipped > 0 ? `${tr.skipped} ${Icon.skip}` : '';
return [nameLink, passed, failed, skipped, time];
});
if (tableData.length > 0) {
const resultsTable = table(['Report', 'Passed', 'Failed', 'Skipped', 'Time'], [Align.Left, Align.Right, Align.Right, Align.Right, Align.Right], ...tableData);
sections.push(resultsTable);
}
if (options.onlySummary === false) {
const suitesReports = testRuns.map((tr, i) => getSuitesReport(tr, i, options)).flat();
const suitesReports = filteredTestRuns.map((tr, i) => getSuitesReport(tr, i, options)).flat();
sections.push(...suitesReports);
}
if (shouldCollapse) {
@ -58931,6 +58938,9 @@ class NetteTesterJunitParser {
@ -58954,8 +58964,8 @@ class TestReporter {
pathReplaceBackslashes = getInput('path-replace-backslashes', { required: false }) === 'true';
reporter = getInput('reporter', { required: true });
listSuites = getInput('list-suites', { required: true });
sortSuites = getInput('sort-suites', { required: false });
listTests = getInput('list-tests', { required: true });
listFiles = getInput('list-files', { required: true });
maxAnnotations = parseInt(getInput('max-annotations', { required: true }));
failOnError = getInput('fail-on-error', { required: true }) === 'true';
failOnEmpty = getInput('fail-on-empty', { required: true }) === 'true';
@ -58975,14 +58985,14 @@ class TestReporter {
setFailed(`Input parameter 'list-suites' has invalid value`);
return;
}
if (this.sortSuites !== 'name' && this.sortSuites !== 'time-desc') {
setFailed(`Input parameter 'sort-suites' has invalid value`);
return;
}
if (this.listTests !== 'all' && this.listTests !== 'failed' && this.listTests !== 'none') {
setFailed(`Input parameter 'list-tests' has invalid value`);
return;
}
if (this.listFiles !== 'all' && this.listFiles !== 'failed' && this.listFiles !== 'none') {
setFailed(`Input parameter 'list-files' has invalid value`);
return;
}
if (this.collapsed !== 'auto' && this.collapsed !== 'always' && this.collapsed !== 'never') {
setFailed(`Input parameter 'collapsed' has invalid value`);
return;
@ -59067,7 +59077,7 @@ class TestReporter {
throw error;
}
}
const { listSuites, sortSuites, listTests, slugPrefix, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed } = this;
const { listSuites, listTests, slugPrefix, listFiles, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed } = this;
const passed = results.reduce((sum, tr) => sum + tr.passed, 0);
const failed = results.reduce((sum, tr) => sum + tr.failed, 0);
const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0);
@ -59076,9 +59086,9 @@ class TestReporter {
if (this.useActionsSummary) {
const summary = getReport(results, {
listSuites,
sortSuites,
listTests,
slugPrefix,
listFiles,
baseUrl,
onlySummary,
useActionsSummary,
@ -59088,6 +59098,7 @@ class TestReporter {
}, shortSummary);
info('Summary content:');
info(summary);
this.writeSummaryFile(summary);
await summary_summary.addRaw(summary).write();
}
else {
@ -59106,9 +59117,9 @@ class TestReporter {
baseUrl = createResp.data.html_url;
const summary = getReport(results, {
listSuites,
sortSuites,
listTests,
slugPrefix,
listFiles,
baseUrl,
onlySummary,
useActionsSummary,
@ -59118,6 +59129,7 @@ class TestReporter {
});
info('Creating annotations');
const annotations = getAnnotations(results, this.maxAnnotations);
this.writeSummaryFile(summary);
const isFailed = this.failOnError && results.some(tr => tr.result === 'failed');
const conclusion = isFailed ? 'failure' : 'success';
info(`Updating check run conclusion (${conclusion}) and output`);
@ -59140,6 +59152,13 @@ class TestReporter {
}
return results;
}
writeSummaryFile(summary) {
const dir = process.env.RUNNER_TEMP || (0,external_node_os_namespaceObject.tmpdir)();
const file = (0,external_node_path_namespaceObject.join)(dir, `test-reporter-summary-${(0,external_node_crypto_.randomBytes)(8).toString('hex')}.md`);
(0,external_node_fs_namespaceObject.writeFileSync)(file, summary);
info(`Summary written to ${file}`);
setOutput('summary_file', file);
}
getParser(reporter, options) {
switch (reporter) {
case 'dart-json':

24
package-lock.json generated
View file

@ -35,7 +35,7 @@
"eslint-plugin-jest": "^29.15.2",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-junit": "^16.0.0",
"jest-junit": "^17.0.0",
"js-yaml": "^4.1.1",
"prettier": "^3.8.3",
"ts-jest": "^29.4.9",
@ -6110,19 +6110,19 @@
}
},
"node_modules/jest-junit": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz",
"integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==",
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-17.0.0.tgz",
"integrity": "sha512-RYWCkq4j59gUXj5DsgbIE7xFBZzu1gtibPhyjSjMmGaOTLnqlXhg7x9zuGCwgbCuMAyoyvk0Mi8wSrRR5uOeLA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"mkdirp": "^1.0.4",
"strip-ansi": "^6.0.1",
"uuid": "^8.3.2",
"uuid": "^14.0.0",
"xml": "^1.0.1"
},
"engines": {
"node": ">=10.12.0"
"node": ">=20.0.0"
}
},
"node_modules/jest-leak-detector": {
@ -8785,13 +8785,17 @@
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {

View file

@ -63,7 +63,7 @@
"eslint-plugin-jest": "^29.15.2",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-junit": "^16.0.0",
"jest-junit": "^17.0.0",
"js-yaml": "^4.1.1",
"prettier": "^3.8.3",
"ts-jest": "^29.4.9",

View file

@ -2,6 +2,9 @@ import * as core from '@actions/core'
import * as github from '@actions/github'
import {GitHub} from '@actions/github/lib/utils'
import {randomBytes} from 'node:crypto'
import {writeFileSync} from 'node:fs'
import {tmpdir} from 'node:os'
import {join} from 'node:path'
import {ArtifactProvider} from './input-providers/artifact-provider.js'
import {LocalFileProvider} from './input-providers/local-file-provider.js'
@ -43,8 +46,8 @@ class TestReporter {
readonly pathReplaceBackslashes = core.getInput('path-replace-backslashes', {required: false}) === 'true'
readonly reporter = core.getInput('reporter', {required: true})
readonly listSuites = core.getInput('list-suites', {required: true}) as 'all' | 'failed' | 'none'
readonly sortSuites = core.getInput('sort-suites', {required: false}) as 'name' | 'time-desc'
readonly listTests = core.getInput('list-tests', {required: true}) as 'all' | 'failed' | 'none'
readonly listFiles = core.getInput('list-files', {required: true}) as 'all' | 'failed' | 'none'
readonly maxAnnotations = parseInt(core.getInput('max-annotations', {required: true}))
readonly failOnError = core.getInput('fail-on-error', {required: true}) === 'true'
readonly failOnEmpty = core.getInput('fail-on-empty', {required: true}) === 'true'
@ -67,13 +70,13 @@ class TestReporter {
return
}
if (this.sortSuites !== 'name' && this.sortSuites !== 'time-desc') {
core.setFailed(`Input parameter 'sort-suites' has invalid value`)
if (this.listTests !== 'all' && this.listTests !== 'failed' && this.listTests !== 'none') {
core.setFailed(`Input parameter 'list-tests' has invalid value`)
return
}
if (this.listTests !== 'all' && this.listTests !== 'failed' && this.listTests !== 'none') {
core.setFailed(`Input parameter 'list-tests' has invalid value`)
if (this.listFiles !== 'all' && this.listFiles !== 'failed' && this.listFiles !== 'none') {
core.setFailed(`Input parameter 'list-files' has invalid value`)
return
}
@ -185,9 +188,9 @@ class TestReporter {
const {
listSuites,
sortSuites,
listTests,
slugPrefix,
listFiles,
onlySummary,
useActionsSummary,
badgeTitle,
@ -206,9 +209,9 @@ class TestReporter {
results,
{
listSuites,
sortSuites,
listTests,
slugPrefix,
listFiles,
baseUrl,
onlySummary,
useActionsSummary,
@ -221,6 +224,7 @@ class TestReporter {
core.info('Summary content:')
core.info(summary)
this.writeSummaryFile(summary)
await core.summary.addRaw(summary).write()
} else {
core.info(`Creating check run ${name}`)
@ -239,9 +243,9 @@ class TestReporter {
baseUrl = createResp.data.html_url as string
const summary = getReport(results, {
listSuites,
sortSuites,
listTests,
slugPrefix,
listFiles,
baseUrl,
onlySummary,
useActionsSummary,
@ -252,6 +256,7 @@ class TestReporter {
core.info('Creating annotations')
const annotations = getAnnotations(results, this.maxAnnotations)
this.writeSummaryFile(summary)
const isFailed = this.failOnError && results.some(tr => tr.result === 'failed')
const conclusion = isFailed ? 'failure' : 'success'
@ -278,6 +283,14 @@ class TestReporter {
return results
}
writeSummaryFile(summary: string): void {
const dir = process.env.RUNNER_TEMP || tmpdir()
const file = join(dir, `test-reporter-summary-${randomBytes(8).toString('hex')}.md`)
writeFileSync(file, summary)
core.info(`Summary written to ${file}`)
core.setOutput('summary_file', file)
}
getParser(reporter: string, options: ParseOptions): TestParser {
switch (reporter) {
case 'dart-json':

View file

@ -12,7 +12,7 @@ export interface ReportOptions {
listSuites: 'all' | 'failed' | 'none'
listTests: 'all' | 'failed' | 'none'
slugPrefix: string
sortSuites: 'name' | 'time-desc'
listFiles: 'all' | 'failed' | 'none'
baseUrl: string
onlySummary: boolean
useActionsSummary: boolean
@ -25,7 +25,7 @@ export const DEFAULT_OPTIONS: ReportOptions = {
listSuites: 'all',
listTests: 'all',
slugPrefix: '',
sortSuites: 'name',
listFiles: 'all',
baseUrl: '',
onlySummary: false,
useActionsSummary: true,
@ -39,7 +39,7 @@ export function getReport(
options: ReportOptions = DEFAULT_OPTIONS,
shortSummary = ''
): string {
applySort(results, options)
applySort(results)
const opts = {...options}
let lines = renderReport(results, opts, shortSummary)
@ -98,14 +98,10 @@ function trimReport(lines: string[], options: ReportOptions): string {
return reportLines.join('\n')
}
function applySort(results: TestRunResult[], options: ReportOptions): void {
function applySort(results: TestRunResult[]): void {
results.sort((a, b) => a.path.localeCompare(b.path, DEFAULT_LOCALE))
for (const res of results) {
if (options.sortSuites === 'time-desc') {
res.suites.sort((a, b) => b.time - a.time)
} else {
res.suites.sort((a, b) => a.name.localeCompare(b.name, DEFAULT_LOCALE))
}
res.suites.sort((a, b) => a.name.localeCompare(b.name, DEFAULT_LOCALE))
}
}
@ -179,21 +175,29 @@ function getTestRunsReport(testRuns: TestRunResult[], options: ReportOptions): s
sections.push(` `)
}
if (testRuns.length > 0 || options.onlySummary) {
const tableData = testRuns
.map((tr, originalIndex) => ({tr, originalIndex}))
.filter(({tr}) => tr.passed > 0 || tr.failed > 0 || tr.skipped > 0)
.map(({tr, originalIndex}) => {
const time = formatTime(tr.time)
const name = tr.path
const addr = options.baseUrl + makeRunSlug(originalIndex, options).link
const nameLink = link(name, addr)
const passed = tr.passed > 0 ? `${tr.passed} ${Icon.success}` : ''
const failed = tr.failed > 0 ? `${tr.failed} ${Icon.fail}` : ''
const skipped = tr.skipped > 0 ? `${tr.skipped} ${Icon.skip}` : ''
return [nameLink, passed, failed, skipped, time]
})
// Filter test runs based on list-files option
const filteredTestRuns =
options.listFiles === 'failed'
? testRuns.filter(tr => tr.result === 'failed')
: options.listFiles === 'none'
? []
: testRuns
const tableData = filteredTestRuns
.map((tr, originalIndex) => ({tr, originalIndex}))
.filter(({tr}) => tr.passed > 0 || tr.failed > 0 || tr.skipped > 0)
.map(({tr, originalIndex}) => {
const time = formatTime(tr.time)
const name = tr.path
const addr = options.baseUrl + makeRunSlug(originalIndex, options).link
const nameLink = link(name, addr)
const passed = tr.passed > 0 ? `${tr.passed} ${Icon.success}` : ''
const failed = tr.failed > 0 ? `${tr.failed} ${Icon.fail}` : ''
const skipped = tr.skipped > 0 ? `${tr.skipped} ${Icon.skip}` : ''
return [nameLink, passed, failed, skipped, time]
})
if (tableData.length > 0) {
const resultsTable = table(
['Report', 'Passed', 'Failed', 'Skipped', 'Time'],
[Align.Left, Align.Right, Align.Right, Align.Right, Align.Right],
@ -203,7 +207,7 @@ function getTestRunsReport(testRuns: TestRunResult[], options: ReportOptions): s
}
if (options.onlySummary === false) {
const suitesReports = testRuns.map((tr, i) => getSuitesReport(tr, i, options)).flat()
const suitesReports = filteredTestRuns.map((tr, i) => getSuitesReport(tr, i, options)).flat()
sections.push(...suitesReports)
}