karma-custom-reporter
Version:
A Karma plugin. Report results in MSTest trx format or custom xml format.
276 lines (239 loc) • 10.6 kB
JavaScript
var path = require('path');
var fs = require('fs');
var builder = require('xmlbuilder');
function defaultNameFormatter(browser, result) {
return browser.name + '_' + result.description;
}
var TRXReporter = function (baseReporterDecorator, config, emitter, logger, helper, formatError) {
var outputFile = config.outputFile;
var shortTestName = !!config.shortTestName;
var trimTimestamps = !!config.trimTimestamps;
var nameFormatter = config.nameFormatter || defaultNameFormatter;
var log = logger.create('reporter.trx');
var hostName = require('os').hostname();
var testRun;
var resultSummary;
var counters;
var testDefinitions;
var testListIdNotInAList;
var testEntries;
var results;
var times;
var customSectionElement;
var customSectionConfig = config.customSection ? {
asSeparateFile: false,
separateFileName: 'custom.xml',
rootElement: 'CustomSection',
testXmlFormater: null, // syntax for this formater is like: function (xmlSectionObject, data) {},
...config.customSection
} :
null;
var getTimestamp = function () {
// todo: use local time ?
return trimTimestamps ?
new Date().toISOString().substr(0, 19) :
new Date().toISOString();
};
var s4 = function () {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
var newGuid = function () {
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
};
var formatDuration = function (duration) {
duration = duration | 0;
var ms = duration % 1000;
duration -= ms;
var s = (duration / 1000) % 60;
duration -= s * 1000;
var m = (duration / 60000) % 60;
duration -= m * 60000;
var h = (duration / 3600000) % 24;
duration -= h * 3600000;
var d = duration / 86400000;
return (d > 0 ? d + '.' : '') +
(h < 10 ? '0' + h : h) + ':' +
(m < 10 ? '0' + m : m) + ':' +
(s < 10 ? '0' + s : s) + '.' +
(ms < 10 ? '00' + ms : ms < 100 ? '0' + ms : ms);
};
baseReporterDecorator(this);
this.onRunStart = function () {
var userName = process.env.USERNAME || process.env.USER || "karma-custom-trx";
var runStartTimestamp = getTimestamp();
testRun = builder.create("TestRun", {
version: '1.0',
encoding: 'UTF-8'
})
.att('id', newGuid())
.att('name', userName + '@' + hostName + ' ' + runStartTimestamp)
.att('runUser', userName)
.att('xmlns', 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010');
testRun.ele('TestSettings')
.att('name', 'Karma Test Run')
.att('id', newGuid());
times = testRun.ele('Times');
times.att('creation', runStartTimestamp);
times.att('queuing', runStartTimestamp);
times.att('start', runStartTimestamp);
resultSummary = testRun.ele('ResultSummary');
counters = resultSummary.ele('Counters');
testDefinitions = testRun.ele('TestDefinitions');
testListIdNotInAList = "8c84fa94-04c1-424b-9868-57a2d4851a1d";
var testLists = testRun.ele('TestLists');
testLists.ele('TestList')
.att('name', 'Results Not in a List')
.att('id', testListIdNotInAList);
// seems to be VS is expecting that exact id
testLists.ele('TestList')
.att('name', 'All Loaded Results')
.att('id', "19431567-8539-422a-85d7-44ee4e166bda");
testEntries = testRun.ele('TestEntries');
results = testRun.ele('Results');
if (customSectionConfig) {
if (customSectionConfig.asSeparateFile === true) {
customSectionElement = builder.create(customSectionConfig.rootElement, {
version: '1.0',
encoding: 'UTF-8'
})
.att('id', newGuid())
.att('name', userName + '@' + hostName + ' ' + runStartTimestamp)
.att('runUser', userName);
} else {
customSectionElement = testRun.ele(customSectionConfig.rootElement);
}
}
};
this.onBrowserStart = function (browser) {};
this.onBrowserComplete = function (browser) {
var result = browser.lastResult;
var passed = result.failed <= 0 && !result.error;
resultSummary.att('outcome', passed ? 'Passed' : 'Failed');
// todo: checkout if all theses numbers map well
counters.att('total', result.total)
.att('executed', result.total - result.skipped)
.att('passed', result.success)
.att('error', result.error ? 1 : 0)
.att('failed', result.failed);
// possible useful info:
// todo: result.disconnected => this seems to happen occasionally? => Possibly handle it!
// (result.netTime || 0) / 1000)
};
this.onRunComplete = function () {
times.att('finish', getTimestamp());
var xmlToOutput = testRun;
let customSectionXml = customSectionElement;
if (outputFile) {
helper.mkdirIfNotExists(path.dirname(outputFile), function () {
fs.writeFile(outputFile, xmlToOutput.end({
pretty: true
}), function (err) {
if (err) {
log.warn('Cannot write TRX testRun\n\t' + err.message);
} else {
log.debug('TRX results written to "%s".', outputFile);
}
});
});
}
if (customSectionConfig && customSectionConfig.asSeparateFile === true) {
fs.writeFile(customSectionConfig.separateFileName, customSectionXml.end({
pretty: true
}), function (err) {
if (err) {
log.warn(`Cannot write XML custom section: '${customSectionConfig.rootElement}' \n\t` + err.message);
} else {
log.debug('Custom XML results written to "%s".', customSectionConfig.separateFileName);
}
});
}
};
this.specSuccess = this.specSkipped = this.specFailure = function (browser, result) {
var unitTestId = newGuid();
var unitTestName = shortTestName ?
result.description :
nameFormatter(browser, result);
var className = result.suite.join('.');
var codeBase = className + '.' + unitTestName;
var unitTest = testDefinitions.ele('UnitTest')
.att('name', unitTestName)
.att('id', unitTestId);
var executionId = newGuid();
unitTest.ele('Execution')
.att('id', executionId);
unitTest.ele('TestMethod')
.att('codeBase', codeBase)
.att('name', unitTestName)
.att('className', className);
testEntries.ele('TestEntry')
.att('testId', unitTestId)
.att('executionId', executionId)
.att('testListId', testListIdNotInAList);
var unitTestResult = results.ele('UnitTestResult')
.att('executionId', executionId)
.att('testId', unitTestId)
.att('testName', unitTestName)
.att('computerName', hostName)
.att('duration', formatDuration(result.time > 0 ? result.time : 0))
.att('startTime', getTimestamp())
.att('endTime', getTimestamp())
// todo: are there other test types?
.att('testType', '13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b') // that guid seems to represent 'unit test'
.att('outcome', result.skipped ? 'NotExecuted' : (result.success ? 'Passed' : 'Failed'))
.att('testListId', testListIdNotInAList);
if (customSectionConfig.testXmlFormater && customSectionConfig.testXmlFormater.call) {
customSectionConfig.testXmlFormater.call(this, customSectionElement, {
unitTestId,
unitTestName,
className,
executionId,
codeBase,
hostName,
duration: formatDuration(result.time > 0 ? result.time : 0),
result
});
} else {
var customUnitTest = customSectionElement.ele('UnitTest')
.att('name', unitTestName)
.att('id', unitTestId);
customUnitTest.ele('Execution')
.att('id', newGuid());
customUnitTest.ele('TestMethod')
.att('codeBase', codeBase)
.att('name', unitTestName)
.att('className', className);
}
if (!result.success) {
unitTestResult.ele('Output')
.ele('ErrorInfo')
.ele('Message', formatError(result.log[0]))
}
};
};
TRXReporter.$inject = ['baseReporterDecorator', 'config.customTrxReporter', 'emitter', 'logger',
'helper', 'formatError'
];
function extractJiraIdXmlFormater(xmlBuilderObject, data, overrideFn) {
if (overrideFn && typeof (overrideFn) === 'function') { // for overriding current behaviour
overrideFn(xmlBuilderObject, data);
return;
}
const regexForJiraId = /(?<=\[).+?(?=\])/gi; // regex for extracting jira id from unit test name
let matches = (data.codeBase || '').match(regexForJiraId); // extract all matches. There could be multiple describes(or its) that have Jira id set in description
matches = matches && matches.length ? matches.filter(m => /\d+/.test(m)) : []; // filter matches that contains numbers
if (matches.length) { // if any match
const requirementValue = `AltID_${matches.reverse()[0]}`; // get the deepest Jira Id from unit test name
const testcaseTag = xmlBuilderObject.ele('testcase')
.att('classname', data.className.replace(regexForJiraId, '').replace('[]', ''))
.att('name', data.unitTestName.replace(regexForJiraId, '').replace('[]', ''));
const requirementsTag = testcaseTag.ele('requirements');
requirementsTag.ele('requirement', requirementValue);
}
}
// PUBLISH DI MODULE
module.exports = {
'reporter:custom-trx': ['type', TRXReporter],
extractJiraIdXmlFormater: extractJiraIdXmlFormater
};