Skip to content

Commit

Permalink
Feature: Ant mode and Jenkins mode (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
clayreimann committed Apr 17, 2019
1 parent 90624a7 commit 6d89b63
Show file tree
Hide file tree
Showing 6 changed files with 590 additions and 70 deletions.
23 changes: 16 additions & 7 deletions README.md
@@ -1,7 +1,7 @@
# JUnit Reporter for Mocha

[![Build Status](https://travis-ci.org/michaelleeallen/mocha-junit-reporter.svg?branch=master)](https://travis-ci.org/michaelleeallen/mocha-junit-reporter)
[![npm](https://img.shields.io/npm/v/mocha-junit-reporter.svg?maxAge=2592000)](https://www.npmjs.com/package/mocha-junit-reporter)
[![Build Status](travis-badge)](travis-build)
[![npm](npm-badge)](npm-listing)

Produces JUnit-style XML test results.

Expand Down Expand Up @@ -47,7 +47,7 @@ var mocha = new Mocha({

You can also add properties to the report under `testsuite`. This is useful if you want your CI environment to add extra build props to the report for analytics purposes

```
```xml
<testsuites>
<testsuite>
<properties>
Expand Down Expand Up @@ -104,10 +104,10 @@ var mocha = new Mocha({

Here is an example of the XML output when using the `testCaseSwitchClassnameAndName` option:

| value | XML output |
|----------------------------------|--------|
| `true` | `<testcase name="should behave like so" classname="Super Suite should behave like so">` |
| `false` (default) | `<testcase name="Super Suite should behave like so" classname="should behave like so">` |
| value | XML output |
|-------------------|------------|
| `true` | `<testcase name="should behave like so" classname="Super Suite should behave like so">` |
| `false` (default) | `<testcase name="Super Suite should behave like so" classname="should behave like so">` |

You can also configure the `testsuites.name` attribute by setting `reporterOptions.testsuitesTitle` and the root suite's `name` attribute by setting `reporterOptions.rootSuiteTitle`.

Expand Down Expand Up @@ -192,3 +192,12 @@ output line 2
| testsuitesTitle | the name for the `testsuites` tag (defaults to 'Mocha Tests') |
| outputs | if set to truthy value will include console output and console error output |
| attachments | if set to truthy value will attach files to report in `JUnit Attachments Plugin` format (after console outputs, if any) |
| antMode | set to truthy value to return xml compatible with [Ant JUnit schema][ant-schema] |
| antHostname | hostname to use when running in `antMode` will default to environment `HOSTNAME` |
| jenkinsMode | if set to truthy value will return xml that will display nice results in Jenkins |

[travis-badge]: https://travis-ci.org/michaelleeallen/mocha-junit-reporter.svg?branch=master
[travis-build]: https://travis-ci.org/michaelleeallen/mocha-junit-reporter
[npm-badge]: https://img.shields.io/npm/v/mocha-junit-reporter.svg?maxAge=2592000
[npm-listing]: https://www.npmjs.com/package/mocha-junit-reporter
[ant-schema]: http://windyroad.org/dl/Open%20Source/JUnit.xsd
204 changes: 143 additions & 61 deletions index.js
Expand Up @@ -19,19 +19,72 @@ function configureDefaults(options) {
debug(options);
options = options || {};
options = options.reporterOptions || {};
options.mochaFile = options.mochaFile || process.env.MOCHA_FILE || 'test-results.xml';
options.properties = options.properties || parsePropertiesFromEnv(process.env.PROPERTIES) || null;
options.attachments = options.attachments || process.env.ATTACHMENTS || false;
options.mochaFile = getSetting(options.mochaFile, 'MOCHA_FILE', 'test-results.xml');
options.attachments = getSetting(options.attachments, 'ATTACHMENTS', false);
options.antMode = getSetting(options.antMode, 'ANT_MODE', false);
options.jenkinsMode = getSetting(options.jenkinsMode, 'JENKINS_MODE', false);
options.properties = getSetting(options.properties, 'PROPERTIES', null, parsePropertiesFromEnv);
options.toConsole = !!options.toConsole;
options.testCaseSwitchClassnameAndName = options.testCaseSwitchClassnameAndName || false;
options.suiteTitleSeparedBy = options.suiteTitleSeparedBy || ' ';
options.suiteTitleSeparatedBy = options.suiteTitleSeparatedBy || options.suiteTitleSeparedBy || ' ';
options.rootSuiteTitle = options.rootSuiteTitle || 'Root Suite';
options.testsuitesTitle = options.testsuitesTitle || 'Mocha Tests';

if (options.antMode) {
updateOptionsForAntMode(options);
}

if (options.jenkinsMode) {
updateOptionsForJenkinsMode(options);
}

options.suiteTitleSeparedBy = options.suiteTitleSeparedBy || ' ';
options.suiteTitleSeparatedBy = options.suiteTitleSeparatedBy || options.suiteTitleSeparedBy;

return options;
}

function updateOptionsForAntMode(options) {
options.antHostname = getSetting(options.antHostname, 'ANT_HOSTNAME', process.env.HOSTNAME);

if (!options.properties) {
options.properties = {};
}
}

function updateOptionsForJenkinsMode(options) {
if (options.useFullSuiteTitle === undefined) {
options.useFullSuiteTitle = true;
}
debug('jenkins mode - testCaseSwitchClassnameAndName', options.testCaseSwitchClassnameAndName);
if (options.testCaseSwitchClassnameAndName === undefined) {
options.testCaseSwitchClassnameAndName = true;
}
if (options.suiteTitleSeparedBy === undefined) {
options.suiteTitleSeparedBy = '.';
}
}

/**
* Determine an option value.
* 1. If `key` is present in the environment, then use the environment value
* 2. If `value` is specified, then use that value
* 3. Fall back to `defaultVal`
* @module mocha-junit-reporter
* @param {Object} value - the value from the reporter options
* @param {String} key - the environment variable to check
* @param {Object} defaultVal - the fallback value
* @param {function} transform - a transformation function to be used when loading values from the environment
*/
function getSetting(value, key, defaultVal, transform) {
if (process.env[key] !== undefined) {
var envVal = process.env[key];
return (typeof transform === 'function') ? transform(envVal) : envVal;
}
if (value !== undefined) {
return value;
}
return defaultVal;
}

function defaultSuiteTitle(suite) {
if (suite.root && suite.title === '') {
return stripAnsi(this._options.rootSuiteTitle);
Expand Down Expand Up @@ -60,35 +113,39 @@ function isInvalidSuite(suite) {
}

function parsePropertiesFromEnv(envValue) {
var properties = null;

if (envValue) {
properties = {};
var propertiesArray = envValue.split(',');
for (var i = 0; i < propertiesArray.length; i++) {
var propertyArgs = propertiesArray[i].split(':');
properties[propertyArgs[0]] = propertyArgs[1];
}
debug('Parsing from env', envValue);
return envValue.split(',').reduce(function(properties, prop) {
var property = prop.split(':');
properties[property[0]] = property[1];
return properties;
}, []);
}

return properties;
return null;
}

function generateProperties(options) {
var properties = [];
for (var propertyName in options.properties) {
if (options.properties.hasOwnProperty(propertyName)) {
properties.push({
property: {
_attr: {
name: propertyName,
value: options.properties[propertyName]
}
}
});
}
var props = options.properties;
if (!props) {
return [];
}
return Object.keys(props).reduce(function(properties, name) {
var value = props[name];
properties.push({ property: { _attr: { name: name, value: value } } });
return properties;
}, []);
}

function getJenkinsClassname (test) {
debug('Building jenkins classname for', test);
var parent = test.parent;
var titles = [];
while (parent) {
parent.title && titles.unshift(parent.title);
parent = parent.parent;
}
return properties;
return titles.join('.');
}

/**
Expand All @@ -101,6 +158,7 @@ function MochaJUnitReporter(runner, options) {
this._options = configureDefaults(options);
this._runner = runner;
this._generateSuiteTitle = this._options.useFullSuiteTitle ? fullSuiteTitle : defaultSuiteTitle;
this._antId = 0;

var testsuites = [];

Expand Down Expand Up @@ -153,29 +211,35 @@ function MochaJUnitReporter(runner, options) {
* @return {Object} - an object representing the xml node
*/
MochaJUnitReporter.prototype.getTestsuiteData = function(suite) {
var testSuite = {
testsuite: [
{
_attr: {
name: this._generateSuiteTitle(suite),
timestamp: new Date().toISOString().slice(0,-5),
tests: suite.tests.length
}
}
]
var antMode = this._options.antMode;

var _attr = {
name: this._generateSuiteTitle(suite),
timestamp: new Date().toISOString().slice(0,-5),
tests: suite.tests.length
};
var testSuite = { testsuite: [ { _attr: _attr } ] };


if(suite.file) {
testSuite.testsuite[0]._attr.file = suite.file;
}

var properties = generateProperties(this._options);
if (properties.length) {
if (properties.length || antMode) {
testSuite.testsuite.push({
properties: properties
});
}

if (antMode) {
_attr.package = _attr.name;
_attr.hostname = this._options.antHostname;
_attr.id = this._antId;
_attr.errors = 0;
this._antId += 1;
}

return testSuite;
};

Expand All @@ -186,10 +250,11 @@ MochaJUnitReporter.prototype.getTestsuiteData = function(suite) {
* @returns {object}
*/
MochaJUnitReporter.prototype.getTestcaseData = function(test, err) {
var jenkinsMode = this._options.jenkinsMode;
var flipClassAndName = this._options.testCaseSwitchClassnameAndName;
var name = stripAnsi(test.fullTitle());
var name = stripAnsi(jenkinsMode ? getJenkinsClassname(test) : test.fullTitle());
var classname = stripAnsi(test.title);
var config = {
var testcase = {
testcase: [{
_attr: {
name: flipClassAndName ? classname : name,
Expand All @@ -213,11 +278,11 @@ MochaJUnitReporter.prototype.getTestcaseData = function(test, err) {
));
}
if (systemOutLines.length > 0) {
config.testcase.push({'system-out': this.removeInvalidCharacters(stripAnsi(systemOutLines.join('\n')))});
testcase.testcase.push({'system-out': this.removeInvalidCharacters(stripAnsi(systemOutLines.join('\n')))});
}

if (this._options.outputs && (test.consoleErrors && test.consoleErrors.length > 0)) {
config.testcase.push({'system-err': this.removeInvalidCharacters(stripAnsi(test.consoleErrors.join('\n')))});
testcase.testcase.push({'system-err': this.removeInvalidCharacters(stripAnsi(test.consoleErrors.join('\n')))});
}

if (err) {
Expand All @@ -238,9 +303,9 @@ MochaJUnitReporter.prototype.getTestcaseData = function(test, err) {
_cdata: this.removeInvalidCharacters(failureMessage)
};

config.testcase.push({failure: failureElement});
testcase.testcase.push({failure: failureElement});
}
return config;
return testcase;
};

/**
Expand Down Expand Up @@ -278,14 +343,16 @@ MochaJUnitReporter.prototype.getXml = function(testsuites) {
var totalSuitesTime = 0;
var totalTests = 0;
var stats = this._runner.stats;
var hasProperties = !!this._options.properties;
var antMode = this._options.antMode;
var hasProperties = (!!this._options.properties) || antMode;

testsuites.forEach(function(suite) {
var _suiteAttr = suite.testsuite[0]._attr;
// properties are added before test cases so we want to make sure that we are grabbing test cases
// at the correct index
// testsuite is an array: [attrs, properties?, testcase, testcase, …]
// we want to make sure that we are grabbing test cases at the correct index
var _casesIndex = hasProperties ? 2 : 1;
var _cases = suite.testsuite.slice(_casesIndex);
var missingProps;

_suiteAttr.failures = 0;
_suiteAttr.time = 0;
Expand All @@ -299,6 +366,20 @@ MochaJUnitReporter.prototype.getXml = function(testsuites) {
_suiteAttr.time += testcase.testcase[0]._attr.time;
});

if (antMode) {
missingProps = ['system-out', 'system-err'];
suite.testsuite.forEach(function(item) {
missingProps = missingProps.filter(function(prop) {
return !item[prop];
});
});
missingProps.forEach(function(prop) {
var obj = {};
obj[prop] = [];
suite.testsuite.push(obj);
});
}

if (!_suiteAttr.skipped) {
delete _suiteAttr.skipped;
}
Expand All @@ -307,22 +388,23 @@ MochaJUnitReporter.prototype.getXml = function(testsuites) {
totalTests += _suiteAttr.tests;
});

var rootSuite = {
_attr: {
name: this._options.testsuitesTitle,
time: totalSuitesTime,
tests: totalTests,
failures: stats.failures
}
};

if (stats.pending) {
rootSuite._attr.skipped = stats.pending;
if (!antMode) {
var rootSuite = {
_attr: {
name: this._options.testsuitesTitle,
time: totalSuitesTime,
tests: totalTests,
failures: stats.failures
}
};
if (stats.pending) {
rootSuite._attr.skipped = stats.pending;
}
testsuites = [ rootSuite ].concat(testsuites);
}

return xml({
testsuites: [ rootSuite ].concat(testsuites)
}, { declaration: true, indent: ' ' });
return xml({ testsuites: testsuites }, { declaration: true, indent: ' ' });
};

/**
Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -26,7 +26,8 @@
"chai": "^3.0.0",
"chai-xml": "^0.3.0",
"eslint": "^4.0.0",
"mocha": "^3.0.0",
"libxmljs": "^0.19.5",
"mocha": "^5.0.0",
"test-console": "^1.0.0"
},
"dependencies": {
Expand Down

0 comments on commit 6d89b63

Please sign in to comment.