diff --git a/__tests__/__outputs__/dotnet-nunit-legacy.md b/__tests__/__outputs__/dotnet-nunit-legacy.md
new file mode 100644
index 0000000..a4b31bf
--- /dev/null
+++ b/__tests__/__outputs__/dotnet-nunit-legacy.md
@@ -0,0 +1,34 @@
+
+|Report|Passed|Failed|Skipped|Time|
+|:---|---:|---:|---:|---:|
+|fixtures/dotnet-nunit-legacy.xml|3 ✅|5 ❌|2 ⚪|0ms|
+## ❌ fixtures/dotnet-nunit-legacy.xml
+**10** tests were completed in **0ms** with **3** passed, **5** failed and **2** skipped.
+|Test suite|Passed|Failed|Skipped|Time|
+|:---|---:|---:|---:|---:|
+|[C:\Users\james_t\source\repos\NUnitLegacy\NUnitLegacy.sln.C:\Users\james_t\source\repos\NUnitLegacy\NUnitLegacyTests\bin\Debug\NUnitLegacyTests.dll.NUnitLegacyTests](#r0s0)|3 ✅|5 ❌|2 ⚪|NaNms|
+### ❌ C:\Users\james_t\source\repos\NUnitLegacy\NUnitLegacy.sln.C:\Users\james_t\source\repos\NUnitLegacy\NUnitLegacyTests\bin\Debug\NUnitLegacyTests.dll.NUnitLegacyTests
+```
+CalculatorTests
+ ✅ NUnitLegacyTests.CalculatorTests.Is_Even_Number(2)
+ ❌ NUnitLegacyTests.CalculatorTests.Is_Even_Number(3)
+ Expected: True
+ But was: False
+
+ ❌ NUnitLegacyTests.CalculatorTests.Exception_In_TargetTest
+ System.DivideByZeroException : Attempted to divide by zero.
+ ❌ NUnitLegacyTests.CalculatorTests.Exception_In_Test
+ System.Exception : Test
+ ❌ NUnitLegacyTests.CalculatorTests.Failing_Test
+ Expected: 3
+ But was: 2
+
+ ⚪ NUnitLegacyTests.CalculatorTests.Inconclusive_Test
+ couldn't run test for some reason
+ ✅ NUnitLegacyTests.CalculatorTests.Passing_Test
+ ✅ NUnitLegacyTests.CalculatorTests.Passing_Test_With_Description
+ ⚪ NUnitLegacyTests.CalculatorTests.Skipped_Test
+ Skipped
+ ❌ NUnitLegacyTests.CalculatorTests.Timeout_Test
+ Test exceeded Timeout value of 1ms
+```
\ No newline at end of file
diff --git a/__tests__/__snapshots__/dotnet-nunit-legacy.test.ts.snap b/__tests__/__snapshots__/dotnet-nunit-legacy.test.ts.snap
new file mode 100644
index 0000000..27d5820
--- /dev/null
+++ b/__tests__/__snapshots__/dotnet-nunit-legacy.test.ts.snap
@@ -0,0 +1,125 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`dotnet-nunit-legacy tests report from ./reports/dotnet test results matches snapshot 1`] = `
+TestRunResult {
+ "path": "fixtures/dotnet-nunit-legacy.xml",
+ "suites": [
+ TestSuiteResult {
+ "groups": [
+ TestGroupResult {
+ "name": "CalculatorTests",
+ "tests": [
+ TestCaseResult {
+ "error": undefined,
+ "name": "NUnitLegacyTests.CalculatorTests.Is_Even_Number(2)",
+ "result": "success",
+ "time": 0.001,
+ },
+ TestCaseResult {
+ "error": {
+ "details": "at NUnitLegacyTests.CalculatorTests.Is_Even_Number(Int32 i) in C:\\Users\\james_t\\source\\repos\\NUnitLegacy\\NUnitLegacyTests\\CalculatorTests.cs:line 61
+",
+ "line": undefined,
+ "message": " Expected: True
+ But was: False
+",
+ "path": undefined,
+ },
+ "name": "NUnitLegacyTests.CalculatorTests.Is_Even_Number(3)",
+ "result": "failed",
+ "time": 0.002,
+ },
+ TestCaseResult {
+ "error": {
+ "details": "at MyLibrary.Calculator.Div(Int32 a, Int32 b) in C:\\Users\\james_t\\source\\repos\\NUnitLegacy\\MyLibrary\\Calculator.cs:line 7
+at NUnitLegacyTests.CalculatorTests.Exception_In_TargetTest() in C:\\Users\\james_t\\source\\repos\\NUnitLegacy\\NUnitLegacyTests\\CalculatorTests.cs:line 33
+",
+ "line": undefined,
+ "message": "System.DivideByZeroException : Attempted to divide by zero.",
+ "path": undefined,
+ },
+ "name": "NUnitLegacyTests.CalculatorTests.Exception_In_TargetTest",
+ "result": "failed",
+ "time": 0.028,
+ },
+ TestCaseResult {
+ "error": {
+ "details": "at NUnitLegacyTests.CalculatorTests.Exception_In_Test() in C:\\Users\\james_t\\source\\repos\\NUnitLegacy\\NUnitLegacyTests\\CalculatorTests.cs:line 39
+",
+ "line": undefined,
+ "message": "System.Exception : Test",
+ "path": undefined,
+ },
+ "name": "NUnitLegacyTests.CalculatorTests.Exception_In_Test",
+ "result": "failed",
+ "time": 0.002,
+ },
+ TestCaseResult {
+ "error": {
+ "details": "at NUnitLegacyTests.CalculatorTests.Failing_Test() in C:\\Users\\james_t\\source\\repos\\NUnitLegacy\\NUnitLegacyTests\\CalculatorTests.cs:line 27
+",
+ "line": undefined,
+ "message": " Expected: 3
+ But was: 2
+",
+ "path": undefined,
+ },
+ "name": "NUnitLegacyTests.CalculatorTests.Failing_Test",
+ "result": "failed",
+ "time": 0.016,
+ },
+ TestCaseResult {
+ "error": {
+ "details": "",
+ "line": undefined,
+ "message": "couldn't run test for some reason",
+ "path": undefined,
+ },
+ "name": "NUnitLegacyTests.CalculatorTests.Inconclusive_Test",
+ "result": "skipped",
+ "time": 0.002,
+ },
+ TestCaseResult {
+ "error": undefined,
+ "name": "NUnitLegacyTests.CalculatorTests.Passing_Test",
+ "result": "success",
+ "time": 0,
+ },
+ TestCaseResult {
+ "error": undefined,
+ "name": "NUnitLegacyTests.CalculatorTests.Passing_Test_With_Description",
+ "result": "success",
+ "time": 0,
+ },
+ TestCaseResult {
+ "error": {
+ "details": "",
+ "line": undefined,
+ "message": "Skipped",
+ "path": undefined,
+ },
+ "name": "NUnitLegacyTests.CalculatorTests.Skipped_Test",
+ "result": "skipped",
+ "time": NaN,
+ },
+ TestCaseResult {
+ "error": {
+ "details": "",
+ "line": undefined,
+ "message": "Test exceeded Timeout value of 1ms",
+ "path": undefined,
+ },
+ "name": "NUnitLegacyTests.CalculatorTests.Timeout_Test",
+ "result": "failed",
+ "time": 0.027,
+ },
+ ],
+ },
+ ],
+ "name": "C:\\Users\\james_t\\source\\repos\\NUnitLegacy\\NUnitLegacy.sln.C:\\Users\\james_t\\source\\repos\\NUnitLegacy\\NUnitLegacyTests\\bin\\Debug\\NUnitLegacyTests.dll.NUnitLegacyTests",
+ "totalTime": undefined,
+ },
+ ],
+ "totalTime": 0,
+}
+`;
diff --git a/__tests__/dotnet-nunit-legacy.test.ts b/__tests__/dotnet-nunit-legacy.test.ts
new file mode 100644
index 0000000..b03809b
--- /dev/null
+++ b/__tests__/dotnet-nunit-legacy.test.ts
@@ -0,0 +1,29 @@
+import * as fs from 'fs'
+import * as path from 'path'
+
+import {DotnetNunitLegacyParser} from '../src/parsers/dotnet-nunit-legacy/dotnet-nunit-legacy-parser'
+import {ParseOptions} from '../src/test-parser'
+import {getReport} from '../src/report/get-report'
+import {normalizeFilePath} from '../src/utils/path-utils'
+
+describe('dotnet-nunit-legacy tests', () => {
+ it('report from ./reports/dotnet test results matches snapshot', async () => {
+ const fixturePath = path.join(__dirname, 'fixtures', 'dotnet-nunit-legacy.xml')
+ const outputPath = path.join(__dirname, '__outputs__', 'dotnet-nunit-legacy.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.NUnitLegacyTests/CalculatorTests.cs']
+ }
+
+ const parser = new DotnetNunitLegacyParser(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)
+ })
+})
diff --git a/__tests__/fixtures/dotnet-nunit-legacy.xml b/__tests__/fixtures/dotnet-nunit-legacy.xml
new file mode 100644
index 0000000..2963491
--- /dev/null
+++ b/__tests__/fixtures/dotnet-nunit-legacy.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dist/index.js b/dist/index.js
index fa45c86..79ed489 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -242,6 +242,7 @@ const get_annotations_1 = __nccwpck_require__(5867);
const get_report_1 = __nccwpck_require__(3737);
const dart_json_parser_1 = __nccwpck_require__(4528);
const dotnet_nunit_parser_1 = __nccwpck_require__(5706);
+const dotnet_nunit_legacy_parser_1 = __nccwpck_require__(6956);
const dotnet_trx_parser_1 = __nccwpck_require__(2664);
const java_junit_parser_1 = __nccwpck_require__(676);
const jest_junit_parser_1 = __nccwpck_require__(1113);
@@ -426,6 +427,8 @@ class TestReporter {
return new dart_json_parser_1.DartJsonParser(options, 'dart');
case 'dotnet-nunit':
return new dotnet_nunit_parser_1.DotnetNunitParser(options);
+ case 'dotnet-nunit-legacy':
+ return new dotnet_nunit_legacy_parser_1.DotnetNunitLegacyParser(options);
case 'dotnet-trx':
return new dotnet_trx_parser_1.DotnetTrxParser(options);
case 'flutter-json':
@@ -723,6 +726,134 @@ function isMessageEvent(event) {
}
+/***/ }),
+
+/***/ 6956:
+/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.DotnetNunitLegacyParser = void 0;
+const xml2js_1 = __nccwpck_require__(6189);
+const node_utils_1 = __nccwpck_require__(5824);
+const path_utils_1 = __nccwpck_require__(4070);
+const test_results_1 = __nccwpck_require__(2768);
+class DotnetNunitLegacyParser {
+ options;
+ assumedWorkDir;
+ constructor(options) {
+ this.options = options;
+ }
+ async parse(path, content) {
+ const ju = await this.getNunitReport(path, content);
+ return this.getTestRunResult(path, ju);
+ }
+ async getNunitReport(path, content) {
+ try {
+ return (await (0, xml2js_1.parseStringPromise)(content));
+ }
+ catch (e) {
+ throw new Error(`Invalid XML at ${path}\n\n${e}`);
+ }
+ }
+ getTestRunResult(path, nunit) {
+ const suites = [];
+ const time = parseFloat(nunit['test-results'].$.time);
+ this.populateTestCasesRecursive(suites, [], nunit['test-results']['test-suite']);
+ return new test_results_1.TestRunResult(path, suites, time);
+ }
+ populateTestCasesRecursive(result, suitePath, testSuites) {
+ if (testSuites === undefined) {
+ return;
+ }
+ for (const suite of testSuites) {
+ if (!suite['results']) {
+ continue;
+ }
+ suitePath.push(suite);
+ const results = suite['results'][0];
+ this.populateTestCasesRecursive(result, suitePath, results['test-suite']);
+ const testcases = results['test-case'];
+ if (testcases !== undefined) {
+ for (const testcase of testcases) {
+ this.addTestCase(result, suitePath, testcase);
+ }
+ }
+ suitePath.pop();
+ }
+ }
+ addTestCase(result, suitePath, 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 test_results_1.TestSuiteResult(suiteName, []);
+ result.push(existingSuite);
+ }
+ let existingGroup = existingSuite.groups.find(existingGroup => existingGroup.name === groupName);
+ if (existingGroup === undefined) {
+ existingGroup = new test_results_1.TestGroupResult(groupName, []);
+ existingSuite.groups.push(existingGroup);
+ }
+ existingGroup.tests.push(new test_results_1.TestCaseResult(testCase.$.name, this.getTestExecutionResult(testCase), parseFloat(testCase.$.time), this.getTestCaseError(testCase)));
+ }
+ getTestExecutionResult(test) {
+ if (test.$.result === 'Failed' || test.failure)
+ return 'failed';
+ if (test.$.result === 'Skipped' || test.$.result === 'Ignored' || test.$.result === 'Inconclusive')
+ return 'skipped';
+ return 'success';
+ }
+ getTestCaseError(tc) {
+ if (!this.options.parseErrors ||
+ ((!tc.failure || tc.failure.length === 0) && (!tc.reason || tc.reason.length === 0))) {
+ return undefined;
+ }
+ const details = (tc.failure && tc.failure[0]) || (tc.reason && tc.reason[0]);
+ if (!details) {
+ throw new Error('details is undefined');
+ }
+ let path;
+ let line;
+ if (details['stack-trace'] !== undefined && details['stack-trace'].length > 0) {
+ const src = (0, node_utils_1.getExceptionSource)(details['stack-trace'][0], this.options.trackedFiles, file => this.getRelativePath(file));
+ if (src) {
+ path = src.path;
+ line = src.line;
+ }
+ }
+ return {
+ path,
+ line,
+ message: details.message && details.message.length > 0 ? details.message[0] : '',
+ details: details['stack-trace'] && details['stack-trace'].length > 0 ? details['stack-trace'][0] : ''
+ };
+ }
+ getRelativePath(path) {
+ path = (0, path_utils_1.normalizeFilePath)(path);
+ const workDir = this.getWorkDir(path);
+ if (workDir !== undefined && path.startsWith(workDir)) {
+ path = path.substring(workDir.length);
+ }
+ return path;
+ }
+ getWorkDir(path) {
+ return (this.options.workDir ??
+ this.assumedWorkDir ??
+ (this.assumedWorkDir = (0, path_utils_1.getBasePath)(path, this.options.trackedFiles)));
+ }
+}
+exports.DotnetNunitLegacyParser = DotnetNunitLegacyParser;
+
+
/***/ }),
/***/ 5706:
diff --git a/src/main.ts b/src/main.ts
index 31eda0d..3fe6bb4 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -12,6 +12,7 @@ import {getReport} from './report/get-report'
import {DartJsonParser} from './parsers/dart-json/dart-json-parser'
import {DotnetNunitParser} from './parsers/dotnet-nunit/dotnet-nunit-parser'
+import {DotnetNunitLegacyParser} from './parsers/dotnet-nunit-legacy/dotnet-nunit-legacy-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'
@@ -229,6 +230,8 @@ class TestReporter {
return new DartJsonParser(options, 'dart')
case 'dotnet-nunit':
return new DotnetNunitParser(options)
+ case 'dotnet-nunit-legacy':
+ return new DotnetNunitLegacyParser(options)
case 'dotnet-trx':
return new DotnetTrxParser(options)
case 'flutter-json':
diff --git a/src/parsers/dotnet-nunit-legacy/dotnet-nunit-legacy-parser.ts b/src/parsers/dotnet-nunit-legacy/dotnet-nunit-legacy-parser.ts
new file mode 100644
index 0000000..0b65355
--- /dev/null
+++ b/src/parsers/dotnet-nunit-legacy/dotnet-nunit-legacy-parser.ts
@@ -0,0 +1,164 @@
+import {ParseOptions, TestParser} from '../../test-parser'
+import {parseStringPromise} from 'xml2js'
+
+import {NunitReport, TestCase, TestSuite} from './dotnet-nunit-legacy-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 DotnetNunitLegacyParser implements TestParser {
+ assumedWorkDir: string | undefined
+
+ constructor(readonly options: ParseOptions) {}
+
+ async parse(path: string, content: string): Promise {
+ const ju = await this.getNunitReport(path, content)
+ return this.getTestRunResult(path, ju)
+ }
+
+ private async getNunitReport(path: string, content: string): Promise {
+ 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-results'].$.time)
+
+ this.populateTestCasesRecursive(suites, [], nunit['test-results']['test-suite'])
+
+ return new TestRunResult(path, suites, time)
+ }
+
+ private populateTestCasesRecursive(
+ result: TestSuiteResult[],
+ suitePath: TestSuite[],
+ testSuites: TestSuite[] | undefined
+ ): void {
+ if (testSuites === undefined) {
+ return
+ }
+
+ for (const suite of testSuites) {
+ if (!suite['results']) {
+ continue
+ }
+
+ suitePath.push(suite)
+
+ const results = suite['results'][0]
+
+ this.populateTestCasesRecursive(result, suitePath, results['test-suite'])
+
+ const testcases = results['test-case']
+ if (testcases !== undefined) {
+ for (const testcase of testcases) {
+ this.addTestCase(result, suitePath, testcase)
+ }
+ }
+
+ suitePath.pop()
+ }
+ }
+
+ private addTestCase(result: TestSuiteResult[], suitePath: TestSuite[], testCase: TestCase): void {
+ // 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.$.time),
+ this.getTestCaseError(testCase)
+ )
+ )
+ }
+
+ private getTestExecutionResult(test: TestCase): TestExecutionResult {
+ if (test.$.result === 'Failed' || test.failure) return 'failed'
+ if (test.$.result === 'Skipped' || test.$.result === 'Ignored' || test.$.result === 'Inconclusive') return 'skipped'
+ return 'success'
+ }
+
+ private getTestCaseError(tc: TestCase): TestCaseError | undefined {
+ if (
+ !this.options.parseErrors ||
+ ((!tc.failure || tc.failure.length === 0) && (!tc.reason || tc.reason.length === 0))
+ ) {
+ return undefined
+ }
+
+ const details = (tc.failure && tc.failure[0]) || (tc.reason && tc.reason[0])
+ if (!details) {
+ throw new Error('details is undefined')
+ }
+
+ 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,
+ 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.substring(workDir.length)
+ }
+ return path
+ }
+
+ private getWorkDir(path: string): string | undefined {
+ return (
+ this.options.workDir ??
+ this.assumedWorkDir ??
+ (this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
+ )
+ }
+}
diff --git a/src/parsers/dotnet-nunit-legacy/dotnet-nunit-legacy-types.ts b/src/parsers/dotnet-nunit-legacy/dotnet-nunit-legacy-types.ts
new file mode 100644
index 0000000..254c8b9
--- /dev/null
+++ b/src/parsers/dotnet-nunit-legacy/dotnet-nunit-legacy-types.ts
@@ -0,0 +1,38 @@
+export interface NunitReport {
+ 'test-results': TestResults
+}
+
+export interface TestResults {
+ $: {
+ time: string
+ }
+ 'test-suite'?: TestSuite[]
+}
+
+export interface TestSuite {
+ $: {
+ type: string
+ name: string
+ }
+ results: TestSuiteResult[]
+}
+
+export interface TestSuiteResult {
+ 'test-case'?: TestCase[]
+ 'test-suite'?: TestSuite[]
+}
+
+export interface TestCase {
+ $: {
+ name: string
+ result: string
+ time: string
+ }
+ failure?: TestFailure[]
+ reason?: TestFailure[]
+}
+
+export interface TestFailure {
+ message?: string[]
+ 'stack-trace'?: string[]
+}