UNPKG

8.15 kBJavaScriptView Raw
1'use-strict';
2
3var xml = require('xml');
4var Base = require('mocha').reporters.Base;
5var fs = require('fs');
6var path = require('path');
7var debug = require('debug')('mocha-junit-reporter');
8var mkdirp = require('mkdirp');
9var md5 = require('md5');
10
11module.exports = MochaJUnitReporter;
12
13// A subset of invalid characters as defined in http://www.w3.org/TR/xml/#charsets that can occur in e.g. stacktraces
14var INVALID_CHARACTERS = ['\u001b'];
15
16function configureDefaults(options) {
17 debug(options);
18 options = options || {};
19 options = options.reporterOptions || {};
20 options.mochaFile = options.mochaFile || process.env.MOCHA_FILE || 'test-results.xml';
21 options.properties = options.properties || parsePropertiesFromEnv(process.env.PROPERTIES) || null;
22 options.toConsole = !!options.toConsole;
23 options.suiteTitleSeparedBy = options.suiteTitleSeparedBy || ' ';
24 options.rootSuiteTitle = 'Root Suite';
25
26 return options;
27}
28
29function defaultSuiteTitle(suite) {
30 if (suite.root && suite.title === '') {
31 return this._options.rootSuiteTitle;
32 }
33 return suite.title;
34}
35
36function fullSuiteTitle(suite) {
37 var parent = suite.parent;
38 var title = [ suite.title ];
39
40 while (parent) {
41 if (parent.root && parent.title === '') {
42 title.unshift(this._options.rootSuiteTitle);
43 } else {
44 title.unshift(parent.title);
45 }
46 parent = parent.parent;
47 }
48
49 return title.join(this._options.suiteTitleSeparedBy);
50}
51
52function isInvalidSuite(suite) {
53 return (!suite.root && suite.title === '') || (suite.tests.length === 0 && suite.suites.length === 0);
54}
55
56function parsePropertiesFromEnv(envValue) {
57 var properties = null;
58
59 if (process.env.PROPERTIES) {
60 properties = {}
61 var propertiesArray = process.env.PROPERTIES.split(',');
62 for (var i = 0; i < propertiesArray.length; i++) {
63 var propertyArgs = propertiesArray[i].split(':');
64 properties[propertyArgs[0]] = propertyArgs[1];
65 }
66 }
67
68 return properties;
69}
70
71function generateProperties(options) {
72 var properties = [];
73 for (var propertyName in options.properties) {
74 if (options.properties.hasOwnProperty(propertyName)) {
75 properties.push({
76 property: {
77 _attr: {
78 name: propertyName,
79 value: options.properties[propertyName]
80 }
81 }
82 })
83 }
84 }
85 return properties;
86}
87
88/**
89 * JUnit reporter for mocha.js.
90 * @module mocha-junit-reporter
91 * @param {EventEmitter} runner - the test runner
92 * @param {Object} options - mocha options
93 */
94function MochaJUnitReporter(runner, options) {
95 this._options = configureDefaults(options);
96 this._runner = runner;
97 this._generateSuiteTitle = this._options.useFullSuiteTitle ? fullSuiteTitle : defaultSuiteTitle;
98
99 var testsuites = [];
100
101 function lastSuite() {
102 return testsuites[testsuites.length - 1].testsuite;
103 }
104
105 // get functionality from the Base reporter
106 Base.call(this, runner);
107
108 // remove old results
109 this._runner.on('start', function() {
110 if (fs.existsSync(this._options.mochaFile)) {
111 debug('removing report file', this._options.mochaFile);
112 fs.unlinkSync(this._options.mochaFile);
113 }
114 }.bind(this));
115
116 this._runner.on('suite', function(suite) {
117 if (!isInvalidSuite(suite)) {
118 testsuites.push(this.getTestsuiteData(suite));
119 }
120 }.bind(this));
121
122 this._runner.on('pass', function(test) {
123 lastSuite().push(this.getTestcaseData(test));
124 }.bind(this));
125
126 this._runner.on('fail', function(test, err) {
127 lastSuite().push(this.getTestcaseData(test, err));
128 }.bind(this));
129
130 if (this._options.includePending) {
131 this._runner.on('pending', function(test) {
132 var testcase = this.getTestcaseData(test);
133
134 testcase.testcase.push({ skipped: null });
135 lastSuite().push(testcase);
136 }.bind(this));
137 }
138
139 this._runner.on('end', function(){
140 this.flush(testsuites);
141 }.bind(this));
142}
143
144/**
145 * Produces an xml node for a test suite
146 * @param {Object} suite - a test suite
147 * @return {Object} - an object representing the xml node
148 */
149MochaJUnitReporter.prototype.getTestsuiteData = function(suite) {
150 var testSuite = {
151 testsuite: [
152 {
153 _attr: {
154 name: this._generateSuiteTitle(suite),
155 timestamp: new Date().toISOString().slice(0,-5),
156 tests: suite.tests.length
157 }
158 }
159 ]
160 }
161
162 var properties = generateProperties(this._options);
163 if (properties.length) {
164 testSuite.testsuite.push({
165 properties: properties
166 });
167 }
168
169 return testSuite;
170};
171
172/**
173 * Produces an xml config for a given test case.
174 * @param {object} test - test case
175 * @param {object} err - if test failed, the failure object
176 * @returns {object}
177 */
178MochaJUnitReporter.prototype.getTestcaseData = function(test, err) {
179 var config = {
180 testcase: [{
181 _attr: {
182 name: test.fullTitle(),
183 time: (typeof test.duration === 'undefined') ? 0 : test.duration / 1000,
184 classname: test.title
185 }
186 }]
187 };
188 if (err) {
189 var message;
190 if (err.message && typeof err.message.toString === 'function') {
191 message = err.message + '';
192 } else if (typeof err.inspect === 'function') {
193 message = err.inspect() + '';
194 } else {
195 message = '';
196 }
197 var failureMessage = err.stack || message;
198 var failureElement = {
199 _cdata: this.removeInvalidCharacters(failureMessage)
200 };
201
202 config.testcase.push({failure: failureElement});
203 }
204 return config;
205};
206
207/**
208 * @param {string} input
209 * @returns {string} without invalid characters
210 */
211MochaJUnitReporter.prototype.removeInvalidCharacters = function(input){
212 return INVALID_CHARACTERS.reduce(function (text, invalidCharacter) {
213 return text.replace(new RegExp(invalidCharacter, 'g'), '');
214 }, input);
215};
216
217/**
218 * Writes xml to disk and ouputs content if "toConsole" is set to true.
219 * @param {Array.<Object>} testsuites - a list of xml configs
220 */
221MochaJUnitReporter.prototype.flush = function(testsuites){
222 var xml = this.getXml(testsuites);
223
224 this.writeXmlToDisk(xml, this._options.mochaFile);
225
226 if (this._options.toConsole === true) {
227 console.log(xml);
228 }
229};
230
231
232/**
233 * Produces an XML string from the given test data.
234 * @param {Array.<Object>} testsuites - a list of xml configs
235 * @returns {string}
236 */
237MochaJUnitReporter.prototype.getXml = function(testsuites) {
238 var totalSuitesTime = 0;
239 var totalTests = 0;
240 var stats = this._runner.stats;
241 var hasProperties = !!this._options.properties;
242
243 testsuites.forEach(function(suite) {
244 var _suiteAttr = suite.testsuite[0]._attr;
245 // properties are added before test cases so we want to make sure that we are grabbing test cases
246 // at the correct index
247 var _casesIndex = hasProperties ? 2 : 1;
248 var _cases = suite.testsuite.slice(_casesIndex);
249
250 _suiteAttr.failures = 0;
251 _suiteAttr.time = 0;
252 _suiteAttr.skipped = 0;
253
254 _cases.forEach(function(testcase) {
255 var lastNode = testcase.testcase[testcase.testcase.length - 1];
256
257 _suiteAttr.skipped += Number('skipped' in lastNode);
258 _suiteAttr.failures += Number('failure' in lastNode);
259 _suiteAttr.time += testcase.testcase[0]._attr.time;
260 });
261
262 if (!_suiteAttr.skipped) {
263 delete _suiteAttr.skipped;
264 }
265
266 totalSuitesTime += _suiteAttr.time;
267 totalTests += _suiteAttr.tests;
268 });
269
270 var rootSuite = {
271 _attr: {
272 name: 'Mocha Tests',
273 time: totalSuitesTime,
274 tests: totalTests,
275 failures: stats.failures
276 }
277 };
278
279 if (stats.pending) {
280 rootSuite._attr.skipped = stats.pending;
281 }
282
283 return xml({
284 testsuites: [ rootSuite ].concat(testsuites)
285 }, { declaration: true, indent: ' ' });
286};
287
288/**
289 * Writes a JUnit test report XML document.
290 * @param {string} xml - xml string
291 * @param {string} filePath - path to output file
292 */
293MochaJUnitReporter.prototype.writeXmlToDisk = function(xml, filePath){
294 if (filePath) {
295 if (filePath.indexOf('[hash]') !== -1) {
296 filePath = filePath.replace('[hash]', md5(xml));
297 }
298
299 debug('writing file to', filePath);
300 mkdirp.sync(path.dirname(filePath));
301
302 fs.writeFileSync(filePath, xml, 'utf-8');
303 debug('results written successfully');
304 }
305};