mirror of
https://github.com/dorny/test-reporter.git
synced 2025-12-15 22:07:09 +01:00
Implement NUnit 3 parser.
This commit is contained in:
parent
b34d4b1bfe
commit
49c1f3ae6c
5 changed files with 372 additions and 0 deletions
28
__tests__/__outputs__/dotnet-nunit.md
Normal file
28
__tests__/__outputs__/dotnet-nunit.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|

|
||||||
|
## ❌ <a id="user-content-r0" href="#r0">fixtures/dotnet-nunit.xml</a>
|
||||||
|
**9** tests were completed in **0ms** with **3** passed, **5** failed and **1** skipped.
|
||||||
|
|Test suite|Passed|Failed|Skipped|Time|
|
||||||
|
|:---|---:|---:|---:|---:|
|
||||||
|
|[DotnetTests.NUnitV3Tests.dll.DotnetTests.XUnitTests](#r0s0)|3✅|5❌|1⚪|0ms|
|
||||||
|
### ❌ <a id="user-content-r0s0" href="#r0s0">DotnetTests.NUnitV3Tests.dll.DotnetTests.XUnitTests</a>
|
||||||
|
```
|
||||||
|
CalculatorTests
|
||||||
|
✅ Is_Even_Number(2)
|
||||||
|
❌ Is_Even_Number(3)
|
||||||
|
Expected: True
|
||||||
|
But was: False
|
||||||
|
|
||||||
|
❌ Exception_In_TargetTest
|
||||||
|
System.DivideByZeroException : Attempted to divide by zero.
|
||||||
|
❌ Exception_In_Test
|
||||||
|
System.Exception : Test
|
||||||
|
❌ Failing_Test
|
||||||
|
Expected: 3
|
||||||
|
But was: 2
|
||||||
|
|
||||||
|
✅ Passing_Test
|
||||||
|
✅ Passing_Test_With_Description
|
||||||
|
⚪ Skipped_Test
|
||||||
|
❌ Timeout_Test
|
||||||
|
|
||||||
|
```
|
||||||
107
__tests__/__snapshots__/dotnet-nunit.test.ts.snap
Normal file
107
__tests__/__snapshots__/dotnet-nunit.test.ts.snap
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`dotnet-nunit tests report from ./reports/dotnet test results matches snapshot 1`] = `
|
||||||
|
TestRunResult {
|
||||||
|
"path": "fixtures/dotnet-nunit.xml",
|
||||||
|
"suites": Array [
|
||||||
|
TestSuiteResult {
|
||||||
|
"groups": Array [
|
||||||
|
TestGroupResult {
|
||||||
|
"name": "CalculatorTests",
|
||||||
|
"tests": Array [
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "Is_Even_Number(2)",
|
||||||
|
"result": "success",
|
||||||
|
"time": 0.000622,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": Object {
|
||||||
|
"details": " at DotnetTests.XUnitTests.CalculatorTests.Is_Even_Number(Int32 i) in C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-reporter\\\\reports\\\\dotnet\\\\DotnetTests.NUnitV3Tests\\\\CalculatorTests.cs:line 61
|
||||||
|
",
|
||||||
|
"line": undefined,
|
||||||
|
"message": " Expected: True
|
||||||
|
But was: False
|
||||||
|
",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"name": "Is_Even_Number(3)",
|
||||||
|
"result": "failed",
|
||||||
|
"time": 0.001098,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": Object {
|
||||||
|
"details": " at DotnetTests.Unit.Calculator.Div(Int32 a, Int32 b) in C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-reporter\\\\reports\\\\dotnet\\\\DotnetTests.Unit\\\\Calculator.cs:line 9
|
||||||
|
at DotnetTests.XUnitTests.CalculatorTests.Exception_In_TargetTest() in C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-reporter\\\\reports\\\\dotnet\\\\DotnetTests.NUnitV3Tests\\\\CalculatorTests.cs:line 33",
|
||||||
|
"line": undefined,
|
||||||
|
"message": "System.DivideByZeroException : Attempted to divide by zero.",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"name": "Exception_In_TargetTest",
|
||||||
|
"result": "failed",
|
||||||
|
"time": 0.022805,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": Object {
|
||||||
|
"details": " at DotnetTests.XUnitTests.CalculatorTests.Exception_In_Test() in C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-reporter\\\\reports\\\\dotnet\\\\DotnetTests.NUnitV3Tests\\\\CalculatorTests.cs:line 39",
|
||||||
|
"line": undefined,
|
||||||
|
"message": "System.Exception : Test",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"name": "Exception_In_Test",
|
||||||
|
"result": "failed",
|
||||||
|
"time": 0.000528,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": Object {
|
||||||
|
"details": " at DotnetTests.XUnitTests.CalculatorTests.Failing_Test() in C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-reporter\\\\reports\\\\dotnet\\\\DotnetTests.NUnitV3Tests\\\\CalculatorTests.cs:line 27
|
||||||
|
",
|
||||||
|
"line": undefined,
|
||||||
|
"message": " Expected: 3
|
||||||
|
But was: 2
|
||||||
|
",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"name": "Failing_Test",
|
||||||
|
"result": "failed",
|
||||||
|
"time": 0.028162,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "Passing_Test",
|
||||||
|
"result": "success",
|
||||||
|
"time": 0.000238,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "Passing_Test_With_Description",
|
||||||
|
"result": "success",
|
||||||
|
"time": 0.000135,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "Skipped_Test",
|
||||||
|
"result": "skipped",
|
||||||
|
"time": 0.000398,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": Object {
|
||||||
|
"details": "",
|
||||||
|
"line": undefined,
|
||||||
|
"message": "",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"name": "Timeout_Test",
|
||||||
|
"result": "failed",
|
||||||
|
"time": 0.014949,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "DotnetTests.NUnitV3Tests.dll.DotnetTests.XUnitTests",
|
||||||
|
"totalTime": undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"totalTime": 0.230308,
|
||||||
|
}
|
||||||
|
`;
|
||||||
29
__tests__/dotnet-nunit.test.ts
Normal file
29
__tests__/dotnet-nunit.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
import {DotNetNunitParser} from '../src/parsers/dotnet-nunit/dotnet-nunit-parser'
|
||||||
|
import {ParseOptions} from '../src/test-parser'
|
||||||
|
import {getReport} from '../src/report/get-report'
|
||||||
|
import {normalizeFilePath} from '../src/utils/path-utils'
|
||||||
|
|
||||||
|
describe('dotnet-nunit tests', () => {
|
||||||
|
it('report from ./reports/dotnet test results matches snapshot', async () => {
|
||||||
|
const fixturePath = path.join(__dirname, 'fixtures', 'dotnet-nunit.xml')
|
||||||
|
const outputPath = path.join(__dirname, '__outputs__', 'dotnet-nunit.md')
|
||||||
|
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
|
||||||
|
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
|
||||||
|
|
||||||
|
const opts: ParseOptions = {
|
||||||
|
parseErrors: true,
|
||||||
|
trackedFiles: ['DotnetTests.Unit/Calculator.cs', 'DotnetTests.NUnitV3Tests/CalculatorTests.cs']
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new DotNetNunitParser(opts)
|
||||||
|
const result = await parser.parse(filePath, fileContent)
|
||||||
|
expect(result).toMatchSnapshot()
|
||||||
|
|
||||||
|
const report = getReport([result])
|
||||||
|
fs.mkdirSync(path.dirname(outputPath), {recursive: true})
|
||||||
|
fs.writeFileSync(outputPath, report)
|
||||||
|
})
|
||||||
|
})
|
||||||
151
src/parsers/dotnet-nunit/dotnet-nunit-parser.ts
Normal file
151
src/parsers/dotnet-nunit/dotnet-nunit-parser.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import {ParseOptions, TestParser} from '../../test-parser'
|
||||||
|
import {parseStringPromise} from 'xml2js'
|
||||||
|
|
||||||
|
import {NunitReport, TestCase, TestRun, TestSuite} from './dotnet-nunit-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 DotNetNunitParser implements TestParser {
|
||||||
|
assumedWorkDir: string | undefined
|
||||||
|
|
||||||
|
constructor(readonly options: ParseOptions) {}
|
||||||
|
|
||||||
|
async parse(path: string, content: string): Promise<TestRunResult> {
|
||||||
|
const ju = await this.getNunitReport(path, content)
|
||||||
|
return this.getTestRunResult(path, ju)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNunitReport(path: string, content: string): Promise<NunitReport> {
|
||||||
|
try {
|
||||||
|
return (await parseStringPromise(content)) as NunitReport
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Invalid XML at ${path}\n\n${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTestRunResult(path: string, nunit: NunitReport): TestRunResult {
|
||||||
|
const suites: TestSuiteResult[] = []
|
||||||
|
const time = parseFloat(nunit['test-run'].$.duration)
|
||||||
|
|
||||||
|
this.populateTestCasesRecursive(suites, [], nunit['test-run']['test-suite'])
|
||||||
|
|
||||||
|
return new TestRunResult(path, suites, time)
|
||||||
|
}
|
||||||
|
|
||||||
|
private populateTestCasesRecursive(
|
||||||
|
result: TestSuiteResult[],
|
||||||
|
suitePath: TestSuite[],
|
||||||
|
testSuites: TestSuite[] | undefined
|
||||||
|
): void {
|
||||||
|
if (testSuites === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testSuites.forEach(suite => {
|
||||||
|
suitePath.push(suite)
|
||||||
|
|
||||||
|
this.populateTestCasesRecursive(result, suitePath, suite['test-suite'])
|
||||||
|
|
||||||
|
const testcases = suite['test-case']
|
||||||
|
if (testcases !== undefined) {
|
||||||
|
testcases.forEach(testcase => {
|
||||||
|
this.addTestCase(result, suitePath, testcase)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suitePath.pop()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTestCase(result: TestSuiteResult[], suitePath: TestSuite[], testCase: TestCase) {
|
||||||
|
// The last suite in the suite path is the "group".
|
||||||
|
// The rest are concatenated together to form the "suite".
|
||||||
|
// But ignore "Theory" suites.
|
||||||
|
const suitesWithoutTheories = suitePath.filter(suite => suite.$.type !== 'Theory')
|
||||||
|
const suiteName = suitesWithoutTheories
|
||||||
|
.slice(0, suitesWithoutTheories.length - 1)
|
||||||
|
.map(suite => suite.$.name)
|
||||||
|
.join('.')
|
||||||
|
const groupName = suitesWithoutTheories[suitesWithoutTheories.length - 1].$.name
|
||||||
|
|
||||||
|
let existingSuite = result.find(existingSuite => existingSuite.name === suiteName)
|
||||||
|
if (existingSuite === undefined) {
|
||||||
|
existingSuite = new TestSuiteResult(suiteName, [])
|
||||||
|
result.push(existingSuite)
|
||||||
|
}
|
||||||
|
|
||||||
|
let existingGroup = existingSuite.groups.find(existingGroup => existingGroup.name === groupName)
|
||||||
|
if (existingGroup === undefined) {
|
||||||
|
existingGroup = new TestGroupResult(groupName, [])
|
||||||
|
existingSuite.groups.push(existingGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingGroup.tests.push(
|
||||||
|
new TestCaseResult(
|
||||||
|
testCase.$.name,
|
||||||
|
this.getTestExecutionResult(testCase),
|
||||||
|
parseFloat(testCase.$.duration),
|
||||||
|
this.getTestCaseError(testCase)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTestExecutionResult(test: TestCase): TestExecutionResult {
|
||||||
|
if (test.$.result === 'Failed' || test.failure) return 'failed'
|
||||||
|
if (test.$.result === 'Skipped') return 'skipped'
|
||||||
|
return 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTestCaseError(tc: TestCase): TestCaseError | undefined {
|
||||||
|
if (!this.options.parseErrors || !tc.failure || tc.failure.length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const details = tc.failure[0]
|
||||||
|
let path
|
||||||
|
let line
|
||||||
|
|
||||||
|
if (details['stack-trace'] !== undefined && details['stack-trace'].length > 0) {
|
||||||
|
const src = getExceptionSource(details['stack-trace'][0], this.options.trackedFiles, file =>
|
||||||
|
this.getRelativePath(file)
|
||||||
|
)
|
||||||
|
if (src) {
|
||||||
|
path = src.path
|
||||||
|
line = src.line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: path,
|
||||||
|
line: line,
|
||||||
|
message: details.message && details.message.length > 0 ? details.message[0] : '',
|
||||||
|
details: details['stack-trace'] && details['stack-trace'].length > 0 ? details['stack-trace'][0] : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/parsers/dotnet-nunit/dotnet-nunit-types.ts
Normal file
57
src/parsers/dotnet-nunit/dotnet-nunit-types.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
export interface NunitReport {
|
||||||
|
"test-run": TestRun
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestRun {
|
||||||
|
$: {
|
||||||
|
id: string
|
||||||
|
runstate: string
|
||||||
|
testcasecount: string
|
||||||
|
result: string
|
||||||
|
total: string
|
||||||
|
passed: string
|
||||||
|
failed: string
|
||||||
|
inconclusive: string
|
||||||
|
skipped: string
|
||||||
|
asserts: string
|
||||||
|
'engine-version': string
|
||||||
|
'clr-version': string
|
||||||
|
'start-time': string
|
||||||
|
'end-time': string
|
||||||
|
duration: string
|
||||||
|
}
|
||||||
|
'test-suite'?: TestSuite[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestSuite {
|
||||||
|
$: {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
'test-case'?: TestCase[]
|
||||||
|
'test-suite'?: TestSuite[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestCase {
|
||||||
|
$: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
fullname: string
|
||||||
|
methodname: string
|
||||||
|
classname: string
|
||||||
|
runstate: string
|
||||||
|
seed: string
|
||||||
|
result: string
|
||||||
|
label: string
|
||||||
|
'start-time': string
|
||||||
|
'end-time': string
|
||||||
|
duration: string
|
||||||
|
asserts: string
|
||||||
|
}
|
||||||
|
failure?: TestFailure[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestFailure {
|
||||||
|
message?: string[]
|
||||||
|
'stack-trace'?: string[]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue