grunt
Version:
A task-based command line build tool for JavaScript projects.
234 lines (213 loc) • 7.39 kB
JavaScript
/*
* grunt
* https://github.com/cowboy/grunt
*
* Copyright (c) 2012 "Cowboy" Ben Alman
* Licensed under the MIT license.
* http://benalman.com/about/license/
*/
var fs = require('fs');
var path = require('path');
var spawn = require('child_process').spawn;
var Tempfile = require('temporary/lib/file');
// Keep track of the last-started module, test and status.
var currentModule, currentTest, status;
// Keep track of the last-started test(s).
var unfinished = {};
// Allow an error message to retain its color when split across multiple lines.
function formatMessage(str) {
return String(str).split('\n').map(function(s) { return s.magenta; }).join('\n');
}
// Keep track of failed assertions for pretty-printing.
var failedAssertions = [];
function logFailedAssertions() {
var assertion;
// Print each assertion error.
while (assertion = failedAssertions.shift()) {
verbose.or.error(assertion.testName);
log.error('Message: ' + formatMessage(assertion.message));
if (assertion.actual !== assertion.expected) {
log.error('Actual: ' + formatMessage(assertion.actual));
log.error('Expected: ' + formatMessage(assertion.expected));
}
if (assertion.source) {
log.error(assertion.source.replace(/ {4}(at)/g, ' $1'));
}
log.writeln();
}
}
// Handle methods passed from PhantomJS, including QUnit hooks.
var phantomHandlers = {
// QUnit hooks.
moduleStart: function(name) {
unfinished[name] = true;
currentModule = name;
},
moduleDone: function(name, failed, passed, total) {
delete unfinished[name];
},
log: function(result, actual, expected, message, source) {
if (!result) {
failedAssertions.push({
actual: actual, expected: expected, message: message, source: source,
testName: currentTest
});
}
},
testStart: function(name) {
currentTest = (currentModule ? currentModule + ' - ' : '') + name;
verbose.write(currentTest + '...');
},
testDone: function(name, failed, passed, total) {
// Log errors if necessary, otherwise success.
if (failed > 0) {
// list assertions
if (option('verbose')) {
log.error();
logFailedAssertions();
} else {
log.write('F'.red);
}
} else {
verbose.ok().or.write('.');
}
},
done: function(failed, passed, total, duration) {
status.failed += failed;
status.passed += passed;
status.total += total;
status.duration += duration;
// Print assertion errors here, if verbose mode is disabled.
if (!option('verbose')) {
if (failed > 0) {
log.writeln();
logFailedAssertions();
} else {
log.ok();
}
}
},
// Error handlers.
done_fail: function(url) {
verbose.write('Running PhantomJS...').or.write('...');
log.error();
fail.warn('PhantomJS unable to load "' + url + '" URI.', 90);
},
done_timeout: function() {
log.writeln();
fail.warn('PhantomJS timed out, possibly due to a missing QUnit start() call.', 90);
},
// console.log pass-through.
console: console.log.bind(console),
// Debugging messages.
debug: log.debug.bind(log, 'phantomjs')
};
// ============================================================================
// TASKS
// ============================================================================
task.registerBasicTask('qunit', 'Run QUnit unit tests in a headless PhantomJS instance.', function(data, name) {
// Get files as URLs.
var urls = file.expandToUrls(data);
// This task is asynchronous.
var done = this.async();
// Reset status.
status = {failed: 0, passed: 0, total: 0, duration: 0};
// Process each filepath in-order.
async.forEachSeries(urls, function(url, next) {
var basename = path.basename(url);
verbose.subhead('Testing ' + basename).or.write('Testing ' + basename);
// Create temporary file to be used for grunt-phantom communication.
var tempfile = new Tempfile();
// Timeout ID.
var id;
// The number of tempfile lines already read.
var n = 0;
// Reset current module.
currentModule = null;
// Clean up.
function cleanup() {
clearTimeout(id);
tempfile.unlink();
}
// It's simple. As QUnit tests, assertions and modules begin and complete,
// the results are written as JSON to a temporary file. This polling loop
// checks that file for new lines, and for each one parses its JSON and
// executes the corresponding method with the specified arguments.
(function loopy() {
// Disable logging temporarily.
log.muted = true;
// Read the file, splitting lines on \n, and removing a trailing line.
var lines = file.read(tempfile.path).split('\n').slice(0, -1);
// Re-enable logging.
log.muted = false;
// Iterate over all lines that haven't already been processed.
var done = lines.slice(n).some(function(line) {
// Get args and method.
var args = JSON.parse(line);
var method = args.shift();
// Execute method if it exists.
if (phantomHandlers[method]) {
phantomHandlers[method].apply(null, args);
}
// If the method name started with test, return true. Because the
// Array#some method was used, this not only sets "done" to true,
// but stops further iteration from occurring.
return (/^done/).test(method);
});
if (done) {
// All done.
cleanup();
next();
} else {
// Update n so previously processed lines are ignored.
n = lines.length;
// Check back in a little bit.
id = setTimeout(loopy, 100);
}
}());
// Launch PhantomJS.
var args = [
// The main script file.
file.taskfile('qunit/phantom.js'),
// The temporary file used for communications.
tempfile.path,
// The QUnit helper file to be injected.
file.taskfile('qunit/qunit.js'),
// URL to the QUnit .html test file to run.
url,
// PhantomJS options.
'--config=' + file.taskfile('qunit/phantom.json')
];
spawn('phantomjs', args).on('exit', function (code) {
if (code === 0) { return; }
// Something went horribly wrong.
cleanup();
verbose.or.writeln();
log.write('Running PhantomJS...').error();
if (code === 127) {
log.error(
'In order for the qunit task to work properly, PhantomJS must be installed and\n' +
'in the system PATH (if you can run "phantomjs" at the command line, this task\n' +
'should work). Unfortunately, PhantomJS cannot be installed automatically via\n' +
'npm or grunt. Visit www.phantomjs.org for detailed installation instructions.'
);
fail.warn('PhantomJS not found.', 90);
} else {
fail.warn('PhantomJS exited unexpectedly with exit code ' + code + '.', 90);
}
done();
});
}, function(err) {
// All tests have been run.
// Log results.
if (status.failed > 0) {
fail.warn(status.failed + '/' + status.total + ' assertions failed (' +
status.duration + 'ms)', Math.min(99, 90 + status.failed));
} else {
verbose.writeln();
log.ok(status.total + ' assertions passed (' + status.duration + 'ms)');
}
// All done!
done();
});
});