diff --git a/README.md b/README.md
index 6d34e2c4..c4de5e6c 100644
--- a/README.md
+++ b/README.md
@@ -137,6 +137,7 @@ jobs:
# Format of test results. Supported options:
# dart-json
+ # dotnet-nunit
# dotnet-trx
# flutter-json
# java-junit
diff --git a/__tests__/__outputs__/dotnet-nunit.md b/__tests__/__outputs__/dotnet-nunit.md
new file mode 100644
index 00000000..904e7d2c
--- /dev/null
+++ b/__tests__/__outputs__/dotnet-nunit.md
@@ -0,0 +1,28 @@
+![Tests failed](https://img.shields.io/badge/tests-3%20passed%2C%205%20failed%2C%201%20skipped-critical)
+## ❌ fixtures/dotnet-nunit.xml
+**9** tests were completed in **230ms** with **3** passed, **5** failed and **1** skipped.
+|Test suite|Passed|Failed|Skipped|Time|
+|:---|---:|---:|---:|---:|
+|[DotnetTests.NUnitV3Tests.dll.DotnetTests.XUnitTests](#r0s0)|3✅|5❌|1⚪|69ms|
+### ❌ DotnetTests.NUnitV3Tests.dll.DotnetTests.XUnitTests
+```
+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
+
+```
\ No newline at end of file
diff --git a/__tests__/__snapshots__/dotnet-nunit.test.ts.snap b/__tests__/__snapshots__/dotnet-nunit.test.ts.snap
new file mode 100644
index 00000000..60d55f2c
--- /dev/null
+++ b/__tests__/__snapshots__/dotnet-nunit.test.ts.snap
@@ -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": [
+ TestSuiteResult {
+ "groups": [
+ TestGroupResult {
+ "name": "CalculatorTests",
+ "tests": [
+ TestCaseResult {
+ "error": undefined,
+ "name": "Is_Even_Number(2)",
+ "result": "success",
+ "time": 0.622,
+ },
+ TestCaseResult {
+ "error": {
+ "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": 1.098,
+ },
+ TestCaseResult {
+ "error": {
+ "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": 22.805,
+ },
+ TestCaseResult {
+ "error": {
+ "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.528,
+ },
+ TestCaseResult {
+ "error": {
+ "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": 28.162,
+ },
+ TestCaseResult {
+ "error": undefined,
+ "name": "Passing_Test",
+ "result": "success",
+ "time": 0.23800000000000002,
+ },
+ TestCaseResult {
+ "error": undefined,
+ "name": "Passing_Test_With_Description",
+ "result": "success",
+ "time": 0.135,
+ },
+ TestCaseResult {
+ "error": undefined,
+ "name": "Skipped_Test",
+ "result": "skipped",
+ "time": 0.398,
+ },
+ TestCaseResult {
+ "error": {
+ "details": "",
+ "line": undefined,
+ "message": "",
+ "path": undefined,
+ },
+ "name": "Timeout_Test",
+ "result": "failed",
+ "time": 14.949,
+ },
+ ],
+ },
+ ],
+ "name": "DotnetTests.NUnitV3Tests.dll.DotnetTests.XUnitTests",
+ "totalTime": undefined,
+ },
+ ],
+ "totalTime": 230.30800000000002,
+}
+`;
diff --git a/__tests__/dotnet-nunit.test.ts b/__tests__/dotnet-nunit.test.ts
new file mode 100644
index 00000000..e0b47783
--- /dev/null
+++ b/__tests__/dotnet-nunit.test.ts
@@ -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)
+ })
+})
diff --git a/__tests__/fixtures/dotnet-nunit.xml b/__tests__/fixtures/dotnet-nunit.xml
new file mode 100644
index 00000000..e08a9f33
--- /dev/null
+++ b/__tests__/fixtures/dotnet-nunit.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/__tests__/fixtures/external/nunit-sample.xml b/__tests__/fixtures/external/nunit-sample.xml
new file mode 100644
index 00000000..b2615600
--- /dev/null
+++ b/__tests__/fixtures/external/nunit-sample.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/action.yml b/action.yml
index 8d9d7282..3ea90c15 100644
--- a/action.yml
+++ b/action.yml
@@ -26,6 +26,7 @@ inputs:
description: |
Format of test results. Supported options:
- dart-json
+ - dotnet-nunit
- dotnet-trx
- flutter-json
- java-junit
diff --git a/dist/index.js b/dist/index.js
index 715691f7..ff99141e 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -261,6 +261,7 @@ const local_file_provider_1 = __nccwpck_require__(9399);
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_trx_parser_1 = __nccwpck_require__(2664);
const java_junit_parser_1 = __nccwpck_require__(676);
const jest_junit_parser_1 = __nccwpck_require__(1113);
@@ -425,6 +426,8 @@ class TestReporter {
switch (reporter) {
case 'dart-json':
return new dart_json_parser_1.DartJsonParser(options, 'dart');
+ case 'dotnet-nunit':
+ return new dotnet_nunit_parser_1.DotnetNunitParser(options);
case 'dotnet-trx':
return new dotnet_trx_parser_1.DotnetTrxParser(options);
case 'flutter-json':
@@ -722,6 +725,136 @@ function isMessageEvent(event) {
exports.isMessageEvent = isMessageEvent;
+/***/ }),
+
+/***/ 5706:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.DotnetNunitParser = 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 DotnetNunitParser {
+ constructor(options) {
+ this.options = options;
+ }
+ parse(path, content) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const ju = yield this.getNunitReport(path, content);
+ return this.getTestRunResult(path, ju);
+ });
+ }
+ getNunitReport(path, content) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ return (yield (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-run'].$.duration) * 1000;
+ this.populateTestCasesRecursive(suites, [], nunit['test-run']['test-suite']);
+ return new test_results_1.TestRunResult(path, suites, time);
+ }
+ populateTestCasesRecursive(result, suitePath, testSuites) {
+ if (testSuites === undefined) {
+ return;
+ }
+ for (const suite of testSuites) {
+ suitePath.push(suite);
+ this.populateTestCasesRecursive(result, suitePath, suite['test-suite']);
+ const testcases = suite['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.$.duration) * 1000, this.getTestCaseError(testCase)));
+ }
+ getTestExecutionResult(test) {
+ if (test.$.result === 'Failed' || test.failure)
+ return 'failed';
+ if (test.$.result === 'Skipped')
+ return 'skipped';
+ return 'success';
+ }
+ getTestCaseError(tc) {
+ 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 = (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.substr(workDir.length);
+ }
+ return path;
+ }
+ getWorkDir(path) {
+ var _a, _b;
+ return ((_b = (_a = this.options.workDir) !== null && _a !== void 0 ? _a : this.assumedWorkDir) !== null && _b !== void 0 ? _b : (this.assumedWorkDir = (0, path_utils_1.getBasePath)(path, this.options.trackedFiles)));
+ }
+}
+exports.DotnetNunitParser = DotnetNunitParser;
+
+
/***/ }),
/***/ 2664:
diff --git a/package.json b/package.json
index 3f156422..a52075ae 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"all": "npm run build && npm run format && npm run lint && npm run package && npm test",
"dart-fixture": "cd \"reports/dart\" && dart test --file-reporter=\"json:../../__tests__/fixtures/dart-json.json\"",
"dotnet-fixture": "dotnet test reports/dotnet/DotnetTests.XUnitTests --logger \"trx;LogFileName=../../../../__tests__/fixtures/dotnet-trx.trx\"",
+ "dotnet-nunit-fixture": "nunit.exe reports/dotnet/DotnetTests.NUnitV3Tests/bin/Debug/netcoreapp3.1/DotnetTests.NUnitV3Tests.dll --result=__tests__/fixtures/dotnet-nunit.xml",
"jest-fixture": "cd \"reports/jest\" && npm test",
"mocha-fixture": "cd \"reports/mocha\" && npm test"
},
diff --git a/reports/dotnet/DotnetTests.NUnitV3Tests/CalculatorTests.cs b/reports/dotnet/DotnetTests.NUnitV3Tests/CalculatorTests.cs
new file mode 100644
index 00000000..9452ae93
--- /dev/null
+++ b/reports/dotnet/DotnetTests.NUnitV3Tests/CalculatorTests.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Threading;
+using DotnetTests.Unit;
+using NUnit.Framework;
+
+namespace DotnetTests.XUnitTests
+{
+ public class CalculatorTests
+ {
+ private readonly Calculator _calculator = new Calculator();
+
+ [Test]
+ public void Passing_Test()
+ {
+ Assert.That(_calculator.Sum(1, 1), Is.EqualTo(2));
+ }
+
+ [Test(Description = "Some description")]
+ public void Passing_Test_With_Description()
+ {
+ Assert.That(2, Is.EqualTo(2));
+ }
+
+ [Test]
+ public void Failing_Test()
+ {
+ Assert.That(_calculator.Sum(1, 1), Is.EqualTo(3));
+ }
+
+ [Test]
+ public void Exception_In_TargetTest()
+ {
+ _calculator.Div(1, 0);
+ }
+
+ [Test]
+ public void Exception_In_Test()
+ {
+ throw new Exception("Test");
+ }
+
+ [Test]
+ [Timeout(1)]
+ public void Timeout_Test()
+ {
+ Thread.Sleep(100);
+ }
+
+ [Test]
+ [Ignore("Skipped")]
+ public void Skipped_Test()
+ {
+ throw new Exception("Test");
+ }
+
+ [Theory]
+ [TestCase(2)]
+ [TestCase(3)]
+ public void Is_Even_Number(int i)
+ {
+ Assert.True(i % 2 == 0);
+ }
+ }
+}
diff --git a/reports/dotnet/DotnetTests.NUnitV3Tests/DotnetTests.NUnitV3Tests.csproj b/reports/dotnet/DotnetTests.NUnitV3Tests/DotnetTests.NUnitV3Tests.csproj
new file mode 100644
index 00000000..932ff5ba
--- /dev/null
+++ b/reports/dotnet/DotnetTests.NUnitV3Tests/DotnetTests.NUnitV3Tests.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netcoreapp3.1
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/reports/dotnet/DotnetTests.sln b/reports/dotnet/DotnetTests.sln
index 1c0bbeb6..7c5ff87c 100644
--- a/reports/dotnet/DotnetTests.sln
+++ b/reports/dotnet/DotnetTests.sln
@@ -9,6 +9,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BCAC3B31
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetTests.XUnitTests", "DotnetTests.XUnitTests\DotnetTests.XUnitTests.csproj", "{F8607EDB-D25D-47AA-8132-38ACA242E845}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetTests.NUnitV3Tests", "DotnetTests.NUnitV3Tests\DotnetTests.NUnitV3Tests.csproj", "{81023ED7-56CB-47E9-86C5-9125A0873C55}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -23,12 +25,17 @@ Global
{F8607EDB-D25D-47AA-8132-38ACA242E845}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F8607EDB-D25D-47AA-8132-38ACA242E845}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F8607EDB-D25D-47AA-8132-38ACA242E845}.Release|Any CPU.Build.0 = Release|Any CPU
+ {81023ED7-56CB-47E9-86C5-9125A0873C55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {81023ED7-56CB-47E9-86C5-9125A0873C55}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {81023ED7-56CB-47E9-86C5-9125A0873C55}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {81023ED7-56CB-47E9-86C5-9125A0873C55}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F8607EDB-D25D-47AA-8132-38ACA242E845} = {BCAC3B31-ADB1-4221-9D5B-182EE868648C}
+ {81023ED7-56CB-47E9-86C5-9125A0873C55} = {BCAC3B31-ADB1-4221-9D5B-182EE868648C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6ED5543C-74AA-4B21-8050-943550F3F66E}
diff --git a/src/main.ts b/src/main.ts
index 8f481afe..d50427b1 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -11,6 +11,7 @@ import {getAnnotations} from './report/get-annotations'
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 {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'
@@ -214,6 +215,8 @@ class TestReporter {
switch (reporter) {
case 'dart-json':
return new DartJsonParser(options, 'dart')
+ case 'dotnet-nunit':
+ return new DotnetNunitParser(options)
case 'dotnet-trx':
return new DotnetTrxParser(options)
case 'flutter-json':
diff --git a/src/parsers/dotnet-nunit/dotnet-nunit-parser.ts b/src/parsers/dotnet-nunit/dotnet-nunit-parser.ts
new file mode 100644
index 00000000..4bc27f1f
--- /dev/null
+++ b/src/parsers/dotnet-nunit/dotnet-nunit-parser.ts
@@ -0,0 +1,151 @@
+import {ParseOptions, TestParser} from '../../test-parser'
+import {parseStringPromise} from 'xml2js'
+
+import {NunitReport, TestCase, 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 {
+ 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-run'].$.duration) * 1000
+
+ 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
+ }
+
+ for (const suite of testSuites) {
+ suitePath.push(suite)
+
+ this.populateTestCasesRecursive(result, suitePath, suite['test-suite'])
+
+ const testcases = suite['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.$.duration) * 1000,
+ 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,
+ 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))
+ )
+ }
+}
diff --git a/src/parsers/dotnet-nunit/dotnet-nunit-types.ts b/src/parsers/dotnet-nunit/dotnet-nunit-types.ts
new file mode 100644
index 00000000..ec1696cd
--- /dev/null
+++ b/src/parsers/dotnet-nunit/dotnet-nunit-types.ts
@@ -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[]
+}
diff --git a/src/parsers/rspec-json/rspec-json-parser.ts b/src/parsers/rspec-json/rspec-json-parser.ts
index bf404a12..bf723935 100644
--- a/src/parsers/rspec-json/rspec-json-parser.ts
+++ b/src/parsers/rspec-json/rspec-json-parser.ts
@@ -1,4 +1,3 @@
-import { Console } from 'console'
import {ParseOptions, TestParser} from '../../test-parser'
import {
TestCaseError,
diff --git a/src/parsers/rspec-json/rspec-json-types.ts b/src/parsers/rspec-json/rspec-json-types.ts
index 495af897..dca00b76 100644
--- a/src/parsers/rspec-json/rspec-json-types.ts
+++ b/src/parsers/rspec-json/rspec-json-types.ts
@@ -17,7 +17,7 @@ export interface RspecExample {
exception?: RspecException
}
-type TestStatus = 'passed' | 'failed' | 'pending';
+type TestStatus = 'passed' | 'failed' | 'pending'
export interface RspecException {
class: string