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