thrill
Version:
Exciting testing
243 lines (198 loc) • 7.42 kB
JavaScript
var path = require('path'),
utils = require('../utils.js'),
vm = require('vm'),
fs = require('fs');
var createThrill = require('./thrill.js'),
createHttpServer = require('./httpServer.js'),
reporters = require('../../').reporters;
adapters = require('../../').adapters;
var runner = module.exports = function(config, callback){
if(!config) throw new Error('Config must be defined');
callback = callback || utils.noop;
// A thrill.js file may pass in a default "base" thrill.js file to use for default values
// This only goes one level down, if the defaults file defines a defaults file, it won't be
// evaluated.
if(config.config){
setDefaults(config, require(config.config));
}
// This fills any default properties to the config object (if they're not defined)
var defaults = require('../../config/runner.json');
setDefaults(config, defaults);
// Collapse the config options and default options in to one variable
var log = config.quiet? utils.noop : process.stdout.write.bind(process.stdout),
debug = config.verbose? process.stdout.write.bind(process.stdout) : utils.noop;
// If we don't have a run variable by this point, we can't proceed.
if(!config.run){
callback(new Error("[Thrill] Unable to find the required run variable in the configuration, aborting."));
return;
}
debug('[Thrill] Starting...\n');
var server = createHttpServer({
callback: onHttpServerReady,
host: config.httpHost,
autoAdaptLibraries: config.autoAdapt,
autoAdaptExtensions: config.autoAdaptExtensions,
log: log,
debug: debug
});
function onThrillReady(thrill){
var served,
thrillConfig,
test,
reporter;
if(thrill instanceof Error){
log('[Thrill] An error occurred when instantiating: ' + thrill + "\n");
return callback(thrill);
}
// If auto adapt is enabled, auto inject the adapters
// HTML files are auto adapted on the fly with the HTTP server
if(config.autoAdapt && typeof config.run !== "string") config.run = autoAdaptFileList(config.run);
// Handles the flag to disable streaming
if(!config.stream) config.run = disableStream(config.run);
// Normalize config.serve to a map
if(typeof config.serve === "string") config.serve = { "" : config.serve };
// Resolve any relative paths within the config
resolveFiles(config);
// If we have an html and don't have a serve directory, guess the best root directory
// to serve this file
if(!config.serve && typeof config.run === "string"){
var contents = fs.readFileSync(config.run).toString();
// This finds relative paths leading to parent directories
// We're going to find the furthest-most parent with it and use that as the
// root directory for this html file.
var rootFinder = /(?:src=|href=)(?:["|']){1}((?:\.{1,2}\/)*)*/gi,
rootMatch,
biggestMatch = ".";
// Go through the entire file, trying to find the furthest-most parent
// of this html file that it utilizes
while(rootMatch = rootFinder.exec(contents)){
if(!rootMatch[1]) continue;
// Since we're only looking at ./../../../ patterns, it's ok
// to use length as a determining factor of level
if(rootMatch[1].length > biggestMatch.length){
biggestMatch = rootMatch[1];
}
}
var rootDir = path.resolve(path.dirname(config.run), biggestMatch);
debug("[Thrill] Guessing that the root directory for this HTML file is " + rootDir + "\n");
config.serve = {"" : rootDir};
}
// Serve files and the run object through the HTTP server
served = server.serve(config.serve, config.run);
thrillConfig = {
run: served.runUrls,
filter: config.filter,
timeout: config.timeout,
autoStart: false
};
// Create the test object
test = thrill(thrillConfig);
var reporterParams = config.reporter.split(':');
var reporterName = reporterParams.shift();
if(!(reporterName in reporters)){
log('Reporter ' + reporterName + " not found. Falling back to the dot reporter.");
reporterName = "dot";
}
try{
createReporter = require(reporters[reporterName]);
} catch(e){
log('Error loading reporter ' + reporterName + ". Falling back to the dot reporter.");
createReporter = require('./reporter/dot.js');
}
// Create a reporter for the test
reporter = createReporter(test, {
log: log,
debug: debug,
params: reporterParams
});
// When the test has results, shut down our services, and execute the callback
reporter.on('results', function(data){
thrill.kill();
server.kill(function(){
callback(data.passed);
});
});
// Starts the test
test.start();
}
function onHttpServerReady(server){
createThrill({
callback: onThrillReady,
log: log,
debug: debug,
queenHost: config.host
});
}
};
// Resolves any relative file paths in the config
function resolveFiles(config){
utils.each(config.serve, function(filePath, key, map){
map[key] = path.resolve(filePath);
});
if(typeof config.run === "string"){
config.run = path.resolve(config.run);
} else {
utils.each(config.serve, function(filePath, index, arr){
arr[index] = path.resolve(filePath);
});
}
}
// Returns an IP address for this machine. Modified from: http://stackoverflow.com/questions/3653065/get-local-ip-address-in-node-js
function getExternalIpAddress(){
var interfaces = require('os').networkInterfaces(),
addresses = [];
utils.each(interfaces, function(interf, name){
addresses = addresses.concat(
utils.filter(interf, function(node){
return node.family === "IPv4" && node.internal === false;
})
);
});
if(addresses.length > 0){
return addresses[0].address;
}
}
// Fills in obj with defaults' variables if obj doesn't already define it.
function setDefaults(obj, defaults){
var variable;
utils.each(defaults, function(value, key){
if(obj[key] === void 0) obj[key] = value;
});
return obj;
}
// Goes through a list of file paths and tries to find the first instance of a library
// and injects the adapter path right after it. The regex just looks for any file with the
// library name in it, so this can be problematic if a library is renamed, or if another file
// using the library name is in the list before the library.
function autoAdaptFileList(fileList){
var adaptableLibraryRegexMap = utils.map(adapters, function(path, name){
return new RegExp('.*' + name + '.*\\.js', 'im');
});
var toInject = [], // Keeps track of where to inject the adapters in the list
skipLibraries = {}; // Keeps track of library adapters we've already injected
fileList.forEach(function(filePath, index){
utils.each(adaptableLibraryRegexMap, function(regex, name){
if(skipLibraries[name]) return; // If we already found this library skip it
if(regex.test(filePath)){
skipLibraries[name] = true;
toInject.push([(index + 1), name]);
}
});
});
// Start from the reverse to keep order
toInject.reverse().forEach(function(args){
fileList.splice(args[0], 0, adapters[args[1]]);
});
return fileList;
}
function disableStream(run){
// If it's an html-like file, add a query parameter to disable streaming
if(typeof run === "string"){
if(!~run.indexOf("?")) run += "?";
else run += "&";
run += "disableStream=true";
} else { // If we're dealing with a list of files, add the disabler as the first item
run.splice(0,0, path.resolve(path.dirname(module.filename), '../../lib/client/disableStream.js'));
}
return run;
}