Refactoring & cleanup of whole codebase

Improves report summary and annotations
This commit is contained in:
Michal Dorner 2021-01-31 20:47:55 +01:00
parent 07a0223ee3
commit 60b35d601a
No known key found for this signature in database
GPG key ID: 9EEE04B48DA36786
20 changed files with 38784 additions and 33667 deletions

View file

@ -0,0 +1,111 @@
import {ellipsis, fixEol} from '../utils/markdown-utils'
import {TestRunResult} from '../test-results'
type Annotation = {
path: string
start_line: number
end_line: number
start_column?: number
end_column?: number
annotation_level: 'notice' | 'warning' | 'failure'
message: string
title?: string
raw_details?: string
}
interface TestError {
testRunPaths: string[]
suiteName: string
testName: string
path: string
line: number
stackTrace: string
message: string
}
export function getAnnotations(results: TestRunResult[], maxCount: number): Annotation[] {
if (maxCount === 0) {
return []
}
// Collect errors from TestRunResults
// Merge duplicates if there are more test results files processed
const errors: TestError[] = []
const mergeDup = results.length > 1
for (const tr of results) {
for (const ts of tr.suites) {
for (const tg of ts.groups) {
for (const tc of tg.tests) {
const err = tc.error
if (err === undefined) {
continue
}
const path = err.path ?? tr.path
const line = err.line ?? 0
if (mergeDup) {
const dup = errors.find(e => path === e.path && line === e.line && err.stackTrace === e.stackTrace)
if (dup !== undefined) {
dup.testRunPaths.push(tr.path)
continue
}
}
errors.push({
testRunPaths: [tr.path],
suiteName: ts.name,
testName: tc.name,
stackTrace: err.stackTrace,
message: err.message ?? getFirstNonEmptyLine(err.stackTrace) ?? 'Test failed',
path,
line
})
}
}
}
}
// Limit number of created annotations
errors.splice(maxCount + 1)
const annotations = errors.map(e => {
const message = [
'Failed test found in:',
e.testRunPaths.map(p => ` ${p}`).join('\n'),
'Error:',
ident(fixEol(e.message), ' ')
].join('\n')
return enforceCheckRunLimits({
path: e.path,
start_line: e.line,
end_line: e.line,
annotation_level: 'failure',
title: `${e.suiteName}${e.testName}`,
raw_details: fixEol(e.stackTrace),
message
})
})
return annotations
}
function enforceCheckRunLimits(err: Annotation): Annotation {
err.title = ellipsis(err.title || '', 255)
err.message = ellipsis(err.message, 65535)
if (err.raw_details) {
err.raw_details = ellipsis(err.raw_details, 65535)
}
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)
.map(line => prefix + line)
.join('\n')
}

View file

@ -1,5 +1,5 @@
import * as core from '@actions/core'
import {TestExecutionResult, TestRunResult, TestSuiteResult} from './test-results'
import {TestExecutionResult, TestRunResult, TestSuiteResult} from '../test-results'
import {Align, formatTime, Icon, link, table} from '../utils/markdown-utils'
import {slug} from '../utils/slugger'
@ -112,7 +112,7 @@ function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOpt
const icon = getResultIcon(tr.result)
sections.push(`## ${nameLink} ${icon}`)
const time = `${(tr.time / 1000).toFixed(3)}s`
const time = formatTime(tr.time)
const headingLine2 = `**${tr.tests}** tests were completed in **${time}** with **${tr.passed}** passed, **${tr.failed}** failed and **${tr.skipped}** skipped.`
sections.push(headingLine2)
@ -161,7 +161,8 @@ function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: numbe
const icon = getResultIcon(ts.result)
sections.push(`### ${tsNameLink} ${icon}`)
const headingLine2 = `**${ts.tests}** tests were completed in **${ts.time}ms** with **${ts.passed}** passed, **${ts.failed}** failed and **${ts.skipped}** skipped.`
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)
for (const grp of groups) {

View file

@ -1,89 +0,0 @@
export class TestRunResult {
constructor(readonly path: string, readonly suites: TestSuiteResult[], private totalTime?: number) {}
get tests(): number {
return this.suites.reduce((sum, g) => sum + g.tests, 0)
}
get passed(): number {
return this.suites.reduce((sum, g) => sum + g.passed, 0)
}
get failed(): number {
return this.suites.reduce((sum, g) => sum + g.failed, 0)
}
get skipped(): number {
return this.suites.reduce((sum, g) => sum + g.skipped, 0)
}
get time(): number {
return this.totalTime ?? this.suites.reduce((sum, g) => sum + g.time, 0)
}
get result(): TestExecutionResult {
return this.suites.some(t => t.result === 'failed') ? 'failed' : 'success'
}
get failedSuites(): TestSuiteResult[] {
return this.suites.filter(s => s.result === 'failed')
}
}
export class TestSuiteResult {
constructor(readonly name: string, readonly groups: TestGroupResult[], private totalTime?: number) {}
get tests(): number {
return this.groups.reduce((sum, g) => sum + g.tests.length, 0)
}
get passed(): number {
return this.groups.reduce((sum, g) => sum + g.passed, 0)
}
get failed(): number {
return this.groups.reduce((sum, g) => sum + g.failed, 0)
}
get skipped(): number {
return this.groups.reduce((sum, g) => sum + g.skipped, 0)
}
get time(): number {
return this.totalTime ?? this.groups.reduce((sum, g) => sum + g.time, 0)
}
get result(): TestExecutionResult {
return this.groups.some(t => t.result === 'failed') ? 'failed' : 'success'
}
get failedGroups(): TestGroupResult[] {
return this.groups.filter(grp => grp.result === 'failed')
}
}
export class TestGroupResult {
constructor(readonly name: string | undefined | null, readonly tests: TestCaseResult[]) {}
get passed(): number {
return this.tests.reduce((sum, t) => (t.result === 'success' ? sum + 1 : sum), 0)
}
get failed(): number {
return this.tests.reduce((sum, t) => (t.result === 'failed' ? sum + 1 : sum), 0)
}
get skipped(): number {
return this.tests.reduce((sum, t) => (t.result === 'skipped' ? sum + 1 : sum), 0)
}
get time(): number {
return this.tests.reduce((sum, t) => sum + t.time, 0)
}
get result(): TestExecutionResult {
return this.tests.some(t => t.result === 'failed') ? 'failed' : 'success'
}
get failedTests(): TestCaseResult[] {
return this.tests.filter(tc => tc.result === 'failed')
}
}
export class TestCaseResult {
constructor(readonly name: string, readonly result: TestExecutionResult, readonly time: number) {}
}
export type TestExecutionResult = 'success' | 'skipped' | 'failed' | undefined