Skip to content

Commit

Permalink
Add dotnet-trx support (no annotations yet)
Browse files Browse the repository at this point in the history
  • Loading branch information
Michal Dorner committed Jan 11, 2021
1 parent 6482e39 commit b28f91c
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 4 deletions.
26 changes: 26 additions & 0 deletions __tests__/__snapshots__/dotnet-trx.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`dotnet-trx tests matches report snapshot 1`] = `
Object {
"annotations": Array [],
"summary": "**7** tests were completed in **1.061s** with **3** passed, **1** skipped and **3** failed.
| Result | Suite | Tests | Time | Passed ✔️ | Failed| Skipped ✖️ |
| :---: | :--- | ---: | ---: | ---: | ---: | ---: |
|| [DotnetTests.XUnitTests.CalculatorTests](#ts-0-DotnetTests-XUnitTests-CalculatorTests) | 7 | 109.5761ms | 3 | 3 | 1 |
# Test Suites
## <a id=\\"user-content-ts-0-DotnetTests-XUnitTests-CalculatorTests\\" href=\\"#ts-0-DotnetTests-XUnitTests-CalculatorTests\\">DotnetTests.XUnitTests.CalculatorTests</a>
| Result | Test | Time |
| :---: | :--- | ---: |
|| Exception_In_TargetTest | 0.4975ms |
|| Exception_In_Test | 2.2728ms |
|| Failing_Test | 3.2953ms |
| ✔️ | Passing_Test | 0.1254ms |
| ✔️ | Passing_Test_With_Name | 0.103ms |
| ✖️ | Skipped_Test | 1ms |
| ✔️ | Timeout_Test | 102.2821ms |
",
"title": "Dotnet TRX tests ❌",
}
`;
26 changes: 26 additions & 0 deletions __tests__/dotnet-trx.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as fs from 'fs'
import * as path from 'path'

import {parseDotnetTrx} from '../src/parsers/dotnet-trx/dotnet-trx-parser'
import {ParseOptions} from '../src/parsers/parser-types'

const xmlFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'dotnet-trx.trx'), {encoding: 'utf8'})
const outputPath = __dirname + '/__outputs__/dotnet-trx.md'

describe('dotnet-trx tests', () => {
it('matches report snapshot', async () => {
const opts: ParseOptions = {
name: 'Dotnet TRX tests',
annotations: true,
trackedFiles: ['DotnetTests.Unit/Calculator.cs', 'DotnetTests.XUnitTests/CalculatorTests.cs'],
workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/dotnet/'
}

const result = await parseDotnetTrx(xmlFixture, opts)
fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, result?.output?.summary ?? '')

expect(result.success).toBeFalsy()
expect(result?.output).toMatchSnapshot()
})
})
5 changes: 3 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import {parseJestJunit} from './parsers/jest-junit/jest-junit-parser'
import {parseDartJson} from './parsers/dart-json/dart-json-parser'
import {parseDotnetTrx} from './parsers/dotnet-trx/dotnet-trx-parser'
import {parseJestJunit} from './parsers/jest-junit/jest-junit-parser'
import {ParseOptions, ParseTestResult} from './parsers/parser-types'
import {getFileContent, normalizeDirPath} from './utils/file-utils'
import {listFiles} from './utils/git'
Expand Down Expand Up @@ -67,7 +68,7 @@ function getParser(reporter: string): ParseTestResult {
case 'dart-json':
return parseDartJson
case 'dotnet-trx':
throw new Error('Not implemented yet!')
return parseDotnetTrx
case 'flutter-machine':
return parseDartJson
case 'jest-junit':
Expand Down
115 changes: 115 additions & 0 deletions src/parsers/dotnet-trx/dotnet-trx-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {ErrorInfo, Outcome, TestMethod, TrxReport} from './dotnet-trx-types'

import {Annotation, ParseOptions, TestResult} from '../parser-types'
import {parseStringPromise} from 'xml2js'

import {parseAttribute} from '../../utils/xml-utils'
import {Icon} from '../../utils/markdown-utils'

import {
TestExecutionResult,
TestRunResult,
TestSuiteResult,
TestGroupResult,
TestCaseResult
} from '../../report/test-results'
import getReport from '../../report/get-report'

class TestClass {
constructor(readonly name: string) {}
readonly tests: Test[] = []
}

class Test {
constructor(
readonly name: string,
readonly outcome: Outcome,
readonly duration: number,
readonly error?: ErrorInfo
) {}

get result(): TestExecutionResult {
switch (this.outcome) {
case 'Passed':
return 'success'
case 'NotExecuted':
return 'skipped'
case 'Failed':
return 'failed'
}
}
}

export async function parseDotnetTrx(content: string, options: ParseOptions): Promise<TestResult> {
const trx = (await parseStringPromise(content, {
attrValueProcessors: [parseAttribute]
})) as TrxReport

const testClasses = getTestClasses(trx)
const testRun = getTestRunResult(trx, testClasses)
const success = testRun.result === 'success'
const icon = success ? Icon.success : Icon.fail

return {
success,
output: {
title: `${options.name.trim()} ${icon}`,
summary: getReport(testRun),
annotations: options.annotations
? getAnnotations(/*testClasses, options.workDir, options.trackedFiles*/)
: undefined
}
}
}

function getTestRunResult(trx: TrxReport, testClasses: TestClass[]): TestRunResult {
const times = trx.TestRun.Times[0].$
const totalTime = times.finish.getTime() - times.start.getTime()

const suites = testClasses.map(tc => {
const tests = tc.tests.map(t => new TestCaseResult(t.name, t.result, t.duration))
const group = new TestGroupResult(null, tests)
return new TestSuiteResult(tc.name, [group])
})

return new TestRunResult(suites, totalTime)
}

function getTestClasses(trx: TrxReport): TestClass[] {
const unitTests: {[id: string]: TestMethod} = {}
for (const td of trx.TestRun.TestDefinitions) {
for (const ut of td.UnitTest) {
unitTests[ut.$.id] = ut.TestMethod[0]
}
}

const unitTestsResults = trx.TestRun.Results.flatMap(r => r.UnitTestResult).flatMap(unitTestResult => ({
unitTestResult,
testMethod: unitTests[unitTestResult.$.testId]
}))

const testClasses: {[name: string]: TestClass} = {}
for (const r of unitTestsResults) {
let tc = testClasses[r.testMethod.$.className]
if (tc === undefined) {
tc = new TestClass(r.testMethod.$.className)
testClasses[tc.name] = tc
}
const output = r.unitTestResult.Output
const error = output?.length > 0 && output[0].ErrorInfo?.length > 0 ? output[0].ErrorInfo[0] : undefined
const test = new Test(r.testMethod.$.name, r.unitTestResult.$.outcome, r.unitTestResult.$.duration, error)
tc.tests.push(test)
}

const result = Object.values(testClasses)
result.sort((a, b) => a.name.localeCompare(b.name))
for (const tc of result) {
tc.tests.sort((a, b) => a.name.localeCompare(b.name))
}

return result
}

function getAnnotations(/*testClasses: TestClass[], workDir: string, trackedFiles: string[]*/): Annotation[] {
return []
}
60 changes: 60 additions & 0 deletions src/parsers/dotnet-trx/dotnet-trx-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export interface TrxReport {
TestRun: TestRun
}

export interface TestRun {
Times: Times[]
Results: Results[]
TestDefinitions: TestDefinitions[]
}

export interface Times {
$: {
creation: Date
queuing: Date
start: Date
finish: Date
}
}

export interface TestDefinitions {
UnitTest: UnitTest[]
}

export interface UnitTest {
$: {
id: string
}
TestMethod: TestMethod[]
}

export interface TestMethod {
$: {
className: string
name: string
}
}

export interface Results {
UnitTestResult: UnitTestResult[]
}

export interface UnitTestResult {
$: {
testId: string
testName: string
duration: number
outcome: Outcome
}
Output: Output[]
}

export interface Output {
ErrorInfo: ErrorInfo[]
}
export interface ErrorInfo {
Message: string[]
StackTrace: string[]
}

export type Outcome = 'Passed' | 'NotExecuted' | 'Failed'
2 changes: 1 addition & 1 deletion src/report/test-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class TestSuiteResult {
}

export class TestGroupResult {
constructor(readonly name: string | undefined, 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)
Expand Down
17 changes: 16 additions & 1 deletion src/utils/xml-utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
export function parseAttribute(str: string | undefined): string | number | undefined {
const isoDateRe = /^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)$/

// matches dotnet duration: 00:00:00.0010000
const durationRe = /^(\d\d):(\d\d):(\d\d\.\d+)$/

export function parseAttribute(str: string | undefined): string | Date | number | undefined {
if (str === '' || str === undefined) {
return str
}

if (isoDateRe.test(str)) {
return new Date(str)
}

const durationMatch = str.match(durationRe)
if (durationMatch !== null) {
const [_, hourStr, minStr, secStr] = durationMatch
return (parseInt(hourStr) * 3600 + parseInt(minStr) * 60 + parseFloat(secStr)) * 1000
}

const num = parseFloat(str)
if (isNaN(num)) {
return str
Expand Down

0 comments on commit b28f91c

Please sign in to comment.