UNPKG

circleci-node-toolkit

Version:

Util scripts and testing reporters for working with CircleCI

259 lines (212 loc) 7.14 kB
// Mostly copied from https://github.com/michaelleeallen/mocha-junit-reporter, with added tweaks to output. 'use-strict'; var xml = require('xml'); var Base = require('mocha').reporters.Base; var fs = require('fs'); var path = require('path'); var debug = require('debug')('mocha-junit-reporter'); var mkdirp = require('mkdirp'); var Spec = require('mocha').reporters.Spec; module.exports = MochaJUnitReporter; // A subset of invalid characters as defined in http://www.w3.org/TR/xml/#charsets that can occur in e.g. stacktraces var INVALID_CHARACTERS = ['\u001b']; function configureDefaults(options) { debug(options); options = options || {}; options = options.reporterOptions || {}; options.mochaFile = options.mochaFile || process.env.MOCHA_FILE || 'test-results.xml'; options.toConsole = !! options.toConsole; options.suiteTitleSeparedBy = options.suiteTitleSeparedBy || ' '; return options; } function defaultSuiteTitle(suite) { return suite.title; } function fullSuiteTitle(suite, options) { var parent = suite.parent; var title = [suite.title]; while (parent) { title.unshift(parent.title); parent = parent.parent; } return title.join(options.suiteTitleSeparedBy); } function isInvalidSuite(suite) { return suite.title === '' || suite.tests.length === 0 && suite.suites.length === 0; } /** * JUnit reporter for mocha.js. * @module mocha-junit-reporter * @param {EventEmitter} runner - the test runner * @param {Object} options - mocha options */ function MochaJUnitReporter(runner, options) { new Spec(runner, options); this._options = configureDefaults(options); this._runner = runner; this._generateSuiteTitle = this._options.useFullSuiteTitle ? fullSuiteTitle : defaultSuiteTitle; var testsuites = []; function lastSuite() { return testsuites[testsuites.length - 1].testsuite; } // get functionality from the Base reporter Base.call(this, runner); // remove old results this._runner.on('start', function() { if (fs.existsSync(this._options.mochaFile)) { debug('removing report file', this._options.mochaFile); fs.unlinkSync(this._options.mochaFile); } }.bind(this)); this._runner.on('suite', function(suite) { if (suite.root) { suite.title = ''; } if (!isInvalidSuite(suite)) { testsuites.push(this.getTestsuiteData(suite)); } }.bind(this)); this._runner.on('pass', function(test) { lastSuite().push(this.getTestcaseData(test)); }.bind(this)); this._runner.on('fail', function(test, err) { lastSuite().push(this.getTestcaseData(test, err)); }.bind(this)); if (this._options.includePending) { this._runner.on('pending', function(test) { var testcase = this.getTestcaseData(test); testcase.testcase.push({ skipped: null }); lastSuite().push(testcase); }.bind(this)); } this._runner.on('end', function() { this.flush(testsuites); }.bind(this)); } /** * Produces an xml node for a test suite * @param {Object} suite - a test suite * @return {Object} - an object representing the xml node */ MochaJUnitReporter.prototype.getTestsuiteData = function(suite) { var attr = { name: this._generateSuiteTitle(suite, this._options), timestamp: new Date().toISOString().slice(0, -5), tests: suite.tests.length }; if (suite.root) attr.file = suite.suites[0].file else attr.file = suite.file; return { testsuite: [{ _attr: attr }] }; }; /** * Produces an xml config for a given test case. * @param {object} test - test case * @param {object} err - if test failed, the failure object * @returns {object} */ MochaJUnitReporter.prototype.getTestcaseData = function(test, err) { var config = { testcase: [{ _attr: { name: path.basename(test.file) + ': ' + test.fullTitle(), time: (typeof test.duration === 'undefined') ? 0 : test.duration / 1000, classname: test.title } }] }; if (err) { var failureElement = { _cdata: this.removeInvalidCharacters(err.stack) }; config.testcase.push({ failure: failureElement }); } return config; }; /** * @param {string} input * @returns {string} without invalid characters */ MochaJUnitReporter.prototype.removeInvalidCharacters = function(input) { return INVALID_CHARACTERS.reduce(function(text, invalidCharacter) { return text.replace(new RegExp(invalidCharacter, 'g'), ''); }, input); }; /** * Writes xml to disk and ouputs content if "toConsole" is set to true. * @param {Array.<Object>} testsuites - a list of xml configs */ MochaJUnitReporter.prototype.flush = function(testsuites) { var xml = this.getXml(testsuites); this.writeXmlToDisk(xml, this._options.mochaFile); if (this._options.toConsole === true) { console.log(xml); } }; /** * Produces an XML string from the given test data. * @param {Array.<Object>} testsuites - a list of xml configs * @returns {string} */ MochaJUnitReporter.prototype.getXml = function(testsuites) { var totalSuitesTime = 0; var totalTests = 0; var stats = this._runner.stats; testsuites.forEach(function(suite) { var _suiteAttr = suite.testsuite[0]._attr; var _cases = suite.testsuite.slice(1); _suiteAttr.failures = 0; _suiteAttr.time = 0; _suiteAttr.skipped = 0; _cases.forEach(function(testcase) { var lastNode = testcase.testcase[testcase.testcase.length - 1]; _suiteAttr.skipped += Number('skipped' in lastNode); _suiteAttr.failures += Number('failure' in lastNode); _suiteAttr.time += testcase.testcase[0]._attr.time; }); if (!_suiteAttr.skipped) { delete _suiteAttr.skipped; } totalSuitesTime += _suiteAttr.time; totalTests += _suiteAttr.tests; }); var rootFile = testsuites[0].testsuite[0]._attr.file; var rootSuite = { _attr: { name: path.basename(rootFile), time: totalSuitesTime, tests: totalTests, failures: stats.failures, file: rootFile } }; if (stats.pending) { rootSuite._attr.skipped = stats.pending; } return xml({ testsuites: [rootSuite].concat(testsuites) }, { declaration: true, indent: ' ' }); }; /** * Writes a JUnit test report XML document. * @param {string} xml - xml string * @param {string} filePath - path to output file */ MochaJUnitReporter.prototype.writeXmlToDisk = function(xml, filePath) { if (filePath) { debug('writing file to', filePath); mkdirp.sync(path.dirname(filePath)); fs.writeFileSync(filePath, xml, 'utf-8'); debug('results written successfully'); } };