Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add StdOut to UnitTestResult object #549

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 92 additions & 83 deletions src/parsers/dotnet-trx/dotnet-trx-parser.ts
Original file line number Diff line number Diff line change
@@ -1,185 +1,194 @@
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,
TestRunResult,
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 {
constructor(
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<TestRunResult> {
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<TrxReport> {
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]
let path
let line
const message = test.error.Message[0];
const stackTrace = test.error.StackTrace[0];
const stdOut = test.stdOut || ''; // Add StdOut

const src = this.exceptionThrowSource(stackTrace)
let path;
let line;

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}`
}
details: `${message}\n${stackTrace}`,
stdOut, // Include StdOut in TestCaseError
};
}

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 };
}
}
}
Expand All @@ -191,6 +200,6 @@ export class DotnetTrxParser implements TestParser {
this.options.workDir ??
this.assumedWorkDir ??
(this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
)
);
}
}
1 change: 1 addition & 0 deletions src/parsers/dotnet-trx/dotnet-trx-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface UnitTestResult {

export interface Output {
ErrorInfo: ErrorInfo[]
StdOut: string[] | undefined;
}
export interface ErrorInfo {
Message: string[]
Expand Down
53 changes: 32 additions & 21 deletions src/report/get-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<a id="${tsSlug.id}" href="${options.baseUrl + tsSlug.link}">${tsName}</a>`
const icon = getResultIcon(ts.result)
sections.push(`### ${icon}\xa0${tsNameLink}`)
const tsName = ts.name;
const tsSlug = makeSuiteSlug(runIndex, suiteIndex);
const tsNameLink = `<a id="${tsSlug.id}" href="${options.baseUrl + tsSlug.link}">${tsName}</a>`;
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}`)
Expand Down
10 changes: 6 additions & 4 deletions src/test-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}