node-query/node_modules/grunt-contrib-nodeunit/tasks/nodeunit.js

297 lines
10 KiB
JavaScript
Raw Normal View History

2014-10-20 16:56:45 -04:00
/*
* grunt-contrib-nodeunit
* http://gruntjs.com/
*
* Copyright (c) 2014 "Cowboy" Ben Alman, contributors
* Licensed under the MIT license.
*/
'use strict';
module.exports = function(grunt) {
// Nodejs libs.
var path = require('path');
var util = require('util');
var fs = require('fs');
// External libs.
var nodeunit = require('nodeunit');
var hooker = require('hooker');
// ==========================================================================
// BETTER ERROR DISPLAY
// ==========================================================================
// Much nicer error formatting than what comes with nodeunit.
var betterErrors = function (assertion) {
var e = assertion.error;
if (!e || !('actual' in e) || !('expected' in e)) { return assertion; }
// Temporarily override the global "inspect" property because logging
// the entire global object is just silly.
var globalInspect = global.inspect;
global.inspect = function() { return '[object global]'; };
e._message = e.message;
// Pretty-formatted objects.
var actual = util.inspect(e.actual, false, 10, true);
var expected = util.inspect(e.expected, false, 10, true);
var indent = function(str) {
return (''+str).split('\n').map(function(s) { return ' ' + s; }).join('\n');
};
var stack;
var multiline = (actual + expected).indexOf('\n') !== -1;
if (multiline) {
stack = [
'Actual:', indent(actual),
'Operator:', indent(e.operator),
'Expected:', indent(expected),
].join('\n');
} else {
stack = e.name + ': ' + actual + ' ' + e.operator + ' ' + expected;
}
if (e.stack) {
stack += '\n' + e.stack.split('\n').slice(1).join('\n');
}
e.stack = stack;
// Restore the global "inspect" property.
global.inspect = globalInspect;
return assertion;
};
// Reformat stack trace to remove nodeunit scripts, fix indentation, etc.
var cleanStack = function(error) {
error._stack = error.stack;
// Show a full stack trace?
var fullStack = grunt.option('verbose') || grunt.option('stack');
// Reformat stack trace output.
error.stack = error.stack.split('\n').map(function(line) {
if (line[0] === ' ') {
// Remove nodeunit script srcs from non-verbose stack trace.
if (!fullStack && line.indexOf(path.join('node_modules', 'nodeunit') + path.sep) !== -1) {
return '';
}
// Remove leading spaces.
line = line.replace(/^ {4}(?=at)/, '');
// Remove cwd.
line = line.replace('(' + process.cwd() + path.sep, '(');
} else {
line = line.replace(/Assertion(Error)/, '$1');
}
return line + '\n';
}).join('');
return error;
};
// ==========================================================================
// CUSTOM NODEUNIT REPORTER
// ==========================================================================
// Keep track of the last-started module.
var currentModule;
// Keep track of the last-started test(s).
var unfinished = {};
// If Nodeunit explodes because a test was missing test.done(), handle it.
process.on('exit', function() {
var len = Object.keys(unfinished).length;
// If there are unfinished tests, tell the user why Nodeunit killed grunt.
if (len > 0) {
grunt.log.muted = false;
grunt.verbose.error().or.writeln('F'.red);
grunt.log.error('Incomplete tests/setups/teardowns:');
Object.keys(unfinished).forEach(grunt.log.error, grunt.log);
grunt.fatal('A test was missing test.done(), so nodeunit exploded. Sorry!',
Math.min(99, 90 + len));
}
});
// Keep track of failed assertions for pretty-printing.
var failedAssertions = [];
function logFailedAssertions() {
var assertion, stack;
// Print each assertion error + stack.
while (assertion = failedAssertions.shift()) {
betterErrors(assertion);
cleanStack(assertion.error);
grunt.verbose.or.error(assertion.testName);
if (assertion.error.name === 'AssertionError' && assertion.message) {
grunt.log.error('Message: ' + assertion.message.magenta);
}
grunt.log.error(assertion.error.stack).writeln();
}
}
// Define our own Nodeunit reporter.
nodeunit.reporters.grunt = {
info: 'Grunt reporter',
run: function(files, options, callback) {
var opts = {
// No idea.
testspec: undefined,
// Executed when the first test in a file is run. If no tests exist in
// the file, this doesn't execute.
moduleStart: function(name) {
// Keep track of this so that moduleDone output can be suppressed in
// cases where a test file contains no tests.
currentModule = name;
grunt.verbose.subhead('Testing ' + name).or.write('Testing ' + name);
},
// Executed after a file is done being processed. This executes whether
// tests exist in the file or not.
moduleDone: function(name) {
// Abort if no tests actually ran.
if (name !== currentModule) { return; }
// Print assertion errors here, if verbose mode is disabled.
if (!grunt.option('verbose')) {
if (failedAssertions.length > 0) {
grunt.log.writeln();
logFailedAssertions();
} else {
grunt.log.ok();
}
}
},
// Executed before each test is run.
testStart: function(name) {
// Keep track of the current test, in case test.done() was omitted
// and Nodeunit explodes.
unfinished[name] = name;
grunt.verbose.write(name + '...');
// Mute output, in cases where a function being tested logs through
// grunt (for testing grunt internals).
grunt.log.muted = true;
},
// Executed after each test and all its assertions are run.
testDone: function(name, assertions) {
delete unfinished[name];
// Un-mute output.
grunt.log.muted = false;
// Log errors if necessary, otherwise success.
if (assertions.failures()) {
assertions.forEach(function(ass) {
if (ass.failed()) {
ass.testName = name;
failedAssertions.push(ass);
}
});
if (grunt.option('verbose')) {
grunt.log.error();
logFailedAssertions();
} else {
grunt.log.write('F'.red);
}
} else {
grunt.verbose.ok().or.write('.');
}
},
// Executed when everything is all done.
done: function (assertions) {
if (assertions.failures()) {
grunt.warn(assertions.failures() + '/' + assertions.length +
' assertions failed (' + assertions.duration + 'ms)');
} else if (assertions.length === 0) {
grunt.warn('0/0 assertions ran (' + assertions.duration + 'ms)');
} else {
grunt.verbose.writeln();
grunt.log.ok(assertions.length + ' assertions passed (' +
assertions.duration + 'ms)');
}
// Tell the task manager we're all done.
callback(); // callback(assertions.failures() === 0);
}
};
// Nodeunit needs absolute paths.
var paths = files.map(function(filepath) {
return path.resolve(filepath);
});
nodeunit.runFiles(paths, opts);
}
};
// ==========================================================================
// TASKS
// ==========================================================================
grunt.registerMultiTask('nodeunit', 'Run Nodeunit unit tests.', function() {
var done = this.async();
// Merge task-specific and/or target-specific options with these defaults.
var options = this.options({
reporterOutput: false,
reporter: 'grunt',
reporterOptions: {}
});
// Ensure the default nodeunit options are set by reading in the nodeunit.json file.
var nodeUnitDefaults = {};
// check for nodeunit under our package's node_modules directory first
var nodeUnitDefaultsFile = path.join(__dirname, '..', 'node_modules', 'nodeunit', 'bin', 'nodeunit.json');
if (!fs.existsSync(nodeUnitDefaultsFile)) {
// if both grunt-contrib-nodeunit and nodeunit are listed as dependencies for this project, they'd
// be located at the same folder level. So check for that location next.
nodeUnitDefaultsFile = path.join(__dirname, '..', '..', 'nodeunit', 'bin', 'nodeunit.json');
}
if (fs.existsSync(nodeUnitDefaultsFile)) {
nodeUnitDefaults = JSON.parse(fs.readFileSync(nodeUnitDefaultsFile, 'utf8'));
}
for (var defaultVal in nodeUnitDefaults) {
if (typeof options.reporterOptions[defaultVal] === 'undefined') {
options.reporterOptions[defaultVal] = nodeUnitDefaults[defaultVal];
}
}
if (!nodeunit.reporters[options.reporter]) {
return done(new Error('Reporter ' + options.reporter + ' not found'));
}
var output = '';
if (options.reporterOutput) {
// Hook into stdout to capture report
hooker.hook(process.stdout, 'write', {
pre: function(str) {
output += str;
return hooker.preempt();
}
});
}
// if reporterOutput has a directory destination make sure to create it.
// See: https://github.com/caolan/nodeunit/issues/262
if (options.reporterOptions.output) {
grunt.file.mkdir(path.normalize(options.reporterOptions.output));
}
// Run test(s).
nodeunit.reporters[options.reporter].run(this.filesSrc, options.reporterOptions, function(err) {
// Write the output of the reporter if wanted
if (options.reporterOutput) {
// no longer hook stdout so we can grunt.log
hooker.unhook(process.stdout, 'write');
// save all of the output we saw up to this point
grunt.file.write(options.reporterOutput, output);
grunt.log.ok('Report "' + options.reporterOutput + '" created.');
}
done(err);
});
});
};