mirror of
https://github.com/dorny/test-reporter.git
synced 2025-12-17 14:57:09 +01:00
Update packages, cleanup (#8)
This commit is contained in:
parent
0443893a99
commit
5aea6ccd17
65 changed files with 7991 additions and 97405 deletions
21
src/main.ts
21
src/main.ts
|
|
@ -9,11 +9,7 @@ import {TestRunResult, TestRunResultWithUrl} from './test-results'
|
|||
import {getAnnotations} from './report/get-annotations'
|
||||
import {getReport} from './report/get-report'
|
||||
|
||||
import {DartJsonParser} from './parsers/dart-json/dart-json-parser'
|
||||
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 {normalizeDirPath, normalizeFilePath} from './utils/path-utils'
|
||||
import {getCheckRunContext} from './utils/github-utils'
|
||||
|
|
@ -370,22 +366,7 @@ class TestReporter {
|
|||
}
|
||||
|
||||
getParser(reporter: string, options: ParseOptions): TestParser {
|
||||
switch (reporter) {
|
||||
case 'dart-json':
|
||||
return new DartJsonParser(options, 'dart')
|
||||
case 'dotnet-trx':
|
||||
return new DotnetTrxParser(options)
|
||||
case 'flutter-json':
|
||||
return new DartJsonParser(options, 'flutter')
|
||||
case 'java-junit':
|
||||
return new JavaJunitParser(options)
|
||||
case 'jest-junit':
|
||||
return new JestJunitParser(options)
|
||||
case 'mocha-json':
|
||||
return new MochaJsonParser(options)
|
||||
default:
|
||||
throw new Error(`Input variable 'reporter' is set to invalid value '${reporter}'`)
|
||||
}
|
||||
return new DotnetTrxParser(options)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,261 +0,0 @@
|
|||
import {ParseOptions, TestParser} from '../../test-parser'
|
||||
|
||||
import {getBasePath, normalizeFilePath} from '../../utils/path-utils'
|
||||
|
||||
import {
|
||||
ReportEvent,
|
||||
Suite,
|
||||
Group,
|
||||
TestStartEvent,
|
||||
TestDoneEvent,
|
||||
ErrorEvent,
|
||||
isSuiteEvent,
|
||||
isGroupEvent,
|
||||
isTestStartEvent,
|
||||
isTestDoneEvent,
|
||||
isErrorEvent,
|
||||
isDoneEvent,
|
||||
isMessageEvent,
|
||||
MessageEvent
|
||||
} from './dart-json-types'
|
||||
|
||||
import {
|
||||
TestExecutionResult,
|
||||
TestRunResult,
|
||||
TestSuiteResult,
|
||||
TestGroupResult,
|
||||
TestCaseResult,
|
||||
TestCaseError
|
||||
} from '../../test-results'
|
||||
|
||||
class TestRun {
|
||||
constructor(
|
||||
readonly path: string,
|
||||
readonly suites: TestSuite[],
|
||||
readonly success: boolean,
|
||||
readonly time: number
|
||||
) {}
|
||||
}
|
||||
|
||||
class TestSuite {
|
||||
constructor(readonly suite: Suite) {}
|
||||
readonly groups: {[id: number]: TestGroup} = {}
|
||||
}
|
||||
|
||||
class TestGroup {
|
||||
constructor(readonly group: Group) {}
|
||||
readonly tests: TestCase[] = []
|
||||
}
|
||||
|
||||
class TestCase {
|
||||
constructor(readonly testStart: TestStartEvent) {
|
||||
this.groupId = testStart.test.groupIDs[testStart.test.groupIDs.length - 1]
|
||||
}
|
||||
readonly groupId: number
|
||||
readonly print: MessageEvent[] = []
|
||||
testDone?: TestDoneEvent
|
||||
error?: ErrorEvent
|
||||
|
||||
get result(): TestExecutionResult {
|
||||
if (this.testDone?.skipped) {
|
||||
return 'skipped'
|
||||
}
|
||||
if (this.testDone?.result === 'success') {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (this.testDone?.result === 'error' || this.testDone?.result === 'failure') {
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
get time(): number {
|
||||
return this.testDone !== undefined ? this.testDone.time - this.testStart.time : 0
|
||||
}
|
||||
}
|
||||
|
||||
export class DartJsonParser implements TestParser {
|
||||
assumedWorkDir: string | undefined
|
||||
|
||||
constructor(
|
||||
readonly options: ParseOptions,
|
||||
readonly sdk: 'dart' | 'flutter'
|
||||
) {}
|
||||
|
||||
async parse(path: string, content: string): Promise<TestRunResult> {
|
||||
const tr = this.getTestRun(path, content)
|
||||
const result = this.getTestRunResult(tr)
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
private getTestRun(path: string, content: string): TestRun {
|
||||
const lines = content.split(/\n\r?/g)
|
||||
const events = lines
|
||||
.map((str, i) => {
|
||||
if (str.trim() === '') {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch (e) {
|
||||
const errWithCol = e as {columnNumber?: number}
|
||||
const col = errWithCol.columnNumber !== undefined ? `:${errWithCol.columnNumber}` : ''
|
||||
throw new Error(`Invalid JSON at ${path}:${i + 1}${col}\n\n${e}`)
|
||||
}
|
||||
})
|
||||
.filter(evt => evt != null) as ReportEvent[]
|
||||
|
||||
let success = false
|
||||
let totalTime = 0
|
||||
const suites: {[id: number]: TestSuite} = {}
|
||||
const tests: {[id: number]: TestCase} = {}
|
||||
|
||||
for (const evt of events) {
|
||||
if (isSuiteEvent(evt)) {
|
||||
suites[evt.suite.id] = new TestSuite(evt.suite)
|
||||
} else if (isGroupEvent(evt)) {
|
||||
suites[evt.group.suiteID].groups[evt.group.id] = new TestGroup(evt.group)
|
||||
} else if (isTestStartEvent(evt) && evt.test.url !== null) {
|
||||
const test: TestCase = new TestCase(evt)
|
||||
const suite = suites[evt.test.suiteID]
|
||||
const group = suite.groups[evt.test.groupIDs[evt.test.groupIDs.length - 1]]
|
||||
group.tests.push(test)
|
||||
tests[evt.test.id] = test
|
||||
} else if (isTestDoneEvent(evt) && !evt.hidden && tests[evt.testID]) {
|
||||
tests[evt.testID].testDone = evt
|
||||
} else if (isErrorEvent(evt) && tests[evt.testID]) {
|
||||
tests[evt.testID].error = evt
|
||||
} else if (isMessageEvent(evt) && tests[evt.testID]) {
|
||||
tests[evt.testID].print.push(evt)
|
||||
} else if (isDoneEvent(evt)) {
|
||||
success = evt.success
|
||||
totalTime = evt.time
|
||||
}
|
||||
}
|
||||
|
||||
return new TestRun(path, Object.values(suites), success, totalTime)
|
||||
}
|
||||
|
||||
private getTestRunResult(tr: TestRun): TestRunResult {
|
||||
const suites = tr.suites.map(s => {
|
||||
return new TestSuiteResult(this.getRelativePath(s.suite.path), this.getGroups(s))
|
||||
})
|
||||
|
||||
return new TestRunResult(tr.path, suites, tr.time)
|
||||
}
|
||||
|
||||
private getGroups(suite: TestSuite): TestGroupResult[] {
|
||||
const groups = Object.values(suite.groups).filter(grp => grp.tests.length > 0)
|
||||
groups.sort((a, b) => (a.group.line ?? 0) - (b.group.line ?? 0))
|
||||
|
||||
return groups.map(group => {
|
||||
group.tests.sort((a, b) => (a.testStart.test.line ?? 0) - (b.testStart.test.line ?? 0))
|
||||
const tests = group.tests.map(tc => {
|
||||
const error = this.getError(suite, tc)
|
||||
const testName =
|
||||
group.group.name !== undefined && tc.testStart.test.name.startsWith(group.group.name)
|
||||
? tc.testStart.test.name.slice(group.group.name.length).trim()
|
||||
: tc.testStart.test.name.trim()
|
||||
return new TestCaseResult(testName, tc.result, tc.time, error)
|
||||
})
|
||||
return new TestGroupResult(group.group.name, tests)
|
||||
})
|
||||
}
|
||||
|
||||
private getError(testSuite: TestSuite, test: TestCase): TestCaseError | undefined {
|
||||
if (!this.options.parseErrors || !test.error) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const {trackedFiles} = this.options
|
||||
const stackTrace = test.error?.stackTrace ?? ''
|
||||
const print = test.print
|
||||
.filter(p => p.messageType === 'print')
|
||||
.map(p => p.message)
|
||||
.join('\n')
|
||||
const details = [print, stackTrace].filter(str => str !== '').join('\n')
|
||||
const src = this.exceptionThrowSource(details, trackedFiles)
|
||||
const message = this.getErrorMessage(test.error?.error ?? '', print)
|
||||
let path
|
||||
let line
|
||||
|
||||
if (src !== undefined) {
|
||||
path = src.path
|
||||
line = src.line
|
||||
} else {
|
||||
const testStartPath = this.getRelativePath(testSuite.suite.path)
|
||||
if (trackedFiles.includes(testStartPath)) {
|
||||
path = testStartPath
|
||||
line = test.testStart.test.root_line ?? test.testStart.test.line ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
line,
|
||||
message,
|
||||
details
|
||||
}
|
||||
}
|
||||
|
||||
private getErrorMessage(message: string, print: string): string {
|
||||
if (this.sdk === 'flutter') {
|
||||
const uselessMessageRe = /^Test failed\. See exception logs above\.\nThe test description was:/m
|
||||
const flutterPrintRe =
|
||||
/^══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞═+\s+(.*)\s+When the exception was thrown, this was the stack:/ms
|
||||
if (uselessMessageRe.test(message)) {
|
||||
const match = print.match(flutterPrintRe)
|
||||
if (match !== null) {
|
||||
return match[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message || print
|
||||
}
|
||||
|
||||
private exceptionThrowSource(ex: string, trackedFiles: string[]): {path: string; line: number} | undefined {
|
||||
const lines = ex.split(/\r?\n/g)
|
||||
|
||||
// regexp to extract file path and line number from stack trace
|
||||
const dartRe = /^(?!package:)(.*)\s+(\d+):\d+\s+/
|
||||
const flutterRe = /^#\d+\s+.*\((?!package:)(.*):(\d+):\d+\)$/
|
||||
const re = this.sdk === 'dart' ? dartRe : flutterRe
|
||||
|
||||
for (const str of lines) {
|
||||
const match = str.match(re)
|
||||
if (match !== null) {
|
||||
const [_, pathStr, lineStr] = match
|
||||
const path = normalizeFilePath(this.getRelativePath(pathStr))
|
||||
if (trackedFiles.includes(path)) {
|
||||
const line = parseInt(lineStr)
|
||||
return {path, line}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getRelativePath(path: string): string {
|
||||
const prefix = 'file://'
|
||||
if (path.startsWith(prefix)) {
|
||||
path = path.substr(prefix.length)
|
||||
}
|
||||
|
||||
path = normalizeFilePath(path)
|
||||
const workDir = this.getWorkDir(path)
|
||||
if (workDir !== undefined && path.startsWith(workDir)) {
|
||||
path = path.substr(workDir.length)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
private getWorkDir(path: string): string | undefined {
|
||||
return (
|
||||
this.options.workDir ??
|
||||
this.assumedWorkDir ??
|
||||
(this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
/// reflects documentation at https://github.com/dart-lang/test/blob/master/pkgs/test/doc/json_reporter.md
|
||||
|
||||
export type ReportEvent =
|
||||
| StartEvent
|
||||
| AllSuitesEvent
|
||||
| SuiteEvent
|
||||
| DebugEvent
|
||||
| GroupEvent
|
||||
| TestStartEvent
|
||||
| TestDoneEvent
|
||||
| DoneEvent
|
||||
| MessageEvent
|
||||
| ErrorEvent
|
||||
|
||||
export interface Event {
|
||||
type: 'start' | 'allSuites' | 'suite' | 'debug' | 'group' | 'testStart' | 'print' | 'error' | 'testDone' | 'done'
|
||||
time: number
|
||||
}
|
||||
|
||||
export interface StartEvent extends Event {
|
||||
type: 'start'
|
||||
protocolVersion: string
|
||||
runnerVersion: string
|
||||
pid: number
|
||||
}
|
||||
|
||||
export interface AllSuitesEvent extends Event {
|
||||
type: 'allSuites'
|
||||
count: number // The total number of suites that will be loaded.
|
||||
}
|
||||
|
||||
export interface SuiteEvent extends Event {
|
||||
type: 'suite'
|
||||
suite: Suite
|
||||
}
|
||||
|
||||
export interface GroupEvent extends Event {
|
||||
type: 'group'
|
||||
group: Group
|
||||
}
|
||||
|
||||
export interface TestStartEvent extends Event {
|
||||
type: 'testStart'
|
||||
test: Test
|
||||
}
|
||||
|
||||
export interface TestDoneEvent extends Event {
|
||||
type: 'testDone'
|
||||
testID: number
|
||||
result: 'success' | 'failure' | 'error'
|
||||
hidden: boolean
|
||||
skipped: boolean
|
||||
}
|
||||
|
||||
export interface DoneEvent extends Event {
|
||||
type: 'done'
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export interface ErrorEvent extends Event {
|
||||
type: 'error'
|
||||
testID: number
|
||||
error: string
|
||||
stackTrace: string
|
||||
isFailure: boolean
|
||||
}
|
||||
|
||||
export interface DebugEvent extends Event {
|
||||
type: 'debug'
|
||||
suiteID: number
|
||||
observatory: string
|
||||
remoteDebugger: string
|
||||
}
|
||||
|
||||
export interface MessageEvent extends Event {
|
||||
type: 'print'
|
||||
testID: number
|
||||
messageType: 'print' | 'skip'
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface Suite {
|
||||
id: number
|
||||
platform?: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: number
|
||||
name?: string
|
||||
suiteID: number
|
||||
parentID?: number
|
||||
testCount: number
|
||||
line: number | null // The (1-based) line on which the group was defined, or `null`.
|
||||
column: number | null // The (1-based) column on which the group was defined, or `null`.
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export interface Test {
|
||||
id: number
|
||||
name: string
|
||||
suiteID: number
|
||||
groupIDs: number[] // The IDs of groups containing this test, in order from outermost to innermost.
|
||||
line: number | null // The (1-based) line on which the test was defined, or `null`.
|
||||
column: number | null // The (1-based) column on which the test was defined, or `null`.
|
||||
url: string | null
|
||||
root_line?: number
|
||||
root_column?: number
|
||||
root_url: string | undefined
|
||||
}
|
||||
|
||||
export function isSuiteEvent(event: Event): event is SuiteEvent {
|
||||
return event.type === 'suite'
|
||||
}
|
||||
export function isGroupEvent(event: Event): event is GroupEvent {
|
||||
return event.type === 'group'
|
||||
}
|
||||
export function isTestStartEvent(event: Event): event is TestStartEvent {
|
||||
return event.type === 'testStart'
|
||||
}
|
||||
export function isTestDoneEvent(event: Event): event is TestDoneEvent {
|
||||
return event.type === 'testDone'
|
||||
}
|
||||
export function isErrorEvent(event: Event): event is ErrorEvent {
|
||||
return event.type === 'error'
|
||||
}
|
||||
export function isDoneEvent(event: Event): event is DoneEvent {
|
||||
return event.type === 'done'
|
||||
}
|
||||
export function isMessageEvent(event: Event): event is MessageEvent {
|
||||
return event.type === 'print'
|
||||
}
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
import * as path from 'path'
|
||||
import {ParseOptions, TestParser} from '../../test-parser'
|
||||
import {parseStringPromise} from 'xml2js'
|
||||
|
||||
import {parseStackTraceElement} from './java-stack-trace-element-parser'
|
||||
import {JunitReport, SingleSuiteReport, TestCase, TestSuite} from './java-junit-types'
|
||||
import {normalizeFilePath} from '../../utils/path-utils'
|
||||
|
||||
import {
|
||||
TestExecutionResult,
|
||||
TestRunResult,
|
||||
TestSuiteResult,
|
||||
TestGroupResult,
|
||||
TestCaseResult,
|
||||
TestCaseError
|
||||
} from '../../test-results'
|
||||
|
||||
export class JavaJunitParser implements TestParser {
|
||||
readonly trackedFiles: {[fileName: string]: string[]}
|
||||
|
||||
constructor(readonly options: ParseOptions) {
|
||||
// Map to efficient lookup of all paths with given file name
|
||||
this.trackedFiles = {}
|
||||
for (const filePath of options.trackedFiles) {
|
||||
const fileName = path.basename(filePath)
|
||||
const files = this.trackedFiles[fileName] ?? (this.trackedFiles[fileName] = [])
|
||||
files.push(normalizeFilePath(filePath))
|
||||
}
|
||||
}
|
||||
|
||||
async parse(filePath: string, content: string): Promise<TestRunResult> {
|
||||
const reportOrSuite = await this.getJunitReport(filePath, content)
|
||||
const isReport = (reportOrSuite as JunitReport).testsuites !== undefined
|
||||
|
||||
// XML might contain:
|
||||
// - multiple suites under <testsuites> root node
|
||||
// - single <testsuite> as root node
|
||||
let ju: JunitReport
|
||||
if (isReport) {
|
||||
ju = reportOrSuite as JunitReport
|
||||
} else {
|
||||
// Make it behave the same way as if suite was inside <testsuites> root node
|
||||
const suite = (reportOrSuite as SingleSuiteReport).testsuite
|
||||
ju = {
|
||||
testsuites: {
|
||||
$: {time: suite.$.time},
|
||||
testsuite: [suite]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.getTestRunResult(filePath, ju)
|
||||
}
|
||||
|
||||
private async getJunitReport(filePath: string, content: string): Promise<JunitReport | SingleSuiteReport> {
|
||||
try {
|
||||
return await parseStringPromise(content)
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid XML at ${filePath}\n\n${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
private getTestRunResult(filePath: string, junit: JunitReport): TestRunResult {
|
||||
const suites =
|
||||
junit.testsuites.testsuite === undefined
|
||||
? []
|
||||
: junit.testsuites.testsuite.map(ts => {
|
||||
const name = ts.$.name.trim()
|
||||
const time = parseFloat(ts.$.time) * 1000
|
||||
const sr = new TestSuiteResult(name, this.getGroups(ts), time)
|
||||
return sr
|
||||
})
|
||||
|
||||
const seconds = parseFloat(junit.testsuites.$?.time)
|
||||
const time = isNaN(seconds) ? undefined : seconds * 1000
|
||||
return new TestRunResult(filePath, suites, time)
|
||||
}
|
||||
|
||||
private getGroups(suite: TestSuite): TestGroupResult[] {
|
||||
if (suite.testcase === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const groups: {name: string; tests: TestCase[]}[] = []
|
||||
for (const tc of suite.testcase) {
|
||||
// Normally classname is same as suite name - both refer to same Java class
|
||||
// Therefore it doesn't make sense to process it as a group
|
||||
// and tests will be added to default group with empty name
|
||||
const className = tc.$.classname === suite.$.name ? '' : tc.$.classname
|
||||
let grp = groups.find(g => g.name === className)
|
||||
if (grp === undefined) {
|
||||
grp = {name: className, tests: []}
|
||||
groups.push(grp)
|
||||
}
|
||||
grp.tests.push(tc)
|
||||
}
|
||||
|
||||
return groups.map(grp => {
|
||||
const tests = grp.tests.map(tc => {
|
||||
const name = tc.$.name.trim()
|
||||
const result = this.getTestCaseResult(tc)
|
||||
const time = parseFloat(tc.$.time) * 1000
|
||||
const error = this.getTestCaseError(tc)
|
||||
return new TestCaseResult(name, result, time, error)
|
||||
})
|
||||
return new TestGroupResult(grp.name, tests)
|
||||
})
|
||||
}
|
||||
|
||||
private getTestCaseResult(test: TestCase): TestExecutionResult {
|
||||
if (test.failure || test.error) return 'failed'
|
||||
if (test.skipped) return 'skipped'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
private getTestCaseError(tc: TestCase): TestCaseError | undefined {
|
||||
if (!this.options.parseErrors) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// We process <error> and <failure> the same way
|
||||
const failures = tc.failure ?? tc.error
|
||||
if (!failures) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const failure = failures[0]
|
||||
const details = typeof failure === 'object' ? failure._ : failure
|
||||
let filePath
|
||||
let line
|
||||
|
||||
if (details != null) {
|
||||
const src = this.exceptionThrowSource(details)
|
||||
if (src) {
|
||||
filePath = src.filePath
|
||||
line = src.line
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
line,
|
||||
details,
|
||||
message: typeof failure === 'object' ? failure.message : undefined
|
||||
}
|
||||
}
|
||||
|
||||
private exceptionThrowSource(stackTrace: string): {filePath: string; line: number} | undefined {
|
||||
const lines = stackTrace.split(/\r?\n/)
|
||||
|
||||
for (const str of lines) {
|
||||
const stackTraceElement = parseStackTraceElement(str)
|
||||
if (stackTraceElement) {
|
||||
const {tracePath, fileName, lineStr} = stackTraceElement
|
||||
const filePath = this.getFilePath(tracePath, fileName)
|
||||
if (filePath !== undefined) {
|
||||
const line = parseInt(lineStr)
|
||||
return {filePath, line}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stacktrace in Java doesn't contain full paths to source file.
|
||||
// There are only package, file name and line.
|
||||
// Assuming folder structure matches package name (as it should in Java),
|
||||
// we can try to match tracked file.
|
||||
private getFilePath(tracePath: string, fileName: string): string | undefined {
|
||||
// Check if there is any tracked file with given name
|
||||
const files = this.trackedFiles[fileName]
|
||||
if (files === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Remove class name and method name from trace.
|
||||
// Take parts until first item with capital letter - package names are lowercase while class name is CamelCase.
|
||||
const packageParts = tracePath.split(/\./g)
|
||||
const packageIndex = packageParts.findIndex(part => part[0] <= 'Z')
|
||||
if (packageIndex !== -1) {
|
||||
packageParts.splice(packageIndex, packageParts.length - packageIndex)
|
||||
}
|
||||
|
||||
if (packageParts.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Get right file
|
||||
// - file name matches
|
||||
// - parent folders structure must reflect the package name
|
||||
for (const filePath of files) {
|
||||
const dirs = path.dirname(filePath).split(/\//g)
|
||||
if (packageParts.length > dirs.length) {
|
||||
continue
|
||||
}
|
||||
// get only N parent folders, where N = length of package name parts
|
||||
if (dirs.length > packageParts.length) {
|
||||
dirs.splice(0, dirs.length - packageParts.length)
|
||||
}
|
||||
// check if parent folder structure matches package name
|
||||
const isMatch = packageParts.every((part, i) => part === dirs[i])
|
||||
if (isMatch) {
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
export interface JunitReport {
|
||||
testsuites: TestSuites
|
||||
}
|
||||
|
||||
export interface SingleSuiteReport {
|
||||
testsuite: TestSuite
|
||||
}
|
||||
|
||||
export interface TestSuites {
|
||||
$: {
|
||||
time: string
|
||||
}
|
||||
testsuite?: TestSuite[]
|
||||
}
|
||||
|
||||
export interface TestSuite {
|
||||
$: {
|
||||
name: string
|
||||
tests: string
|
||||
errors: string
|
||||
failures: string
|
||||
skipped: string
|
||||
time: string
|
||||
timestamp?: Date
|
||||
}
|
||||
testcase: TestCase[]
|
||||
}
|
||||
|
||||
export interface TestCase {
|
||||
$: {
|
||||
classname: string
|
||||
file?: string
|
||||
name: string
|
||||
time: string
|
||||
}
|
||||
failure?: string | Failure[]
|
||||
error?: string | Failure[]
|
||||
skipped?: string[]
|
||||
}
|
||||
|
||||
export interface Failure {
|
||||
_: string
|
||||
type: string
|
||||
message: string
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
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}
|
||||
}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import {ParseOptions, TestParser} from '../../test-parser'
|
||||
import {parseStringPromise} from 'xml2js'
|
||||
|
||||
import {JunitReport, TestCase, TestSuite} from './jest-junit-types'
|
||||
import {getExceptionSource} from '../../utils/node-utils'
|
||||
import {getBasePath, normalizeFilePath} from '../../utils/path-utils'
|
||||
|
||||
import {
|
||||
TestExecutionResult,
|
||||
TestRunResult,
|
||||
TestSuiteResult,
|
||||
TestGroupResult,
|
||||
TestCaseResult,
|
||||
TestCaseError
|
||||
} from '../../test-results'
|
||||
|
||||
export class JestJunitParser implements TestParser {
|
||||
assumedWorkDir: string | undefined
|
||||
|
||||
constructor(readonly options: ParseOptions) {}
|
||||
|
||||
async parse(path: string, content: string): Promise<TestRunResult> {
|
||||
const ju = await this.getJunitReport(path, content)
|
||||
return this.getTestRunResult(path, ju)
|
||||
}
|
||||
|
||||
private async getJunitReport(path: string, content: string): Promise<JunitReport> {
|
||||
try {
|
||||
return (await parseStringPromise(content)) as JunitReport
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid XML at ${path}\n\n${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
private getTestRunResult(path: string, junit: JunitReport): TestRunResult {
|
||||
const suites =
|
||||
junit.testsuites.testsuite === undefined
|
||||
? []
|
||||
: junit.testsuites.testsuite.map(ts => {
|
||||
const name = this.escapeCharacters(ts.$.name.trim())
|
||||
const time = parseFloat(ts.$.time) * 1000
|
||||
const sr = new TestSuiteResult(name, this.getGroups(ts), time)
|
||||
return sr
|
||||
})
|
||||
|
||||
const time = parseFloat(junit.testsuites.$.time) * 1000
|
||||
return new TestRunResult(path, suites, time)
|
||||
}
|
||||
|
||||
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)
|
||||
if (grp === undefined) {
|
||||
grp = {describe: tc.$.classname, tests: []}
|
||||
groups.push(grp)
|
||||
}
|
||||
grp.tests.push(tc)
|
||||
}
|
||||
|
||||
return groups.map(grp => {
|
||||
const tests = grp.tests.map(tc => {
|
||||
const name = tc.$.name.trim()
|
||||
const result = this.getTestCaseResult(tc)
|
||||
const time = parseFloat(tc.$.time) * 1000
|
||||
const error = this.getTestCaseError(tc)
|
||||
return new TestCaseResult(name, result, time, error)
|
||||
})
|
||||
return new TestGroupResult(grp.describe, tests)
|
||||
})
|
||||
}
|
||||
|
||||
private getTestCaseResult(test: TestCase): TestExecutionResult {
|
||||
if (test.failure) return 'failed'
|
||||
if (test.skipped) return 'skipped'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
private getTestCaseError(tc: TestCase): TestCaseError | undefined {
|
||||
if (!this.options.parseErrors || !tc.failure) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const details = tc.failure[0]
|
||||
let path
|
||||
let line
|
||||
|
||||
const src = getExceptionSource(details, this.options.trackedFiles, file => this.getRelativePath(file))
|
||||
if (src) {
|
||||
path = src.path
|
||||
line = src.line
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
line,
|
||||
details
|
||||
}
|
||||
}
|
||||
|
||||
private getRelativePath(path: string): string {
|
||||
path = normalizeFilePath(path)
|
||||
const workDir = this.getWorkDir(path)
|
||||
if (workDir !== undefined && path.startsWith(workDir)) {
|
||||
path = path.substr(workDir.length)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
private getWorkDir(path: string): string | undefined {
|
||||
return (
|
||||
this.options.workDir ??
|
||||
this.assumedWorkDir ??
|
||||
(this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
|
||||
)
|
||||
}
|
||||
|
||||
private escapeCharacters(s: string): string {
|
||||
return s.replace(/([<>])/g, '\\$1')
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
export interface JunitReport {
|
||||
testsuites: TestSuites
|
||||
}
|
||||
|
||||
export interface TestSuites {
|
||||
$: {
|
||||
time: string
|
||||
}
|
||||
testsuite?: TestSuite[]
|
||||
}
|
||||
|
||||
export interface TestSuite {
|
||||
$: {
|
||||
name: string
|
||||
tests: string
|
||||
errors: string
|
||||
failures: string
|
||||
skipped: string
|
||||
time: string
|
||||
timestamp?: Date
|
||||
}
|
||||
testcase?: TestCase[]
|
||||
}
|
||||
|
||||
export interface TestCase {
|
||||
$: {
|
||||
classname: string
|
||||
file?: string
|
||||
name: string
|
||||
time: string
|
||||
}
|
||||
failure?: string[]
|
||||
skipped?: string[]
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
import {ParseOptions, TestParser} from '../../test-parser'
|
||||
import {
|
||||
TestCaseError,
|
||||
TestCaseResult,
|
||||
TestExecutionResult,
|
||||
TestGroupResult,
|
||||
TestRunResult,
|
||||
TestSuiteResult
|
||||
} from '../../test-results'
|
||||
import {getExceptionSource} from '../../utils/node-utils'
|
||||
import {getBasePath, normalizeFilePath} from '../../utils/path-utils'
|
||||
import {MochaJson, MochaJsonTest} from './mocha-json-types'
|
||||
|
||||
export class MochaJsonParser implements TestParser {
|
||||
assumedWorkDir: string | undefined
|
||||
|
||||
constructor(readonly options: ParseOptions) {}
|
||||
|
||||
async parse(path: string, content: string): Promise<TestRunResult> {
|
||||
const mocha = this.getMochaJson(path, content)
|
||||
const result = this.getTestRunResult(path, mocha)
|
||||
result.sort(true)
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
private getMochaJson(path: string, content: string): MochaJson {
|
||||
try {
|
||||
return JSON.parse(content)
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid JSON at ${path}\n\n${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
private getTestRunResult(resultsPath: string, mocha: MochaJson): TestRunResult {
|
||||
const suitesMap: {[path: string]: TestSuiteResult} = {}
|
||||
|
||||
const getSuite = (test: MochaJsonTest): TestSuiteResult => {
|
||||
const path = this.getRelativePath(test.file)
|
||||
return suitesMap[path] ?? (suitesMap[path] = new TestSuiteResult(path, []))
|
||||
}
|
||||
|
||||
for (const test of mocha.passes) {
|
||||
const suite = getSuite(test)
|
||||
this.processTest(suite, test, 'success')
|
||||
}
|
||||
|
||||
for (const test of mocha.failures) {
|
||||
const suite = getSuite(test)
|
||||
this.processTest(suite, test, 'failed')
|
||||
}
|
||||
|
||||
for (const test of mocha.pending) {
|
||||
const suite = getSuite(test)
|
||||
this.processTest(suite, test, 'skipped')
|
||||
}
|
||||
|
||||
const suites = Object.values(suitesMap)
|
||||
return new TestRunResult(resultsPath, suites, mocha.stats.duration)
|
||||
}
|
||||
|
||||
private processTest(suite: TestSuiteResult, test: MochaJsonTest, result: TestExecutionResult): void {
|
||||
const groupName =
|
||||
test.fullTitle !== test.title
|
||||
? test.fullTitle.substr(0, test.fullTitle.length - test.title.length).trimEnd()
|
||||
: null
|
||||
|
||||
let group = suite.groups.find(grp => grp.name === groupName)
|
||||
if (group === undefined) {
|
||||
group = new TestGroupResult(groupName, [])
|
||||
suite.groups.push(group)
|
||||
}
|
||||
|
||||
const error = this.getTestCaseError(test)
|
||||
const testCase = new TestCaseResult(test.title, result, test.duration ?? 0, error)
|
||||
group.tests.push(testCase)
|
||||
}
|
||||
|
||||
private getTestCaseError(test: MochaJsonTest): TestCaseError | undefined {
|
||||
const details = test.err.stack
|
||||
const message = test.err.message
|
||||
if (details === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let path
|
||||
let line
|
||||
|
||||
const src = getExceptionSource(details, this.options.trackedFiles, file => this.getRelativePath(file))
|
||||
if (src) {
|
||||
path = src.path
|
||||
line = src.line
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
line,
|
||||
message,
|
||||
details
|
||||
}
|
||||
}
|
||||
|
||||
private getRelativePath(path: string): string {
|
||||
path = normalizeFilePath(path)
|
||||
const workDir = this.getWorkDir(path)
|
||||
if (workDir !== undefined && path.startsWith(workDir)) {
|
||||
path = path.substr(workDir.length)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
private getWorkDir(path: string): string | undefined {
|
||||
return (
|
||||
this.options.workDir ??
|
||||
this.assumedWorkDir ??
|
||||
(this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
export interface MochaJson {
|
||||
stats: MochaJsonStats
|
||||
passes: MochaJsonTest[]
|
||||
pending: MochaJsonTest[]
|
||||
failures: MochaJsonTest[]
|
||||
}
|
||||
|
||||
export interface MochaJsonStats {
|
||||
duration: number
|
||||
}
|
||||
|
||||
export interface MochaJsonTest {
|
||||
title: string
|
||||
fullTitle: string
|
||||
file: string
|
||||
duration?: number
|
||||
err: MochaJsonTestError
|
||||
}
|
||||
|
||||
export interface MochaJsonTestError {
|
||||
stack?: string
|
||||
message?: string
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import {ParseOptions} from '../../test-parser'
|
||||
import {JavaJunitParser} from '../java-junit/java-junit-parser'
|
||||
|
||||
export class SwiftXunitParser extends JavaJunitParser {
|
||||
constructor(readonly options: ParseOptions) {
|
||||
super(options)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue