Skip to content

Commit

Permalink
Implement NUnit 3 parser.
Browse files Browse the repository at this point in the history
  • Loading branch information
Kevin Ring authored and Jozef Izso committed Jun 25, 2024
1 parent b34d4b1 commit 49c1f3a
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 0 deletions.
28 changes: 28 additions & 0 deletions __tests__/__outputs__/dotnet-nunit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
![Tests failed](https://img.shields.io/badge/tests-3%20passed%2C%205%20failed%2C%201%20skipped-critical)
## ❌ <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 changes: 107 additions & 0 deletions __tests__/__snapshots__/dotnet-nunit.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions __tests__/dotnet-nunit.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 151 additions & 0 deletions src/parsers/dotnet-nunit/dotnet-nunit-parser.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 57 additions & 0 deletions src/parsers/dotnet-nunit/dotnet-nunit-types.ts
Original file line number Diff line number Diff line change
@@ -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[]
}

0 comments on commit 49c1f3a

Please sign in to comment.