grunt-jstestdriver-phantomjs
Version:
Grunt task for unit testing with jsTestDriver and PhantomJS.
317 lines (250 loc) • 10.5 kB
JavaScript
/*
* grunt-jstestdriver-phantomjs
* https://github.com/tolu/grunt-jstestdriver-phantomjs
*
* Copyright (c) 2013 Tobias Lundin
* Licensed under the MIT license.
*/
;
module.exports = function (grunt) {
var JSTDFLAGS_FLAGS = ['tests', 'verbose', 'captureConsole', 'preloadFiles', 'plugins', 'runnerMode', 'testOutput'];
var taskName = "jstdPhantom";
var _ = grunt.util._;
function silentGrunt(grunt) {
var clonedGrunt = _.clone(grunt);
clonedGrunt.warn = function () { };
return clonedGrunt;
}
// Nodejs libs.
var path = require('path');
var Q = require('q');
var http = require("http");
// npm lib
var phantomjs = require('grunt-lib-phantomjs').init(silentGrunt(grunt));
grunt.registerTask(taskName, 'Grunt task for unit testing using JS Test Driver.', function () {
var options = this.options({
tests: 'all',
timeout: 60000,
retries: 3,
port: _.random(1025, 5000),
useLatest: false
}),
useLatest = options.useLatest,
config = grunt.config.get(taskName),
async = this.async(),
numberOfConfigs,
numberOfPassedTests = 0,
numberOfFailedTests = 0,
timeouts = [],
childProcesses = [],
jarFile = path.join(__dirname, '..', 'lib', useLatest ? 'jstestdriver-1.3.5.jar': 'jstestdriver.jar');
// had an issue with 1.3.5 not being as stable in a certain setting and thats why we default to 1.3.3.d
delete options.useLatest;
grunt.verbose.writeflags(options, 'Options');
function done(success) {
killChildProcesses().then(function () {
if (success === false) {
grunt.fail.warn(taskName +" task failed!");
}
async.apply(this, arguments);
});
}
function killChildProcesses() {
var deferred = Q.defer();
// Don't let killed child processes do any logging
grunt.util.hooker.hook(process.stdout, 'write', {
pre: function(out) {
if (/(Running PhantomJS...|ERROR|>>.*0.*\[)/.test(out)) {
return grunt.util.hooker.preempt();
}
}
});
deferred.promise.then(function () {
setTimeout(function() {
grunt.util.hooker.unhook(process.stdout, 'write');
}, 3000);
});
function poll () {
var success = _.every(childProcesses, function (cp) {
return cp.killed || cp.exitCode !== null;
});
setTimeout(success ? deferred.resolve : poll, 100);
}
// clear all timeouts
timeouts.map(clearTimeout);
// kill all child processes
_.invoke(childProcesses, 'kill', 'SIGKILL');
// wait for child processes to finish
poll();
// wait no more than 10s
Q.delay(10000).then(deferred.resolve);
return deferred.promise;
}
function taskComplete() {
grunt.log.writeln('');
var msg = 'Total Passed: ' + numberOfPassedTests + ', Fails: ' + numberOfFailedTests;
if (numberOfFailedTests > 0) {
grunt.log.error(msg);
done(false);
} else {
grunt.log.ok(msg);
done();
}
}
function runJSTestDriver(configFileLocation, options) {
function itDidntWork (msg) {
grunt.log.writeln(msg);
done(false);
}
function getOptionsArray(options) {
var names, name, i, l, arr = [];
names = Object.getOwnPropertyNames(options);
l = names.length;
for (i = 0; i < l; i += 1) {
name = names[i];
if (JSTDFLAGS_FLAGS.indexOf(name) !== -1) {
arr.push("--" + name);
arr.push(options[name]);
}
}
return arr;
}
function startServer () {
var deferred = Q.defer();
grunt.log.write('Starting jstd server.');
deferred.promise.then(function() {
grunt.log.writeln("");
});
var server = grunt.util.spawn({
cmd: 'java',
args: [
"-jar",
jarFile,
"--port",
options.port
]
}, function(error, result, code){
grunt.verbose.writeln(error);
});
childProcesses.push(server);
function poll () {
grunt.log.write(".");
var httpOptions = {
host: 'localhost',
port: options.port,
path: '/',
method: 'GET'
};
var req = http.request(httpOptions, function(res) {
if (200 === res.statusCode) {
timeouts.push(setTimeout(deferred.resolve, 1000));
} else {
timeouts.push(setTimeout(poll, 100));
}
});
req.on('error', function(e) {
timeouts.push(setTimeout(poll, 1000));
});
// do request
req.end();
}
poll();
return deferred.promise;
}
function startBrowser () {
var deferred = Q.defer();
grunt.log.writeln("Starting PhantomJS...");
phantomjs.on('onResourceReceived', function(request){
if(/\/capture$/.test(request.url) && request.status === 404){
itDidntWork('server did not respond');
}
if(/\/heartbeat$/.test(request.url)){
deferred.resolve();
}
});
var phantom = phantomjs.spawn("http://localhost:" + options.port + "/capture", {
options: {},
done: function () {}
});
childProcesses.push(phantom);
return deferred.promise;
}
function runTests () {
var deferred = Q.defer();
grunt.log.writeln("Running tests...\n");
var jstdCmd = {
cmd: 'java',
args: ["-jar",
jarFile,
"--config",
configFileLocation,
'--reset',
'--server',
'http://localhost:' + options.port].concat(getOptionsArray(options))
};
var runner = grunt.util.spawn(jstdCmd, function (error, result) {
if (result && typeof result.stdout === "string") {
deferred.resolve(result.stdout);
}
else {
done(false);
}
});
runner.stdout.pipe(process.stdout);
runner.stderr.pipe(process.stderr);
childProcesses.push(runner);
return deferred.promise;
}
function handleTestResults (result) {
setNumberOfPassesAndFails(result);
if (hasFailedTests(result)) {
grunt.verbose.writeln(' ONE or MORE tests have failed in:');
}
processCompleteTests();
}
function setNumberOfPassesAndFails(result) {
var passedReg = /\d+(?=;\sFails)/,
failsReg = /\d+(?=;\sErrors)/;
if (result && result.indexOf('RuntimeException') === -1) {
numberOfPassedTests += parseInt(passedReg.exec(result)[0], 10);
numberOfFailedTests += parseInt(failsReg.exec(result)[0], 10);
}
}
function hasFailedTests(result) {
return result.indexOf("Error:") > -1;
}
function processCompleteTests() {
grunt.log.verbose.writeln('>> Finished running file: ' + configFileLocation);
grunt.log.verbose.writeln('');
numberOfConfigs -= 1;
if (numberOfConfigs === 0) {
taskComplete();
}
}
function onTimeout () {
grunt.verbose.writeln("A Timeout has been triggered. Retries left: " + (options.retries-1));
if (0 === options.retries--) {
grunt.log.error("Something took too long");
done(false);
}
else {
killChildProcesses().then(function() {
runJSTestDriver(configFileLocation, options);
});
}
}
Q.delay(options.timeout).then(onTimeout);
startServer().then(startBrowser).then(runTests).then(handleTestResults);
}
if (typeof config.files === 'string') {
config.files = [config.files];
}
if (options.testOutput) {
grunt.file.mkdir(options.testOutput);
}
numberOfConfigs = config.files.length;
grunt.util.async.forEach(config.files, function (filename) {
runJSTestDriver(filename, options);
}.bind(this));
});
};