'use strict'; /** * @module XUnit */ /** * Module dependencies. */ var Base = require('./base'); var utils = require('../utils'); var inherits = utils.inherits; var fs = require('fs'); var escape = utils.escape; var mkdirp = require('mkdirp'); var path = require('path'); /** * Save timer references to avoid Sinon interfering (see GH-237). */ /* eslint-disable no-unused-vars, no-native-reassign */ var Date = global.Date; var setTimeout = global.setTimeout; var setInterval = global.setInterval; var clearTimeout = global.clearTimeout; var clearInterval = global.clearInterval; /* eslint-enable no-unused-vars, no-native-reassign */ /** * Expose `XUnit`. */ exports = module.exports = XUnit; /** * Initialize a new `XUnit` reporter. * * @public * @class * @memberof Mocha.reporters * @extends Mocha.reporters.Base * @api public * @param {Runner} runner */ function XUnit(runner, options) { Base.call(this, runner); var stats = this.stats; var tests = []; var self = this; // the name of the test suite, as it will appear in the resulting XML file var suiteName; // the default name of the test suite if none is provided var DEFAULT_SUITE_NAME = 'Mocha Tests'; if (options && options.reporterOptions) { if (options.reporterOptions.output) { if (!fs.createWriteStream) { throw new Error('file output not supported in browser'); } mkdirp.sync(path.dirname(options.reporterOptions.output)); self.fileStream = fs.createWriteStream(options.reporterOptions.output); } // get the suite name from the reporter options (if provided) suiteName = options.reporterOptions.suiteName; } // fall back to the default suite name suiteName = suiteName || DEFAULT_SUITE_NAME; runner.on('pending', function(test) { tests.push(test); }); runner.on('pass', function(test) { tests.push(test); }); runner.on('fail', function(test) { tests.push(test); }); runner.once('end', function() { self.write( tag( 'testsuite', { name: suiteName, tests: stats.tests, failures: stats.failures, errors: stats.failures, skipped: stats.tests - stats.failures - stats.passes, timestamp: new Date().toUTCString(), time: stats.duration / 1000 || 0 }, false ) ); tests.forEach(function(t) { self.test(t); }); self.write(''); }); } /** * Inherit from `Base.prototype`. */ inherits(XUnit, Base); /** * Override done to close the stream (if it's a file). * * @param failures * @param {Function} fn */ XUnit.prototype.done = function(failures, fn) { if (this.fileStream) { this.fileStream.end(function() { fn(failures); }); } else { fn(failures); } }; /** * Write out the given line. * * @param {string} line */ XUnit.prototype.write = function(line) { if (this.fileStream) { this.fileStream.write(line + '\n'); } else if (typeof process === 'object' && process.stdout) { process.stdout.write(line + '\n'); } else { console.log(line); } }; /** * Output tag for the given `test.` * * @param {Test} test */ XUnit.prototype.test = function(test) { var attrs = { classname: test.parent.fullTitle(), name: test.title, time: test.duration / 1000 || 0 }; if (test.state === 'failed') { var err = test.err; this.write( tag( 'testcase', attrs, false, tag( 'failure', {}, false, escape(err.message) + '\n' + escape(err.stack) ) ) ); } else if (test.isPending()) { this.write(tag('testcase', attrs, false, tag('skipped', {}, true))); } else { this.write(tag('testcase', attrs, true)); } }; /** * HTML tag helper. * * @param name * @param attrs * @param close * @param content * @return {string} */ function tag(name, attrs, close, content) { var end = close ? '/>' : '>'; var pairs = []; var tag; for (var key in attrs) { if (Object.prototype.hasOwnProperty.call(attrs, key)) { pairs.push(key + '="' + escape(attrs[key]) + '"'); } } tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end; if (content) { tag += content + '