mirror of
https://github.com/dorny/test-reporter.git
synced 2025-12-17 06:47:09 +01:00
Added support for dotnet nunit
This commit is contained in:
parent
c9b3d0e2bd
commit
a97564ca53
21 changed files with 3265 additions and 1615 deletions
|
|
@ -15,6 +15,7 @@ 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 {DotnetNunitParser} from './parsers/dotnet-nunit/dotnet-nunit-parser'
|
||||
|
||||
import {normalizeDirPath, normalizeFilePath} from './utils/path-utils'
|
||||
import {getCheckRunContext} from './utils/github-utils'
|
||||
|
|
@ -211,6 +212,8 @@ class TestReporter {
|
|||
return new JestJunitParser(options)
|
||||
case 'mocha-json':
|
||||
return new MochaJsonParser(options)
|
||||
case 'dotnet-nunit':
|
||||
return new DotnetNunitParser(options)
|
||||
default:
|
||||
throw new Error(`Input variable 'reporter' is set to invalid value '${reporter}'`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {ParseOptions, TestParser} from '../../test-parser'
|
||||
|
||||
import {DEFAULT_LOCALE} from '../../utils/node-utils'
|
||||
import {getBasePath, normalizeFilePath} from '../../utils/path-utils'
|
||||
|
||||
import {
|
||||
|
|
@ -135,6 +135,8 @@ export class DartJsonParser implements TestParser {
|
|||
return new TestSuiteResult(this.getRelativePath(s.suite.path), this.getGroups(s))
|
||||
})
|
||||
|
||||
suites.sort((a, b) => a.name.localeCompare(b.name, DEFAULT_LOCALE))
|
||||
|
||||
return new TestRunResult(tr.path, suites, tr.time)
|
||||
}
|
||||
|
||||
|
|
|
|||
150
src/parsers/dotnet-nunit/dotnet-nunit-parser.ts
Normal file
150
src/parsers/dotnet-nunit/dotnet-nunit-parser.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import * as path from 'path'
|
||||
import {ParseOptions, TestParser} from '../../test-parser'
|
||||
import {parseStringPromise} from 'xml2js'
|
||||
|
||||
import {NunitReport, TestCase, TestSuite} from './dotnet-nunit-types'
|
||||
import {normalizeFilePath} from '../../utils/path-utils'
|
||||
|
||||
import {
|
||||
TestExecutionResult,
|
||||
TestRunResult,
|
||||
TestSuiteResult,
|
||||
TestGroupResult,
|
||||
TestCaseResult,
|
||||
TestCaseError
|
||||
} from '../../test-results'
|
||||
|
||||
export class DotnetNunitParser implements TestParser {
|
||||
readonly trackedFiles: {[fileName: string]: string[]}
|
||||
|
||||
constructor(readonly options: ParseOptions) {
|
||||
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.getNunitReport(filePath, content)
|
||||
return this.getTestRunResult(filePath, reportOrSuite)
|
||||
}
|
||||
|
||||
private async getNunitReport(filePath: string, content: string): Promise<NunitReport> {
|
||||
try {
|
||||
return await parseStringPromise(content)
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid XML at ${filePath}\n\n${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
private getTestSuiteResultRecursive(
|
||||
testSuites: TestSuite[] | undefined,
|
||||
suiteResults: TestSuiteResult[],
|
||||
depth: number
|
||||
): void {
|
||||
if (testSuites !== undefined) {
|
||||
testSuites.map(ts => {
|
||||
const name = ts.$.name.trim()
|
||||
const time = parseFloat(ts.$.duration) * 1000
|
||||
const groups = this.getGroups(ts)
|
||||
const sr = new TestSuiteResult(name, groups, time, depth)
|
||||
suiteResults.push(sr)
|
||||
|
||||
if (groups.length === 0) {
|
||||
const nestedTestSuites = ts['test-suite']
|
||||
if (nestedTestSuites !== undefined) {
|
||||
this.getTestSuiteResultRecursive(nestedTestSuites, suiteResults, depth + 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private getTestRunResult(filePath: string, nunit: NunitReport): TestRunResult {
|
||||
const suites: TestSuiteResult[] = []
|
||||
|
||||
const testSuites = nunit['test-run']['test-suite']
|
||||
this.getTestSuiteResultRecursive(testSuites, suites, 0)
|
||||
|
||||
const seconds = parseFloat(nunit['test-run'].$?.time)
|
||||
const time = isNaN(seconds) ? undefined : seconds * 1000
|
||||
return new TestRunResult(filePath, suites, time)
|
||||
}
|
||||
|
||||
private getGroups(suite: TestSuite): TestGroupResult[] {
|
||||
const groups: {describe: string; tests: TestCase[]}[] = []
|
||||
if (suite['test-case'] === undefined) {
|
||||
return []
|
||||
}
|
||||
for (const tc of suite['test-case']) {
|
||||
let grp = groups.find(g => g.describe === tc.$.name)
|
||||
if (grp === undefined) {
|
||||
grp = {describe: tc.$.name, 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.$.result === 'Skipped') return 'skipped'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
private getTestCaseError(tc: TestCase): TestCaseError | undefined {
|
||||
if (!this.options.parseErrors) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const failure = tc.failure
|
||||
if (!failure) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const details = failure[0]['stack-trace'] === undefined ? '' : failure[0]['stack-trace'][0]
|
||||
|
||||
let filePath
|
||||
let line
|
||||
|
||||
const src = this.exceptionThrowSource(details)
|
||||
if (src) {
|
||||
filePath = src.filePath
|
||||
line = src.line
|
||||
}
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
line,
|
||||
details,
|
||||
message: failure[0].message === undefined ? '' : failure[0].message[0]
|
||||
}
|
||||
}
|
||||
|
||||
private exceptionThrowSource(stackTrace: string): {filePath: string; line: number} | undefined {
|
||||
const lines = stackTrace.split(/\r?\n/)
|
||||
const re = /^at (.*\) in .*):(.+)$/
|
||||
|
||||
for (const str of lines) {
|
||||
const match = str.match(re)
|
||||
if (match !== null) {
|
||||
const [, , filePath, lineStr] = match
|
||||
const line = parseInt(lineStr)
|
||||
return {filePath, line}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/parsers/dotnet-nunit/dotnet-nunit-types.ts
Normal file
39
src/parsers/dotnet-nunit/dotnet-nunit-types.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
export interface NunitReport {
|
||||
'test-run': TestRun
|
||||
}
|
||||
|
||||
export interface TestRun {
|
||||
$: {
|
||||
time: string
|
||||
}
|
||||
'test-suite'?: TestSuite[]
|
||||
}
|
||||
|
||||
export interface TestSuite {
|
||||
$: {
|
||||
name: string
|
||||
tests: string
|
||||
errors: string
|
||||
failed: string
|
||||
skipped: string
|
||||
passed: string
|
||||
duration: string
|
||||
}
|
||||
'test-case'?: TestCase[]
|
||||
'test-suite'?: TestSuite[]
|
||||
}
|
||||
|
||||
export interface TestCase {
|
||||
$: {
|
||||
fullname: string
|
||||
name: string
|
||||
time: string
|
||||
result: string
|
||||
}
|
||||
failure?: Failure[]
|
||||
}
|
||||
|
||||
export interface Failure {
|
||||
'stack-trace'?: string
|
||||
message: string
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import * as path from 'path'
|
|||
import {ParseOptions, TestParser} from '../../test-parser'
|
||||
import {parseStringPromise} from 'xml2js'
|
||||
|
||||
import {DEFAULT_LOCALE} from '../../utils/node-utils'
|
||||
import {JunitReport, SingleSuiteReport, TestCase, TestSuite} from './java-junit-types'
|
||||
import {normalizeFilePath} from '../../utils/path-utils'
|
||||
|
||||
|
|
@ -70,6 +71,8 @@ export class JavaJunitParser implements TestParser {
|
|||
return sr
|
||||
})
|
||||
|
||||
suites.sort((a, b) => a.name.localeCompare(b.name, DEFAULT_LOCALE))
|
||||
|
||||
const seconds = parseFloat(junit.testsuites.$?.time)
|
||||
const time = isNaN(seconds) ? undefined : seconds * 1000
|
||||
return new TestRunResult(filePath, suites, time)
|
||||
|
|
|
|||
|
|
@ -81,9 +81,6 @@ function trimReport(lines: string[]): string {
|
|||
|
||||
function applySort(results: TestRunResult[]): void {
|
||||
results.sort((a, b) => a.path.localeCompare(b.path, DEFAULT_LOCALE))
|
||||
for (const res of results) {
|
||||
res.suites.sort((a, b) => a.name.localeCompare(b.name, DEFAULT_LOCALE))
|
||||
}
|
||||
}
|
||||
|
||||
function getByteLength(text: string): number {
|
||||
|
|
@ -184,7 +181,7 @@ function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOpt
|
|||
[Align.Left, Align.Right, Align.Right, Align.Right, Align.Right],
|
||||
...suites.map((s, suiteIndex) => {
|
||||
const tsTime = formatTime(s.time)
|
||||
const tsName = s.name
|
||||
const tsName = prependDepthIndentationToName(s.name.trim(), s.depth)
|
||||
const skipLink = options.listTests === 'none' || (options.listTests === 'failed' && s.result !== 'failed')
|
||||
const tsAddr = options.baseUrl + makeSuiteSlug(runIndex, suiteIndex).link
|
||||
const tsNameLink = skipLink ? tsName : link(tsName, tsAddr)
|
||||
|
|
@ -208,6 +205,11 @@ function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOpt
|
|||
return sections
|
||||
}
|
||||
|
||||
function prependDepthIndentationToName(name: string, depth: number): string {
|
||||
const depthPrefix = Array(depth).fill(' ').join('')
|
||||
return depthPrefix + name
|
||||
}
|
||||
|
||||
function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: number, options: ReportOptions): string[] {
|
||||
if (options.listTests === 'failed' && ts.result !== 'failed') {
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -40,7 +40,12 @@ 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,
|
||||
private indentationDepth: number = 0
|
||||
) {}
|
||||
|
||||
get tests(): number {
|
||||
return this.groups.reduce((sum, g) => sum + g.tests.length, 0)
|
||||
|
|
@ -49,12 +54,15 @@ export class TestSuiteResult {
|
|||
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)
|
||||
}
|
||||
|
|
@ -67,6 +75,10 @@ export class TestSuiteResult {
|
|||
return this.groups.filter(grp => grp.result === 'failed')
|
||||
}
|
||||
|
||||
get depth(): number {
|
||||
return this.indentationDepth
|
||||
}
|
||||
|
||||
sort(deep: boolean): void {
|
||||
this.groups.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '', DEFAULT_LOCALE))
|
||||
if (deep) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue