mirror of
https://github.com/dorny/test-reporter.git
synced 2025-12-16 06:17:10 +01:00
New report rendering with code blocks instead of tables
Previously we listed tests using markdown tables. Each test group had it's own table and textual preface saying how many tests were executed in what time. This was completely reworked - now tests are listed inside code block. Grouping is achieved using simple indentation. Duration of individual tests is no longer shown - it produced too much "noise" in the report. Pass/Fail check-mark was also moved before name of test suite. Behavior of "listTests" option was also changed - now if set to failed, it will list all tests, but only if suite is failed. Otherwise test listing is completely omitted. Last change affects report trimming - if report is still too big after "listTests" is set to "failed" - it will trim report to fit max size and add informational message at the end.
This commit is contained in:
parent
96df6db61e
commit
690ec77880
13 changed files with 3292 additions and 1046 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import {ellipsis, fixEol} from '../utils/markdown-utils'
|
||||
import {TestRunResult} from '../test-results'
|
||||
import {getFirstNonEmptyLine} from '../utils/parse-utils'
|
||||
|
||||
type Annotation = {
|
||||
path: string
|
||||
|
|
@ -98,11 +99,6 @@ function enforceCheckRunLimits(err: Annotation): Annotation {
|
|||
return err
|
||||
}
|
||||
|
||||
function getFirstNonEmptyLine(stackTrace: string): string | undefined {
|
||||
const lines = stackTrace.split(/\r?\n/g)
|
||||
return lines.find(str => !/^\s*$/.test(str))
|
||||
}
|
||||
|
||||
function ident(text: string, prefix: string): string {
|
||||
return text
|
||||
.split(/\n/g)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import * as core from '@actions/core'
|
||||
import {TestExecutionResult, TestRunResult, TestSuiteResult} from '../test-results'
|
||||
import {Align, formatTime, Icon, link, table} from '../utils/markdown-utils'
|
||||
import {getFirstNonEmptyLine} from '../utils/parse-utils'
|
||||
import {slug} from '../utils/slugger'
|
||||
|
||||
const MAX_REPORT_LENGTH = 65535
|
||||
|
||||
export interface ReportOptions {
|
||||
listSuites: 'all' | 'failed'
|
||||
listTests: 'all' | 'failed' | 'none'
|
||||
|
|
@ -16,46 +19,59 @@ const defaultOptions: ReportOptions = {
|
|||
export function getReport(results: TestRunResult[], options: ReportOptions = defaultOptions): string {
|
||||
core.info('Generating check run summary')
|
||||
|
||||
const maxReportLength = 65535
|
||||
applySort(results)
|
||||
|
||||
const opts = {...options}
|
||||
let report = renderReport(results, opts)
|
||||
if (getByteLength(report) <= maxReportLength) {
|
||||
let lines = renderReport(results, opts)
|
||||
let report = lines.join('\n')
|
||||
|
||||
if (getByteLength(report) <= MAX_REPORT_LENGTH) {
|
||||
return report
|
||||
}
|
||||
|
||||
if (opts.listTests === 'all') {
|
||||
core.info("Test report summary is too big - setting 'listTests' to 'failed'")
|
||||
opts.listTests = 'failed'
|
||||
report = renderReport(results, opts)
|
||||
if (getByteLength(report) <= maxReportLength) {
|
||||
lines = renderReport(results, opts)
|
||||
report = lines.join('\n')
|
||||
if (getByteLength(report) <= MAX_REPORT_LENGTH) {
|
||||
return report
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.listSuites === 'all') {
|
||||
core.info("Test report summary is too big - setting 'listSuites' to 'failed'")
|
||||
opts.listSuites = 'failed'
|
||||
report = renderReport(results, opts)
|
||||
if (getByteLength(report) <= maxReportLength) {
|
||||
return report
|
||||
core.warning(`Test report summary exceeded limit of ${MAX_REPORT_LENGTH} bytes and will be trimmed`)
|
||||
return trimReport(lines)
|
||||
}
|
||||
|
||||
function trimReport(lines: string[]): string {
|
||||
const closingBlock = '```'
|
||||
const errorMsg = `**Report exceeded GitHub limit of ${MAX_REPORT_LENGTH} bytes and has been trimmed**`
|
||||
const maxErrorMsgLength = closingBlock.length + errorMsg.length + 2
|
||||
const maxReportLength = MAX_REPORT_LENGTH - maxErrorMsgLength
|
||||
|
||||
let reportLength = 0
|
||||
let codeBlock = false
|
||||
let endLineIndex = 0
|
||||
for (endLineIndex = 0; endLineIndex < lines.length; endLineIndex++) {
|
||||
const line = lines[endLineIndex]
|
||||
const lineLength = getByteLength(line)
|
||||
|
||||
reportLength += lineLength + 1
|
||||
if (reportLength > maxReportLength) {
|
||||
break
|
||||
}
|
||||
|
||||
if (line === '```') {
|
||||
codeBlock = !codeBlock
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.listTests !== 'none') {
|
||||
core.info("Test report summary is too big - setting 'listTests' to 'none'")
|
||||
opts.listTests = 'none'
|
||||
report = renderReport(results, opts)
|
||||
if (getByteLength(report) <= maxReportLength) {
|
||||
return report
|
||||
}
|
||||
const reportLines = lines.slice(0, endLineIndex)
|
||||
if (codeBlock) {
|
||||
reportLines.push('```')
|
||||
}
|
||||
|
||||
core.warning(`Test report summary exceeded limit of ${maxReportLength} bytes`)
|
||||
const badge = getReportBadge(results)
|
||||
const msg = `**Test report summary exceeded limit of ${maxReportLength} bytes and was removed**`
|
||||
return `${badge}\n${msg}`
|
||||
reportLines.push(errorMsg)
|
||||
return reportLines.join('\n')
|
||||
}
|
||||
|
||||
function applySort(results: TestRunResult[]): void {
|
||||
|
|
@ -69,7 +85,7 @@ function getByteLength(text: string): number {
|
|||
return Buffer.byteLength(text, 'utf8')
|
||||
}
|
||||
|
||||
function renderReport(results: TestRunResult[], options: ReportOptions): string {
|
||||
function renderReport(results: TestRunResult[], options: ReportOptions): string[] {
|
||||
const sections: string[] = []
|
||||
const badge = getReportBadge(results)
|
||||
sections.push(badge)
|
||||
|
|
@ -77,7 +93,7 @@ function renderReport(results: TestRunResult[], options: ReportOptions): string
|
|||
const runs = getTestRunsReport(results, options)
|
||||
sections.push(...runs)
|
||||
|
||||
return sections.join('\n')
|
||||
return sections
|
||||
}
|
||||
|
||||
function getReportBadge(results: TestRunResult[]): string {
|
||||
|
|
@ -145,7 +161,7 @@ function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOpt
|
|||
const trSlug = makeRunSlug(runIndex)
|
||||
const nameLink = `<a id="${trSlug.id}" href="${trSlug.link}">${tr.path}</a>`
|
||||
const icon = getResultIcon(tr.result)
|
||||
sections.push(`## ${nameLink} ${icon}`)
|
||||
sections.push(`## ${icon} ${nameLink}`)
|
||||
|
||||
const time = formatTime(tr.time)
|
||||
const headingLine2 =
|
||||
|
|
@ -186,7 +202,10 @@ function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOpt
|
|||
}
|
||||
|
||||
function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: number, options: ReportOptions): string[] {
|
||||
const groups = options.listTests === 'failed' ? ts.failedGroups : ts.groups
|
||||
if (options.listTests === 'failed' && ts.result !== 'failed') {
|
||||
return []
|
||||
}
|
||||
const groups = ts.groups
|
||||
if (groups.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
|
@ -197,31 +216,28 @@ function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: numbe
|
|||
const tsSlug = makeSuiteSlug(runIndex, suiteIndex)
|
||||
const tsNameLink = `<a id="${tsSlug.id}" href="${tsSlug.link}">${tsName}</a>`
|
||||
const icon = getResultIcon(ts.result)
|
||||
sections.push(`### ${tsNameLink} ${icon}`)
|
||||
|
||||
const tsTime = formatTime(ts.time)
|
||||
const headingLine2 = `**${ts.tests}** tests were completed in **${tsTime}** with **${ts.passed}** passed, **${ts.failed}** failed and **${ts.skipped}** skipped.`
|
||||
sections.push(headingLine2)
|
||||
sections.push(`### ${icon} ${tsNameLink}`)
|
||||
|
||||
sections.push('```')
|
||||
for (const grp of groups) {
|
||||
const tests = options.listTests === 'failed' ? grp.failedTests : grp.tests
|
||||
if (tests.length === 0) {
|
||||
continue
|
||||
if (grp.name) {
|
||||
sections.push(grp.name)
|
||||
}
|
||||
const space = grp.name ? ' ' : ''
|
||||
for (const tc of grp.tests) {
|
||||
const result = getResultIcon(tc.result)
|
||||
sections.push(`${space}${result} ${tc.name}`)
|
||||
if (tc.error) {
|
||||
const lines = (tc.error.message ?? getFirstNonEmptyLine(tc.error.details)?.trim())
|
||||
?.split(/\r?\n/g)
|
||||
.map(l => '\t' + l)
|
||||
if (lines) {
|
||||
sections.push(...lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
const grpHeader = grp.name ? `\n**${grp.name}**` : ''
|
||||
const testsTable = table(
|
||||
['Result', 'Test', 'Time'],
|
||||
[Align.Center, Align.Left, Align.Right],
|
||||
...grp.tests.map(tc => {
|
||||
const name = tc.name
|
||||
const time = formatTime(tc.time)
|
||||
const result = getResultIcon(tc.result)
|
||||
return [result, name, time]
|
||||
})
|
||||
)
|
||||
|
||||
sections.push(grpHeader, testsTable)
|
||||
}
|
||||
sections.push('```')
|
||||
|
||||
return sections
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,3 +17,8 @@ export function parseIsoDate(str: string): Date {
|
|||
|
||||
return new Date(str)
|
||||
}
|
||||
|
||||
export function getFirstNonEmptyLine(stackTrace: string): string | undefined {
|
||||
const lines = stackTrace.split(/\r?\n/g)
|
||||
return lines.find(str => !/^\s*$/.test(str))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue