@marshallofsound/mocha-appveyor-reporter
Version:
Mocha Reporter to report tests while running in AppVeyor CI
235 lines (207 loc) • 6.87 kB
JavaScript
var util = require('util'),
Base = require('mocha').reporters.Base;
function AppVeyorReporter(runner, options) {
Base.call(this, runner);
this.options = (options && options.reporterOptions) || {};
// Default options
this.options.appveyorApiUrl = process.env.APPVEYOR_API_URL || this.options.appveyorApiUrl;
this.options.appveyorBatchSize = process.env.APPVEYOR_BATCH_SIZE || this.options.appveyorBatchSize || 100;
this.options.appveyorBatchIntervalInMs = process.env.APPVEYOR_BATCH_INTERVAL_IN_MS || this.options.appveyorBatchIntervalInMs || 1000;
var got = require('got'),
log = console.log.bind(console),
error = console.error.bind(console),
warn = console.warn.bind(console),
logEntries = [],
self = this;
if(!this.options.appveyorApiUrl) {
warn("appveyorApiUrl option and APPVEYOR_API_URL environment variable not set, will not report to AppVeyor.");
}
// Client to be used to make API calls.
this.client = this.options.appveyorApiUrl ? got.default.extend({
prefixUrl: this.options.appveyorApiUrl
}) : undefined;
// Callback to be invoked (if defined) when an in flight request is completed.
this.doneCb = undefined;
// Whether or not API call is in flight.
this.inFlight = false;
// Enqueued tests.
this.testQueue = [];
// Timeout configured to make next API call.
this.sendTimeout = undefined;
/**
* Adds test to queue of tests to be sent with next API call.
*
* May also trigger pending tests to be sent.
*
* @param test test to send.
*/
var addTest = function(test) {
if(!self.client) {
return;
}
self.testQueue.push(test);
self.maybeSendTests();
};
/**
* Takes a test result and maps it to the output format that AppVeyor accepts.
* @param test Result generated by mocha.
* @returns {{testName: string, testFramework: string, fileName: string, outcome: string, durationMilliseconds: Number, StdOut: string, StdErr: string}}
*/
var mapTest = function(test) {
var stdout = "";
var stderr = "";
logEntries.forEach(function (data) {
var msg = util.format.apply(util, data.msg);
if (data.type === 'out') {
stdout = stdout + msg + '\n';
} else {
stderr = stderr + msg + '\n';
}
});
return {
testName: test.fullTitle(),
testFramework: 'Mocha',
fileName: test.file,
outcome: test.state === "passed" ? "Passed" : null,
durationMilliseconds: test.duration,
StdOut: stdout,
StdErr: stderr
};
};
/**
* @param type {String} The type of message being logged (either 'out' for stdout or anything else for stderr).
* @param original {Function} Function to be wrapped.
* @returns {Function} A function that wraps the given original function with behavior that records messages to
* a separate array.
*/
var handleConsole = function(type, original) {
return function() {
var args = Array.prototype.slice.call(arguments);
if(original.apply && typeof original.apply === 'function') {
original.apply(original, arguments);
}
logEntries.push({ type: type, msg: args });
}
};
runner.on('test', function () {
// trap console so we can record stdout/stderr to appveyor.
logEntries = [];
console.log = handleConsole("out", log);
console.warn = handleConsole("warn", warn);
console.error = handleConsole("err", error);
});
runner.on('test end', function() {
// reset console implementations to original implementation.
console.log = log;
console.warn = warn;
console.error = error;
});
runner.on('pass', function(mochaTest){
var test = mapTest(mochaTest);
addTest(test);
});
runner.on('pending', function(mochaTest) {
var test = mapTest(mochaTest);
test.outcome = 'Ignored';
addTest(test);
});
runner.on('fail', function(mochaTest, err) {
var test = mapTest(mochaTest);
test.outcome = 'Failed';
test.ErrorMessage = err.message;
test.ErrorStackTrace = err.stack;
addTest(test);
});
// Schedule tests to be sent initially.
self.scheduleTests();
}
util.inherits(AppVeyorReporter, Base);
/**
* Schedule tests to be sent after appveyorBatchIntervalInMs.
*/
AppVeyorReporter.prototype.scheduleTests = function() {
this.sendTimeout = setTimeout(this.sendTests.bind(this), this.options.appveyorBatchIntervalInMs);
};
/**
* Trigger tests to be sent.
*/
AppVeyorReporter.prototype.sendTests = function() {
var self = this;
// Don't send tests if endpoint not set up.
if(!self.client) {
return;
}
// clear existing timeout.
if(self.sendTimeout) {
clearTimeout(self.sendTimeout);
self.sendTimeout = undefined;
}
// if queue is empty, schedule tests to be submitted later, but only if not done.
if(self.testQueue.length === 0) {
if (!self.doneCb) {
self.scheduleTests();
}
} else {
self.inFlight = true;
var data = this.testQueue.slice();
self.testQueue = [];
self.client.post('api/tests/batch', {
json: data,
}).then((response) => {
self.inFlight = false;
// if doneCb set, and test queue is empty, invoke doneCb, otherwise if not empty, send tests immediately.
if (self.doneCb) {
if (self.testQueue.length === 0) {
self.doneCb();
self.doneCb = undefined;
} else {
self.sendTests();
}
} else {
// If tests weren't ready to be sent, schedule.
if (!self.maybeSendTests()) {
self.scheduleTests();
}
}
}).catch((err) => {
self.inFlight = false;
console.error("Error returned from posting test result to AppVeyor. Error: %s. \n.", err);
});
}
};
/**
* Trigger enqueued tests to be sent to appveyor if at least appveyorBatchSize tests are enqueued and there is no API
* @returns {boolean}
*/
AppVeyorReporter.prototype.maybeSendTests = function() {
// if more than appveyor_batch_size tests are enqueued, send them.
if(!this.inFlight && this.testQueue.length >= this.options.appveyorBatchSize) {
this.sendTests();
return true;
} else {
return false;
}
};
/**
* Override done to await any in flight HTTP requests to post tests to AppVeyor.
*
* @param failures
* @param {Function} fn
*/
AppVeyorReporter.prototype.done = function (failures, fn) {
// register doneCb to be invoked when API call finishes.
var invoked = false;
this.doneCb = function() {
invoked = true;
fn(failures);
};
// If no API call in flight and there are pending tests, send them.
if(!this.inFlight && this.testQueue.length > 0) {
this.sendTests();
}
// If no API call is in flight and the callback hasn't been invoked yet, invoke it here.
if(!this.inFlight && !invoked) {
fn(failures);
}
};
module.exports = AppVeyorReporter;