UNPKG

junit-converter

Version:

Test report files to JUnit XML converter

222 lines (190 loc) 8.52 kB
import * as fs from 'fs'; import * as path from 'path'; import * as _ from 'lodash'; import xmlFormat from 'xml-formatter'; import * as xsltProcessor from 'xslt-processor'; import {toJson, toXml} from 'p3x-xml2json'; import { ConverterOptions, XmlParserOptions, TestSuites, TestCase } from './interfaces'; export class XmlConverter { /** * Sort test cases by classname and restructure test suites * @param options Converter configuration * @param json Parsed test suites JSON * @returns Modified JSON with sorted test cases */ private static sortByClassname(options: ConverterOptions, json: TestSuites): TestSuites { if (options.testType !== 'trx' && !options.splitByClassname) { return json; } if (!json?.testsuites?.[0]?.testsuite?.[0]?.testcase) { return json; } // Sort test cases by classname and name json.testsuites[0].testsuite[0].testcase = _.sortBy( json.testsuites[0].testsuite[0].testcase, ['classname', 'name'] ); // Extract unique classnames let classnames = _.map(json.testsuites[0].testsuite[0].testcase, 'classname') .filter((value: string | undefined, index: number, array: (string | undefined)[]): boolean => array.indexOf(value) === index ) as string[]; classnames = _.sortBy(classnames, (o) => o); // Calculate total time const time = _.sumBy( json.testsuites[0].testsuite, (suite) => _.sumBy(suite.testcase, (testCase: TestCase) => Number(testCase.time)) ); json.testsuites[0].time = time; json.testsuites[0].testsuite[0].time = String(time); if (classnames.length > 1) { const testSuites = classnames.map((classname) => { const testcases = _.filter(json.testsuites[0].testsuite[0].testcase, { classname, }); const suiteTime = _.sumBy(testcases, (testCase: TestCase) => Number(testCase.time)); const failures = testcases.filter((testCase: TestCase) => testCase.status === 'Failed').length; const skipped = testcases.filter((testCase: TestCase) => testCase.status === 'Skipped').length; return { name: classname, tests: `${testcases.length}`, failures: `${failures}`, skipped: `${skipped}`, time: `${suiteTime}`, testcase: testcases, }; }); json.testsuites[0].testsuite = testSuites; } else { json.testsuites[0].testsuite[0].time = String(time); json.testsuites[0].testsuite[0].name = json.testsuites[0].testsuite[0].testcase[0].classname; } return json; } /** * Enhance XML string and optionally restructure by classname * @param options Converter configuration * @param xml XML string to process * @returns Formatted XML string * @throws {Error} If JSON parsing fails or no test suites found */ private static enhanceXml(options: ConverterOptions, xml: string): string { const xmlParserOptions: XmlParserOptions = { object: true, arrayNotation: true, sanitize: false, reversible: true, }; let json: TestSuites; try { json = toJson(xml, xmlParserOptions) as TestSuites; } catch (e) { throw new Error( `Could not parse JSON from converted XML ${options.testFile}.\n ${ e instanceof Error ? e.message : String(e) }` ); } // Validate that test suites exist if ( !json?.testsuites?.length || !json.testsuites[0]?.testsuite?.length ) { console.warn('No test suites found, skipping JUnit file creation.'); return null as any; } // Save intermediate JSON if requested if (options.saveIntermediateFiles) { const fileName = `${path.parse(options.testFile).name}-converted.json`; const reportDir = options.reportDir || './report'; fs.writeFileSync( path.join(reportDir, fileName), JSON.stringify(json, null, 2), 'utf8' ); } // Sort test suites if (json.testsuites[0].testsuite[0].file && json.testsuites[0].testsuite[0].name) { json.testsuites[0].testsuite = _.sortBy(json.testsuites[0].testsuite, ['file', 'name']); } else if (json.testsuites[0].testsuite[0].name) { json.testsuites[0].testsuite = _.sortBy(json.testsuites[0].testsuite, ['name']); } // Apply classname sorting if configured json = this.sortByClassname(options, json); // Convert back to XML and format xmlParserOptions.sanitize = true; const xmlOutput = toXml(json, xmlParserOptions); return options.minify ? xmlFormat.minify(xmlOutput, { forceSelfClosingEmptyTag: true }) : xmlFormat(xmlOutput, { forceSelfClosingEmptyTag: true }); } /** * Process converted XML and optionally split by classname * @param {ConverterOptions} options Converter configuration * @param {Promise<string>} xmlString Converted XML string * @returns Formatted XML string * @throws {Error} If XML formatting fails */ private static async processXml(options: ConverterOptions, xmlString: string): Promise<string> { let parsedXml: string; // Save intermediate XML if requested if (options.saveIntermediateFiles) { const fileName = `${path.parse(options.testFile).name}-converted.xml`; const reportDir = options.reportDir || './report'; fs.writeFileSync(path.join(reportDir, fileName), xmlString, 'utf8'); } try { parsedXml = options.minify ? xmlFormat.minify(xmlString, { forceSelfClosingEmptyTag: true }) : xmlFormat(xmlString, { forceSelfClosingEmptyTag: true }); } catch (e) { throw new Error( `\nXML parsed from ${options.testFile} is empty or invalid \n${ e instanceof Error ? e.message : String(e) }` ); } // return this // .enhanceXml(options, parsedXml) // .replaceAll('&apos;', '\'') // .replaceAll('&amp;gt;', '&gt;') // .replaceAll('&amp;lt;', '&lt;'); if (options.testType !== 'trx' && !options.splitByClassname) { return parsedXml.replaceAll('&#xD;', ''); } else { return this .enhanceXml(options, parsedXml) .replaceAll('&amp;#xD;', '') .replaceAll('&amp;#xD;', '') .replaceAll('&amp;gt;', '&gt;') .replaceAll('&amp;lt;', '&lt;'); } } /** * Convert test report to JUnit XML format * @param {ConverterOptions} options Converter configuration * @returns {Promise<string>} Async formatted JUnit XML string * @throws {Error} If XSLT processing fails */ static async convert(options: ConverterOptions): Promise<string> { const xsltFile = `../xslt/${options.testType}-junit.xslt`; const xsltString = fs.readFileSync(path.join(__dirname, xsltFile), 'utf8'); const xmlString = fs.readFileSync(options.testFile, 'utf8'); const xslt = new xsltProcessor.Xslt(); const xmlParser = new xsltProcessor.XmlParser(); let xml: string; try { xml = await xslt.xsltProcess( xmlParser.xmlParse(xmlString), xmlParser.xmlParse(xsltString) ); } catch (e) { throw new Error( `Could not process XML file ${options.testFile} using XSLT ${xsltFile} \n${ e instanceof Error ? e.message : String(e) }` ); } return await this.processXml(options, xml); } }