mirror of
https://github.com/dorny/test-reporter.git
synced 2025-12-15 13:57:09 +01:00
Merge branch 'dorny:main'
This commit is contained in:
commit
0b7d35fd12
46 changed files with 35637 additions and 15914 deletions
|
|
@ -4,7 +4,10 @@ import {FileContent, InputProvider, ReportInput} from './input-provider'
|
|||
import {listFiles} from '../utils/git'
|
||||
|
||||
export class LocalFileProvider implements InputProvider {
|
||||
constructor(readonly name: string, readonly pattern: string[]) {}
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly pattern: string[]
|
||||
) {}
|
||||
|
||||
async load(): Promise<ReportInput> {
|
||||
const result: FileContent[] = []
|
||||
|
|
|
|||
65
src/main.ts
65
src/main.ts
|
|
@ -16,10 +16,10 @@ import {DotnetTrxParser} from './parsers/dotnet-trx/dotnet-trx-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 {SwiftXunitParser} from './parsers/swift-xunit/swift-xunit-parser'
|
||||
|
||||
import {normalizeDirPath, normalizeFilePath} from './utils/path-utils'
|
||||
import {getCheckRunContext} from './utils/github-utils'
|
||||
import {Icon} from './utils/markdown-utils'
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
|
|
@ -41,6 +41,7 @@ class TestReporter {
|
|||
readonly listTests = core.getInput('list-tests', {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'
|
||||
readonly workDirInput = core.getInput('working-directory', {required: false})
|
||||
readonly onlySummary = core.getInput('only-summary', {required: false}) === 'true'
|
||||
readonly useActionsSummary = core.getInput('use-actions-summary', {required: false}) === 'true'
|
||||
|
|
@ -94,10 +95,10 @@ class TestReporter {
|
|||
: new LocalFileProvider(this.name, pattern)
|
||||
|
||||
const parseErrors = this.maxAnnotations > 0
|
||||
const trackedFiles = await inputProvider.listTrackedFiles()
|
||||
const trackedFiles = parseErrors ? await inputProvider.listTrackedFiles() : []
|
||||
const workDir = this.artifact ? undefined : normalizeDirPath(process.cwd(), true)
|
||||
|
||||
core.info(`Found ${trackedFiles.length} files tracked by GitHub`)
|
||||
if (parseErrors) core.info(`Found ${trackedFiles.length} files tracked by GitHub`)
|
||||
|
||||
const options: ParseOptions = {
|
||||
workDir,
|
||||
|
|
@ -138,7 +139,7 @@ class TestReporter {
|
|||
return
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
if (results.length === 0 && this.failOnEmpty) {
|
||||
core.setFailed(`No test report files were found`)
|
||||
return
|
||||
}
|
||||
|
|
@ -150,18 +151,30 @@ class TestReporter {
|
|||
return []
|
||||
}
|
||||
|
||||
core.info(`Processing test results for check run ${name}`)
|
||||
const results: TestRunResult[] = []
|
||||
for (const {file, content} of files) {
|
||||
core.info(`Processing test results from ${file}`)
|
||||
const tr = await parser.parse(file, content)
|
||||
results.push(tr)
|
||||
try {
|
||||
const tr = await parser.parse(file, content)
|
||||
results.push(tr)
|
||||
} catch (error) {
|
||||
core.error(`Processing test results from ${file} failed`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
core.info('Creating report summary')
|
||||
const {listSuites, listTests, onlySummary, useActionsSummary, badgeTitle} = this
|
||||
|
||||
let baseUrl = ''
|
||||
let checkRunId = 0
|
||||
if (!this.useActionsSummary) {
|
||||
if (this.useActionsSummary) {
|
||||
const summary = getReport(results, {listSuites, listTests, baseUrl, onlySummary, useActionsSummary, badgeTitle})
|
||||
|
||||
core.info('Summary content:')
|
||||
core.info(summary)
|
||||
await fs.promises.writeFile(this.path.replace('*.trx', 'test-summary.md'), summary)
|
||||
core.info('File content:')
|
||||
core.info(fs.readFileSync(this.path.replace('*.trx', 'test-summary.md'), 'utf8'))
|
||||
} else {
|
||||
core.info(`Creating check run ${name}`)
|
||||
const createResp = await this.octokit.rest.checks.create({
|
||||
head_sha: this.context.sha,
|
||||
|
|
@ -173,35 +186,29 @@ class TestReporter {
|
|||
},
|
||||
...github.context.repo
|
||||
})
|
||||
|
||||
core.info('Creating report summary')
|
||||
baseUrl = createResp.data.html_url as string
|
||||
checkRunId = createResp.data.id
|
||||
}
|
||||
const summary = getReport(results, {listSuites, listTests, baseUrl, onlySummary, useActionsSummary, badgeTitle})
|
||||
|
||||
const summary = getReport(results, {listSuites, listTests, baseUrl, onlySummary, useActionsSummary, badgeTitle})
|
||||
|
||||
if (this.useActionsSummary) {
|
||||
core.info('Summary content:')
|
||||
core.info(summary)
|
||||
await fs.promises.writeFile(this.path.replace('*.trx', 'test-summary.md'), summary)
|
||||
core.info('File content:')
|
||||
core.info(fs.readFileSync(this.path.replace('*.trx', 'test-summary.md'), 'utf8'))
|
||||
}
|
||||
|
||||
if (!this.useActionsSummary) {
|
||||
core.info('Creating annotations')
|
||||
const annotations = getAnnotations(results, this.maxAnnotations)
|
||||
|
||||
const isFailed = results.some(tr => tr.result === 'failed')
|
||||
const isFailed = this.failOnError && results.some(tr => tr.result === 'failed')
|
||||
const conclusion = isFailed ? 'failure' : 'success'
|
||||
const icon = isFailed ? Icon.fail : Icon.success
|
||||
|
||||
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)
|
||||
const shortSummary = `${passed} passed, ${failed} failed and ${skipped} skipped `
|
||||
|
||||
core.info(`Updating check run conclusion (${conclusion}) and output`)
|
||||
const resp = await this.octokit.rest.checks.update({
|
||||
check_run_id: checkRunId,
|
||||
check_run_id: createResp.data.id,
|
||||
conclusion,
|
||||
status: 'completed',
|
||||
output: {
|
||||
title: `${name} ${icon}`,
|
||||
title: shortSummary,
|
||||
summary,
|
||||
annotations
|
||||
},
|
||||
|
|
@ -210,6 +217,8 @@ class TestReporter {
|
|||
core.info(`Check run create response: ${resp.status}`)
|
||||
core.info(`Check run URL: ${resp.data.url}`)
|
||||
core.info(`Check run HTML: ${resp.data.html_url}`)
|
||||
core.setOutput('url', resp.data.url)
|
||||
core.setOutput('url_html', resp.data.html_url)
|
||||
}
|
||||
|
||||
return results
|
||||
|
|
@ -229,6 +238,8 @@ class TestReporter {
|
|||
return new JestJunitParser(options)
|
||||
case 'mocha-json':
|
||||
return new MochaJsonParser(options)
|
||||
case 'swift-xunit':
|
||||
return new SwiftXunitParser(options)
|
||||
default:
|
||||
throw new Error(`Input variable 'reporter' is set to invalid value '${reporter}'`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,12 @@ import {
|
|||
} from '../../test-results'
|
||||
|
||||
class TestRun {
|
||||
constructor(readonly path: string, readonly suites: TestSuite[], readonly success: boolean, readonly time: number) {}
|
||||
constructor(
|
||||
readonly path: string,
|
||||
readonly suites: TestSuite[],
|
||||
readonly success: boolean,
|
||||
readonly time: number
|
||||
) {}
|
||||
}
|
||||
|
||||
class TestSuite {
|
||||
|
|
@ -74,7 +79,10 @@ class TestCase {
|
|||
export class DartJsonParser implements TestParser {
|
||||
assumedWorkDir: string | undefined
|
||||
|
||||
constructor(readonly options: ParseOptions, readonly sdk: 'dart' | 'flutter') {}
|
||||
constructor(
|
||||
readonly options: ParseOptions,
|
||||
readonly sdk: 'dart' | 'flutter'
|
||||
) {}
|
||||
|
||||
async parse(path: string, content: string): Promise<TestRunResult> {
|
||||
const tr = this.getTestRun(path, content)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {ParseOptions, TestParser} from '../../test-parser'
|
|||
import {parseStringPromise} from 'xml2js'
|
||||
|
||||
import {JunitReport, SingleSuiteReport, TestCase, TestSuite} from './java-junit-types'
|
||||
import {parseStackTraceElement} from './java-stack-trace-element-parser'
|
||||
import {normalizeFilePath} from '../../utils/path-utils'
|
||||
|
||||
import {
|
||||
|
|
@ -128,10 +129,12 @@ export class JavaJunitParser implements TestParser {
|
|||
let filePath
|
||||
let line
|
||||
|
||||
const src = this.exceptionThrowSource(details)
|
||||
if (src) {
|
||||
filePath = src.filePath
|
||||
line = src.line
|
||||
if (details != null) {
|
||||
const src = this.exceptionThrowSource(details)
|
||||
if (src) {
|
||||
filePath = src.filePath
|
||||
line = src.line
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -144,12 +147,11 @@ export class JavaJunitParser implements TestParser {
|
|||
|
||||
private exceptionThrowSource(stackTrace: string): {filePath: string; line: number} | undefined {
|
||||
const lines = stackTrace.split(/\r?\n/)
|
||||
const re = /^at (.*)\((.*):(\d+)\)$/
|
||||
|
||||
for (const str of lines) {
|
||||
const match = str.match(re)
|
||||
if (match !== null) {
|
||||
const [_, tracePath, fileName, lineStr] = match
|
||||
const stackTraceElement = parseStackTraceElement(str)
|
||||
if (stackTraceElement) {
|
||||
const {tracePath, fileName, lineStr} = stackTraceElement
|
||||
const filePath = this.getFilePath(tracePath, fileName)
|
||||
if (filePath !== undefined) {
|
||||
const line = parseInt(lineStr)
|
||||
|
|
|
|||
44
src/parsers/java-junit/java-stack-trace-element-parser.ts
Normal file
44
src/parsers/java-junit/java-stack-trace-element-parser.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
export interface StackTraceElement {
|
||||
classLoader: string | undefined
|
||||
moduleNameAndVersion: string | undefined
|
||||
tracePath: string
|
||||
fileName: string
|
||||
lineStr: string
|
||||
}
|
||||
|
||||
// classloader and module name are optional:
|
||||
// at <CLASSLOADER>/<MODULE_NAME_AND_VERSION>/<FULLY_QUALIFIED_METHOD_NAME>(<FILE_NAME>:<LINE_NUMBER>)
|
||||
// https://github.com/eclipse-openj9/openj9/issues/11452#issuecomment-754946992
|
||||
const re = /^\s*at (\S+\/\S*\/)?(.*)\((.*):(\d+)\)$/
|
||||
|
||||
export function parseStackTraceElement(stackTraceLine: string): StackTraceElement | undefined {
|
||||
const match = stackTraceLine.match(re)
|
||||
if (match !== null) {
|
||||
const [_, maybeClassLoaderAndModuleNameAndVersion, tracePath, fileName, lineStr] = match
|
||||
const {classLoader, moduleNameAndVersion} = parseClassLoaderAndModule(maybeClassLoaderAndModuleNameAndVersion)
|
||||
return {
|
||||
classLoader,
|
||||
moduleNameAndVersion,
|
||||
tracePath,
|
||||
fileName,
|
||||
lineStr
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function parseClassLoaderAndModule(maybeClassLoaderAndModuleNameAndVersion?: string): {
|
||||
classLoader?: string
|
||||
moduleNameAndVersion?: string
|
||||
} {
|
||||
if (maybeClassLoaderAndModuleNameAndVersion) {
|
||||
const res = maybeClassLoaderAndModuleNameAndVersion.split('/')
|
||||
const classLoader = res[0]
|
||||
let moduleNameAndVersion: string | undefined = res[1]
|
||||
if (moduleNameAndVersion === '') {
|
||||
moduleNameAndVersion = undefined
|
||||
}
|
||||
return {classLoader, moduleNameAndVersion}
|
||||
}
|
||||
return {classLoader: undefined, moduleNameAndVersion: undefined}
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ export class JestJunitParser implements TestParser {
|
|||
junit.testsuites.testsuite === undefined
|
||||
? []
|
||||
: junit.testsuites.testsuite.map(ts => {
|
||||
const name = ts.$.name.trim()
|
||||
const name = this.escapeCharacters(ts.$.name.trim())
|
||||
const time = parseFloat(ts.$.time) * 1000
|
||||
const sr = new TestSuiteResult(name, this.getGroups(ts), time)
|
||||
return sr
|
||||
|
|
@ -48,6 +48,10 @@ export class JestJunitParser implements TestParser {
|
|||
}
|
||||
|
||||
private getGroups(suite: TestSuite): TestGroupResult[] {
|
||||
if (!suite.testcase) {
|
||||
return []
|
||||
}
|
||||
|
||||
const groups: {describe: string; tests: TestCase[]}[] = []
|
||||
for (const tc of suite.testcase) {
|
||||
let grp = groups.find(g => g.describe === tc.$.classname)
|
||||
|
|
@ -114,4 +118,8 @@ export class JestJunitParser implements TestParser {
|
|||
(this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
|
||||
)
|
||||
}
|
||||
|
||||
private escapeCharacters(s: string): string {
|
||||
return s.replace(/([<>])/g, '\\$1')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export interface TestSuite {
|
|||
time: string
|
||||
timestamp?: Date
|
||||
}
|
||||
testcase: TestCase[]
|
||||
testcase?: TestCase[]
|
||||
}
|
||||
|
||||
export interface TestCase {
|
||||
|
|
|
|||
8
src/parsers/swift-xunit/swift-xunit-parser.ts
Normal file
8
src/parsers/swift-xunit/swift-xunit-parser.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import {ParseOptions} from '../../test-parser'
|
||||
import {JavaJunitParser} from '../java-junit/java-junit-parser'
|
||||
|
||||
export class SwiftXunitParser extends JavaJunitParser {
|
||||
constructor(readonly options: ParseOptions) {
|
||||
super(options)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
import {DEFAULT_LOCALE} from './utils/node-utils'
|
||||
|
||||
export class TestRunResult {
|
||||
constructor(readonly path: string, readonly suites: TestSuiteResult[], private totalTime?: number) {}
|
||||
constructor(
|
||||
readonly path: string,
|
||||
readonly suites: TestSuiteResult[],
|
||||
private totalTime?: number
|
||||
) {}
|
||||
|
||||
get tests(): number {
|
||||
return this.suites.reduce((sum, g) => sum + g.tests, 0)
|
||||
|
|
@ -40,7 +44,11 @@ export class TestRunResult {
|
|||
}
|
||||
|
||||
export class TestSuiteResult {
|
||||
constructor(readonly name: string, readonly groups: TestGroupResult[], private totalTime?: number) {}
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly groups: TestGroupResult[],
|
||||
private totalTime?: number
|
||||
) {}
|
||||
|
||||
get tests(): number {
|
||||
return this.groups.reduce((sum, g) => sum + g.tests.length, 0)
|
||||
|
|
@ -78,7 +86,10 @@ export class TestSuiteResult {
|
|||
}
|
||||
|
||||
export class TestGroupResult {
|
||||
constructor(readonly name: string | undefined | null, readonly tests: TestCaseResult[]) {}
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@ export function parseIsoDate(str: string): Date {
|
|||
}
|
||||
|
||||
export function getFirstNonEmptyLine(stackTrace: string): string | undefined {
|
||||
const lines = stackTrace.split(/\r?\n/g)
|
||||
return lines.find(str => !/^\s*$/.test(str))
|
||||
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