From 02ccabffdb80b44c987a61d262b5cf77d8cf7615 Mon Sep 17 00:00:00 2001 From: Yedidya Schwartz <36074789+yedidyas@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:46:51 +0200 Subject: [PATCH 1/5] add StdOut to UnitTestResult object --- src/parsers/dotnet-trx/dotnet-trx-types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/parsers/dotnet-trx/dotnet-trx-types.ts b/src/parsers/dotnet-trx/dotnet-trx-types.ts index 97dbf8ac..7f1bb845 100644 --- a/src/parsers/dotnet-trx/dotnet-trx-types.ts +++ b/src/parsers/dotnet-trx/dotnet-trx-types.ts @@ -51,6 +51,7 @@ export interface UnitTestResult { export interface Output { ErrorInfo: ErrorInfo[] + StdOut: string } export interface ErrorInfo { Message: string[] From c60bb05bd3615ea207c90d5f128a847cf2e4d336 Mon Sep 17 00:00:00 2001 From: Yedidya Schwartz <36074789+yedidyas@users.noreply.github.com> Date: Tue, 14 Jan 2025 20:07:39 +0200 Subject: [PATCH 2/5] change stdout to string[] --- src/parsers/dotnet-trx/dotnet-trx-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parsers/dotnet-trx/dotnet-trx-types.ts b/src/parsers/dotnet-trx/dotnet-trx-types.ts index 7f1bb845..501ae5f7 100644 --- a/src/parsers/dotnet-trx/dotnet-trx-types.ts +++ b/src/parsers/dotnet-trx/dotnet-trx-types.ts @@ -51,7 +51,7 @@ export interface UnitTestResult { export interface Output { ErrorInfo: ErrorInfo[] - StdOut: string + StdOut: string[] } export interface ErrorInfo { Message: string[] From 46691ebeafbbec4f41da6b5deeab0dd97c0f8806 Mon Sep 17 00:00:00 2001 From: Yedidya Schwartz <36074789+yedidyas@users.noreply.github.com> Date: Tue, 14 Jan 2025 20:19:55 +0200 Subject: [PATCH 3/5] use stdout --- src/parsers/dotnet-trx/dotnet-trx-parser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parsers/dotnet-trx/dotnet-trx-parser.ts b/src/parsers/dotnet-trx/dotnet-trx-parser.ts index a81d4a23..8804705c 100644 --- a/src/parsers/dotnet-trx/dotnet-trx-parser.ts +++ b/src/parsers/dotnet-trx/dotnet-trx-parser.ts @@ -147,6 +147,7 @@ export class DotnetTrxParser implements TestParser { const message = test.error.Message[0] const stackTrace = test.error.StackTrace[0] + const stdOut = test.error.StdOut?.join('\n') || '' let path let line @@ -160,7 +161,7 @@ export class DotnetTrxParser implements TestParser { path, line, message, - details: `${message}\n${stackTrace}` + details: `${message}\n${stackTrace}\n${stdOut}` } } From 2db037664f289aa2f5d8a12d25cd2d68e5d4a8e5 Mon Sep 17 00:00:00 2001 From: yedidyas Date: Tue, 14 Jan 2025 20:36:13 +0200 Subject: [PATCH 4/5] fix stdout issue --- src/parsers/dotnet-trx/dotnet-trx-parser.ts | 174 ++++++++++---------- src/parsers/dotnet-trx/dotnet-trx-types.ts | 2 +- 2 files changed, 91 insertions(+), 85 deletions(-) diff --git a/src/parsers/dotnet-trx/dotnet-trx-parser.ts b/src/parsers/dotnet-trx/dotnet-trx-parser.ts index 8804705c..47491900 100644 --- a/src/parsers/dotnet-trx/dotnet-trx-parser.ts +++ b/src/parsers/dotnet-trx/dotnet-trx-parser.ts @@ -1,10 +1,10 @@ -import {parseStringPromise} from 'xml2js' +import { parseStringPromise } from 'xml2js'; -import {ErrorInfo, Outcome, TrxReport, UnitTest, UnitTestResult} from './dotnet-trx-types' -import {ParseOptions, TestParser} from '../../test-parser' +import { ErrorInfo, Outcome, TrxReport, UnitTest, UnitTestResult } from './dotnet-trx-types'; +import { ParseOptions, TestParser } from '../../test-parser'; -import {getBasePath, normalizeFilePath} from '../../utils/path-utils' -import {parseIsoDate, parseNetDuration} from '../../utils/parse-utils' +import { getBasePath, normalizeFilePath } from '../../utils/path-utils'; +import { parseIsoDate, parseNetDuration } from '../../utils/parse-utils'; import { TestExecutionResult, @@ -12,12 +12,12 @@ import { TestSuiteResult, TestGroupResult, TestCaseResult, - TestCaseError -} from '../../test-results' + TestCaseError, +} from '../../test-results'; class TestClass { constructor(readonly name: string) {} - readonly tests: Test[] = [] + readonly tests: Test[] = []; } class Test { @@ -25,162 +25,168 @@ class Test { readonly name: string, readonly outcome: Outcome, readonly duration: number, - readonly error?: ErrorInfo + readonly error?: ErrorInfo, + readonly stdOut?: string ) {} get result(): TestExecutionResult | undefined { switch (this.outcome) { case 'Passed': - return 'success' + return 'success'; case 'NotExecuted': - return 'skipped' + return 'skipped'; case 'Failed': - return 'failed' + return 'failed'; } } } export class DotnetTrxParser implements TestParser { - assumedWorkDir: string | undefined + assumedWorkDir: string | undefined; constructor(readonly options: ParseOptions) {} async parse(path: string, content: string): Promise { - const trx = await this.getTrxReport(path, content) - const tc = this.getTestClasses(trx) - const tr = this.getTestRunResult(path, trx, tc) - tr.sort(true) - return tr + const trx = await this.getTrxReport(path, content); + const tc = this.getTestClasses(trx); + const tr = this.getTestRunResult(path, trx, tc); + tr.sort(true); + return tr; } private async getTrxReport(path: string, content: string): Promise { try { - return (await parseStringPromise(content)) as TrxReport + return (await parseStringPromise(content)) as TrxReport; } catch (e) { - throw new Error(`Invalid XML at ${path}\n\n${e}`) + throw new Error(`Invalid XML at ${path}\n\n${e}`); } } private getTestClasses(trx: TrxReport): TestClass[] { if (trx.TestRun.TestDefinitions === undefined || trx.TestRun.Results === undefined) { - return [] + return []; } - const unitTests: {[id: string]: UnitTest} = {} + const unitTests: { [id: string]: UnitTest } = {}; for (const td of trx.TestRun.TestDefinitions) { for (const ut of td.UnitTest) { - unitTests[ut.$.id] = ut + unitTests[ut.$.id] = ut; } } - const unitTestsResults = trx.TestRun.Results.flatMap(r => r.UnitTestResult).flatMap(result => ({ - result, - test: unitTests[result.$.testId] - })) + const unitTestsResults = trx.TestRun.Results.flatMap((r) => + r.UnitTestResult.map((result) => ({ + result, + test: unitTests[result.$.testId], + })) + ); - const testClasses: {[name: string]: TestClass} = {} + const testClasses: { [name: string]: TestClass } = {}; for (const r of unitTestsResults) { - const className = r.test.TestMethod[0].$.className - let tc = testClasses[className] + const className = r.test.TestMethod[0].$.className; + let tc = testClasses[className]; if (tc === undefined) { - tc = new TestClass(className) - testClasses[tc.name] = tc + tc = new TestClass(className); + testClasses[tc.name] = tc; } - const error = this.getErrorInfo(r.result) - const durationAttr = r.result.$.duration - const duration = durationAttr ? parseNetDuration(durationAttr) : 0 - const resultTestName = r.result.$.testName + const error = this.getErrorInfo(r.result); + const durationAttr = r.result.$.duration; + const duration = durationAttr ? parseNetDuration(durationAttr) : 0; + + const stdOut = r.result.Output?.[0]?.StdOut?.join('\n') || ''; // Extract StdOut + + const resultTestName = r.result.$.testName; const testName = resultTestName.startsWith(className) && resultTestName[className.length] === '.' ? resultTestName.substr(className.length + 1) - : resultTestName - - const test = new Test(testName, r.result.$.outcome, duration, error) - tc.tests.push(test) + : resultTestName; + + const test = new Test(testName, r.result.$.outcome, duration, error, stdOut); + tc.tests.push(test); } - const result = Object.values(testClasses) - return result + const result = Object.values(testClasses); + return result; } private getTestRunResult(path: string, trx: TrxReport, testClasses: TestClass[]): TestRunResult { - const times = trx.TestRun.Times[0].$ - const totalTime = parseIsoDate(times.finish).getTime() - parseIsoDate(times.start).getTime() - - const suites = testClasses.map(testClass => { - const tests = testClass.tests.map(test => { - const error = this.getError(test) - return new TestCaseResult(test.name, test.result, test.duration, error) - }) - const group = new TestGroupResult(null, tests) - return new TestSuiteResult(testClass.name, [group]) - }) - - return new TestRunResult(path, suites, totalTime) + const times = trx.TestRun.Times[0].$; + const totalTime = parseIsoDate(times.finish).getTime() - parseIsoDate(times.start).getTime(); + + const suites = testClasses.map((testClass) => { + const tests = testClass.tests.map((test) => { + const error = this.getError(test); + return new TestCaseResult(test.name, test.result, test.duration, error); + }); + const group = new TestGroupResult(null, tests); + return new TestSuiteResult(testClass.name, [group]); + }); + + return new TestRunResult(path, suites, totalTime); } private getErrorInfo(testResult: UnitTestResult): ErrorInfo | undefined { if (testResult.$.outcome !== 'Failed') { - return undefined + return undefined; } - const output = testResult.Output - const error = output?.length > 0 && output[0].ErrorInfo?.length > 0 ? output[0].ErrorInfo[0] : undefined - return error + const output = testResult.Output; + const error = output?.length > 0 && output[0].ErrorInfo?.length > 0 ? output[0].ErrorInfo[0] : undefined; + return error; } private getError(test: Test): TestCaseError | undefined { if (!this.options.parseErrors || !test.error) { - return undefined + return undefined; } - const error = test.error + const error = test.error; if ( !Array.isArray(error.Message) || error.Message.length === 0 || !Array.isArray(error.StackTrace) || error.StackTrace.length === 0 ) { - return undefined + return undefined; } - const message = test.error.Message[0] - const stackTrace = test.error.StackTrace[0] - const stdOut = test.error.StdOut?.join('\n') || '' - let path - let line + const message = error.Message[0]; + const stackTrace = error.StackTrace[0]; + const stdOut = test.stdOut || ''; // Use StdOut from Test object + let path; + let line; - const src = this.exceptionThrowSource(stackTrace) + const src = this.exceptionThrowSource(stackTrace); if (src) { - path = src.path - line = src.line + path = src.path; + line = src.line; } return { path, line, message, - details: `${message}\n${stackTrace}\n${stdOut}` - } + details: `${message}\n${stackTrace}\n${stdOut}`, + }; } - private exceptionThrowSource(stackTrace: string): {path: string; line: number} | undefined { - const lines = stackTrace.split(/\r*\n/) - const re = / in (.+):line (\d+)$/ - const {trackedFiles} = this.options + private exceptionThrowSource(stackTrace: string): { path: string; line: number } | undefined { + const lines = stackTrace.split(/\r*\n/); + const re = / in (.+):line (\d+)$/; + const { trackedFiles } = this.options; for (const str of lines) { - const match = str.match(re) + const match = str.match(re); if (match !== null) { - const [_, fileStr, lineStr] = match - const filePath = normalizeFilePath(fileStr) - const workDir = this.getWorkDir(filePath) + const [_, fileStr, lineStr] = match; + const filePath = normalizeFilePath(fileStr); + const workDir = this.getWorkDir(filePath); if (workDir) { - const file = filePath.substr(workDir.length) + const file = filePath.substr(workDir.length); if (trackedFiles.includes(file)) { - const line = parseInt(lineStr) - return {path: file, line} + const line = parseInt(lineStr); + return { path: file, line }; } } } @@ -192,6 +198,6 @@ export class DotnetTrxParser implements TestParser { this.options.workDir ?? this.assumedWorkDir ?? (this.assumedWorkDir = getBasePath(path, this.options.trackedFiles)) - ) + ); } } diff --git a/src/parsers/dotnet-trx/dotnet-trx-types.ts b/src/parsers/dotnet-trx/dotnet-trx-types.ts index 501ae5f7..fe485f1e 100644 --- a/src/parsers/dotnet-trx/dotnet-trx-types.ts +++ b/src/parsers/dotnet-trx/dotnet-trx-types.ts @@ -51,7 +51,7 @@ export interface UnitTestResult { export interface Output { ErrorInfo: ErrorInfo[] - StdOut: string[] + StdOut: string[] | undefined; } export interface ErrorInfo { Message: string[] From 7f713bd00e3297481e28b758ec2b5461aa43d06d Mon Sep 17 00:00:00 2001 From: yedidyas Date: Tue, 14 Jan 2025 20:56:21 +0200 Subject: [PATCH 5/5] fix --- src/parsers/dotnet-trx/dotnet-trx-parser.ts | 10 ++-- src/report/get-report.ts | 53 +++++++++++++-------- src/test-results.ts | 10 ++-- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/parsers/dotnet-trx/dotnet-trx-parser.ts b/src/parsers/dotnet-trx/dotnet-trx-parser.ts index 47491900..22f07a75 100644 --- a/src/parsers/dotnet-trx/dotnet-trx-parser.ts +++ b/src/parsers/dotnet-trx/dotnet-trx-parser.ts @@ -151,9 +151,10 @@ export class DotnetTrxParser implements TestParser { return undefined; } - const message = error.Message[0]; - const stackTrace = error.StackTrace[0]; - const stdOut = test.stdOut || ''; // Use StdOut from Test object + const message = test.error.Message[0]; + const stackTrace = test.error.StackTrace[0]; + const stdOut = test.stdOut || ''; // Add StdOut + let path; let line; @@ -167,7 +168,8 @@ export class DotnetTrxParser implements TestParser { path, line, message, - details: `${message}\n${stackTrace}\n${stdOut}`, + details: `${message}\n${stackTrace}`, + stdOut, // Include StdOut in TestCaseError }; } diff --git a/src/report/get-report.ts b/src/report/get-report.ts index 99bf270e..abdd2560 100644 --- a/src/report/get-report.ts +++ b/src/report/get-report.ts @@ -230,45 +230,56 @@ function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOpt function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: number, options: ReportOptions): string[] { if (options.listTests === 'failed' && ts.result !== 'failed') { - return [] + return []; } - const groups = ts.groups + const groups = ts.groups; if (groups.length === 0) { - return [] + return []; } - const sections: string[] = [] + const sections: string[] = []; - const tsName = ts.name - const tsSlug = makeSuiteSlug(runIndex, suiteIndex) - const tsNameLink = `${tsName}` - const icon = getResultIcon(ts.result) - sections.push(`### ${icon}\xa0${tsNameLink}`) + const tsName = ts.name; + const tsSlug = makeSuiteSlug(runIndex, suiteIndex); + const tsNameLink = `${tsName}`; + const icon = getResultIcon(ts.result); + sections.push(`### ${icon}\xa0${tsNameLink}`); - sections.push('```') + sections.push('```'); for (const grp of groups) { if (grp.name) { - sections.push(grp.name) + sections.push(grp.name); } - const space = grp.name ? ' ' : '' + const space = grp.name ? ' ' : ''; for (const tc of grp.tests) { - const result = getResultIcon(tc.result) - sections.push(`${space}${result} ${tc.name}`) + const result = getResultIcon(tc.result); + sections.push(`${space}${result} ${tc.name}`); if (tc.error) { - const lines = (tc.error.message ?? getFirstNonEmptyLine(tc.error.details)?.trim()) - ?.split(/\r?\n/g) - .map(l => '\t' + l) - if (lines) { - sections.push(...lines) + const errorDetails: string[] = []; + + if (tc.error.message) { + errorDetails.push(tc.error.message); + } + if (tc.error.details) { + errorDetails.push(tc.error.details); + } + if (tc.error.stdOut) { + errorDetails.push(`StdOut:\n${tc.error.stdOut}`); // Include StdOut in report } + + const lines = errorDetails.flatMap((detail) => + detail.split(/\r?\n/g).map((line) => `\t${line}`) + ); + sections.push(...lines); } } } - sections.push('```') + sections.push('```'); - return sections + return sections; } + function makeRunSlug(runIndex: number): {id: string; link: string} { // use prefix to avoid slug conflicts after escaping the paths return slug(`r${runIndex}`) diff --git a/src/test-results.ts b/src/test-results.ts index bca8c416..1f6343a7 100644 --- a/src/test-results.ts +++ b/src/test-results.ts @@ -129,8 +129,10 @@ export class TestCaseResult { export type TestExecutionResult = 'success' | 'skipped' | 'failed' | undefined export interface TestCaseError { - path?: string - line?: number - message?: string - details: string + path?: string; + line?: number; + message: string; + details: string; + stdOut?: string; } +