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 @@ +![Tests failed](https://img.shields.io/badge/tests-3%20passed%2C%205%20failed%2C%202%20skipped-critical) +|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[] +}