test-results-parser
Version:
Parse test results from JUnit, TestNG, xUnit, cucumber and many more
246 lines (205 loc) • 8.28 kB
JavaScript
const { getJsonFromXMLFile } = require('../helpers/helper');
const path = require('path');
const TestResult = require('../models/TestResult');
const TestSuite = require('../models/TestSuite');
const TestCase = require('../models/TestCase');
const TestAttachment = require('../models/TestAttachment');
const RESULT_MAP = {
Passed: "PASS",
Failed: "FAIL",
NotExecuted: "SKIP",
}
/**
*
* @param {*} rawElement
* @param {TestCase | TestSuite} test_element
*/
function populateMetaData(rawElement, test_element) {
if (rawElement.TestCategory && rawElement.TestCategory.TestCategoryItem) {
let rawCategories = rawElement.TestCategory.TestCategoryItem;
for (let i = 0; i < rawCategories.length; i++) {
let categoryName = rawCategories[i]["@_TestCategory"];
test_element.tags.push(categoryName);
// create comma-delimited list of categories
if (test_element.metadata["Categories"]) {
test_element.metadata["Categories"] = test_element.metadata["Categories"].concat(",", categoryName)
} else {
test_element.metadata["Categories"] = categoryName;
}
}
}
// as per https://github.com/microsoft/vstest/issues/2480:
// - properties are supported by the XSD but are not included in TRX Visual Studio output
// - including support for properties because third-party extensions might generate this data
if (rawElement.Properties) {
let rawProperties = rawElement.Properties.Property;
for (let i = 0; i < rawProperties.length; i++) {
let key = rawProperties[i].Key ?? "not-set";
let val = rawProperties[i].Value ?? "";
map[key] = val;
}
}
}
function populateAttachments(rawResultElement, attachments, testRunName) {
// attachments are in /TestRun/Results/UnitTestResult/ResultFiles/ResultFile[@path]
if (rawResultElement.ResultFiles && rawResultElement.ResultFiles.ResultFile) {
let executionId = rawResultElement["@_executionId"];
let rawAttachments = rawResultElement.ResultFiles.ResultFile;
for (let i = 0; i < rawAttachments.length; i++) {
let filePath = rawAttachments[i]["@_path"];
if (filePath) {
// file path is relative to testresults.trx
// stored in ./<testrunname>/in/<executionId>/path
let attachment = new TestAttachment();
attachment.path = path.join(testRunName, "In", executionId, ...(filePath.split(/[\\/]/g)));
attachments.push(attachment);
}
}
}
}
function getTestResultDuration(rawTestResult) {
// durations are represented in a timeformat with 7 digit microsecond precision
const durationString = rawTestResult["@_duration"];
if (!durationString) return 0;
// Split the duration into hours, minutes, seconds, and microseconds
const [hours, minutes, seconds] = durationString.split(':');
// Convert everything to milliseconds
const totalMilliseconds =
parseInt(hours) * 3600000 + // hours to ms
parseInt(minutes) * 60000 + // minutes to ms
parseFloat(seconds) * 1000; // seconds to ms
return totalMilliseconds.toFixed(4);
}
function getTestCaseName(rawDefinition) {
if (rawDefinition.TestMethod) {
let className = rawDefinition.TestMethod["@_className"];
let name = rawDefinition.TestMethod["@_name"];
// attempt to produce fully-qualified name
if (className) {
className = className.split(",")[0]; // handle strong-name scenario (typeName, assembly, culture, version)
return className.concat(".", name);
} else {
return name;
}
} else {
throw new Error("Unrecognized TestDefinition");
}
}
function getTestSuiteName(testCase) {
// assume testCase.name is full-qualified namespace.classname.methodname
let index = testCase.name.lastIndexOf(".");
return testCase.name.substring(0, index);
}
function getTestRunName(rawTestRun) {
// testrun.name contains '@', spaces and ':'
let name = rawTestRun["@_name"];
if (name) {
return name.replace(/[ @:]/g, '_');
}
return '';
}
function getTestCase(rawTestResult, definitionMap, testRunName) {
let id = rawTestResult["@_testId"];
if (definitionMap.has(id)) {
var rawDefinition = definitionMap.get(id);
var testCase = new TestCase();
testCase.id = id;
testCase.name = getTestCaseName(rawDefinition);
testCase.status = RESULT_MAP[rawTestResult["@_outcome"]];
testCase.duration = getTestResultDuration(rawTestResult);
// collect error messages
if (rawTestResult.Output && rawTestResult.Output.ErrorInfo) {
testCase.setFailure(rawTestResult.Output.ErrorInfo.Message);
testCase.stack_trace = rawTestResult.Output.ErrorInfo.StackTrace ?? '';
}
// populate attachments
populateAttachments(rawTestResult, testCase.attachments, testRunName);
// populate meta
populateMetaData(rawDefinition, testCase);
return testCase;
} else {
throw new Error(`Unrecognized testId ${id ?? ''}`);
}
}
function getTestDefinitionsMap(rawTestDefinitions) {
let map = new Map();
// assume all definitions are 'UnitTest' elements
if (rawTestDefinitions.UnitTest) {
let rawUnitTests = rawTestDefinitions.UnitTest;
for (let i = 0; i < rawUnitTests.length; i++) {
let rawUnitTest = rawUnitTests[i];
let id = rawUnitTest["@_id"];
if (id) {
map.set(id, rawUnitTest);
}
}
}
return map;
}
function getTestResults(rawTestResults) {
let results = [];
// assume all results are UnitTestResult elements
if (rawTestResults.UnitTestResult) {
let unitTests = rawTestResults.UnitTestResult;
for (let i = 0; i < unitTests.length; i++) {
results.push(unitTests[i]);
}
}
return results;
}
function getTestSuites(rawTestRun) {
// test attachments are stored in a testrun specific folder <name>/in/<executionid>/<computername>
const testRunName = getTestRunName(rawTestRun);
// outcomes + durations are stored in /TestRun/TestResults/*
const testResults = getTestResults(rawTestRun.Results);
// test names and details are stored in /TestRun/TestDefinitions/*
const testDefinitions = getTestDefinitionsMap(rawTestRun.TestDefinitions);
// trx does not include suites, so we'll reverse engineer them by
// grouping results from the same className
let suiteMap = new Map();
for (let i = 0; i < testResults.length; i++) {
let rawTestResult = testResults[i];
let testCase = getTestCase(rawTestResult, testDefinitions, testRunName);
let suiteName = getTestSuiteName(testCase);
if (!suiteMap.has(suiteName)) {
let suite = new TestSuite();
suite.name = suiteName;
suiteMap.set(suiteName, suite);
}
suiteMap.get(suiteName).cases.push(testCase);
}
var result = [];
for (let suite of suiteMap.values()) {
suite.total = suite.cases.length;
suite.passed = suite.cases.filter(i => i.status == "PASS").length;
suite.failed = suite.cases.filter(i => i.status == "FAIL").length;
suite.skipped = suite.cases.filter(i => i.status == "SKIP").length;
suite.errors = suite.cases.filter(i => i.status == "ERROR").length;
suite.duration = suite.cases.reduce((total, test) => { return total + test.duration }, 0);
suite.status = (suite.failed + suite.errors) > 0 ? "FAIL" : "PASS";
result.push(suite);
}
return result;
}
function getTestResult(json) {
const rawTestRun = json.TestRun;
let result = new TestResult();
result.id = rawTestRun["@_id"];
result.suites.push(...getTestSuites(rawTestRun));
// calculate totals
result.total = result.suites.reduce((total, suite) => { return total + suite.total }, 0);
result.passed = result.suites.reduce((total, suite) => { return total + suite.passed }, 0);
result.failed = result.suites.reduce((total, suite) => { return total + suite.failed }, 0);
result.skipped = result.suites.reduce((total, suite) => { return total + suite.skipped }, 0);
result.errors = result.suites.reduce((total, suite) => { return total + suite.errors }, 0);
result.duration = result.suites.reduce((total, suite) => { return total + suite.duration }, 0);
result.status = (result.failed + result.errors) > 0 ? "FAIL" : "PASS";
return result;
}
function parse(file) {
const json = getJsonFromXMLFile(file);
return getTestResult(json);
}
module.exports = {
parse
}