nodeunit
Version:
Easy unit testing for node.js and the browser.
255 lines (225 loc) • 7.74 kB
JavaScript
/*!
* Nodeunit
* Copyright (c) 2010 Caolan McMahon
* MIT Licensed
*/
/**
* Module dependencies
*/
var async = require('../deps/async'),
fs = require('fs'),
util = require('util'),
Script = require('vm').Script,
http = require('http'),
path = require('path');
/**
* Detect if coffee-script, iced-coffeescript, or streamline are available and
* the respective file extensions to the search filter in modulePaths if it is.
*/
var extensions = [ 'js' ]; // js is always supported: add it unconditionally
var extensionPattern;
try {
require('coffee' + '-script/register');
extensions.push('coffee');
} catch (e) { }
try {
require('iced-coffee' + '-script/register');
extensions.push('iced');
} catch (e) { }
try {
require('stream' + 'line').register();
extensions.push('_coffee');
extensions.push('_js');
} catch (e) { }
extensionPattern = new RegExp('\\.(?:' + extensions.join('|') + ')$');
/**
* Finds all modules at each path in an array, If a path is a directory, it
* returns all supported file types inside it. This only reads 1 level deep in
* the directory and does not recurse through sub-directories.
*
* The extension (.js, .coffee etc) is stripped from the filenames so they can
* simply be require()'ed.
*
* @param {Array} paths
* @param {Function} callback
* @param {Boolean=} recursive
* @api public
*/
exports.modulePaths = function modulePaths(paths, callback, recursive) {
recursive = (recursive === true);
async.concatSeries(paths, function (p, cb) {
fs.stat(p, function (err, stats) {
if (err) {
return cb(err);
}
if (stats.isFile()) {
return cb(null, [p]);
}
if (stats.isDirectory()) {
fs.readdir(p, function (err, files) {
if (err) {
return cb(err);
}
// filter out any filenames with unsupported extensions
var modules = files.filter(function (filename) {
return extensionPattern.exec(filename);
});
// remove extension from module name and prepend the
// directory path
var fullpaths = modules.map(function (filename) {
var mod_name = filename.replace(extensionPattern, '');
return [p, mod_name].join('/');
});
if (recursive) {
// get all sub directories
var directories =
files
.map(function(filename) {
// resolve path first
return path.resolve(p, filename);
})
.filter(function(filename) {
// fetch only directories
return (fs.statSync(filename).isDirectory());
});
// recursively call modulePaths() with sub directories
modulePaths(directories, function(err, files) {
if (!err) {
cb(null, fullpaths.concat(files).sort())
} else {
cb(err);
}
}, recursive);
} else {
// sort filenames here, because Array.map changes order
fullpaths.sort();
// finish
cb(null, fullpaths);
}
});
}
});
}, callback);
};
/**
* Evaluates JavaScript files in a sandbox, returning the context. The first
* argument can either be a single filename or an array of filenames. If
* multiple filenames are given their contents are concatenated before
* evalution. The second argument is an optional context to use for the sandbox.
*
* @param files
* @param {Object} sandbox
* @return {Object}
* @api public
*/
exports.sandbox = function (files, /*optional*/sandbox) {
var source, script, result;
if (!(files instanceof Array)) {
files = [files];
}
source = files.map(function (file) {
return fs.readFileSync(file, 'utf8');
}).join('');
if (!sandbox) {
sandbox = {};
}
script = new Script(source);
result = script.runInNewContext(sandbox);
return sandbox;
};
/**
* Provides a http request, response testing environment.
*
* Example:
*
* var httputil = require('nodeunit').utils.httputil
* exports.testSomething = function(test) {
* httputil(function (req, resp) {
* resp.writeHead(200, {});
* resp.end('test data');
* },
* function(server, client) {
* client.fetch('GET', '/', {}, function(resp) {
* test.equal('test data', resp.body);
* server.close();
* test.done();
* })
* });
* };
*
* @param {Function} cgi
* @param {Function} envReady
* @api public
*/
exports.httputil = function (cgi, envReady) {
var hostname = process.env.HOSTNAME || 'localhost';
var port = process.env.PORT || 3000;
var server = http.createServer(cgi);
server.listen(port, hostname);
var agent = new http.Agent({ host: hostname, port: port, maxSockets: 1 });
var client = {
fetch: function (method, path, headers, respReady) {
var request = http.request({
host: hostname,
port: port,
agent: agent,
method: method,
path: path,
headers: headers
});
request.end();
request.on('response', function (response) {
response.setEncoding('utf8');
response.on('data', function (chunk) {
if (response.body) {
response.body += chunk;
} else {
response.body = chunk;
}
});
response.on('end', function () {
if (response.headers['content-type'] === 'application/json') {
response.bodyAsObject = JSON.parse(response.body);
}
respReady(response);
});
});
}
};
process.nextTick(function () {
if (envReady && typeof envReady === 'function') {
envReady(server, client);
}
});
};
/**
* Improves formatting of AssertionError messages to make deepEqual etc more
* readable.
*
* @param {Object} assertion
* @return {Object}
* @api public
*/
exports.betterErrors = function (assertion) {
if (!assertion.error) {
return assertion;
}
var e = assertion.error;
if (typeof e.actual !== 'undefined' && typeof e.expected !== 'undefined') {
var actual = util.inspect(e.actual, false, 10).replace(/\n$/, '');
var expected = util.inspect(e.expected, false, 10).replace(/\n$/, '');
var multiline = (
actual.indexOf('\n') !== -1 ||
expected.indexOf('\n') !== -1
);
var spacing = (multiline ? '\n' : ' ');
e._message = e.message;
e.stack = (
e.name + ':' + spacing +
actual + spacing + e.operator + spacing +
expected + '\n' +
e.stack.split('\n').slice(1).join('\n')
);
}
return assertion;
};