Merge branch 'dorny:main'

This commit is contained in:
Ray Xu 2024-03-03 14:06:44 -08:00
commit 0b7d35fd12
46 changed files with 35637 additions and 15914 deletions

View file

@ -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[] = []

View file

@ -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}'`)
}

View file

@ -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)

View file

@ -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)

View 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}
}

View file

@ -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')
}
}

View file

@ -19,7 +19,7 @@ export interface TestSuite {
time: string
timestamp?: Date
}
testcase: TestCase[]
testcase?: TestCase[]
}
export interface TestCase {

View 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)
}
}

View file

@ -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)

View file

@ -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))
}