'use strict';
var fs = require('graceful-fs');
var os = require('os');
var path = require('path');
var q = require('q');
var through = require('through');
var utils = require('./lib/utils');
var WorkerPool = require('node-worker-pool');
var Console = require('./Console');
var TEST_WORKER_PATH = require.resolve('./TestWorker');
var DEFAULT_OPTIONS = {
runInBand: false,
maxWorkers: Math.max(os.cpus().length, 1),
nodePath: process.execPath,
nodeArgv: process.execArgv.filter(function(arg) {
return arg !== '--debug';
})
};
var HIDDEN_FILE_RE = /\/\.[^\/]*$/;
function TestRunner(config, options) {
this._config = config;
this._configDeps = null;
this._moduleLoaderResourceMap = null;
this._testPathDirsRegExp = new RegExp(
config.testPathDirs
.map(function(dir) {
return utils.escapeStrForRegex(dir);
})
.join('|')
);
this._nodeHasteTestRegExp = new RegExp(
'/' + utils.escapeStrForRegex(config.testDirectoryName) + '/' +
'.*\\.(' +
config.testFileExtensions.map(function(ext) {
return utils.escapeStrForRegex(ext);
})
.join('|') +
')$'
);
this._opts = Object.create(DEFAULT_OPTIONS);
if (options) {
for (var key in options) {
this._opts[key] = options[key];
}
}
}
TestRunner.prototype._constructModuleLoader = function(environment, customCfg) {
var config = customCfg || this._config;
var ModuleLoader = this._loadConfigDependencies().ModuleLoader;
return this._getModuleLoaderResourceMap().then(function(resourceMap) {
return new ModuleLoader(config, environment, resourceMap);
});
};
TestRunner.prototype._getModuleLoaderResourceMap = function() {
var ModuleLoader = this._loadConfigDependencies().ModuleLoader;
if (this._moduleLoaderResourceMap === null) {
if (this._opts.useCachedModuleLoaderResourceMap) {
this._moduleLoaderResourceMap =
ModuleLoader.loadResourceMapFromCacheFile(this._config);
} else {
this._moduleLoaderResourceMap =
ModuleLoader.loadResourceMap(this._config);
}
}
return this._moduleLoaderResourceMap;
};
TestRunner.prototype._isTestFilePath = function(filePath) {
filePath = utils.pathNormalize(filePath);
var testPathIgnorePattern =
this._config.testPathIgnorePatterns
? new RegExp(this._config.testPathIgnorePatterns.join('|'))
: null;
return (
this._nodeHasteTestRegExp.test(filePath)
&& !HIDDEN_FILE_RE.test(filePath)
&& (!testPathIgnorePattern || !testPathIgnorePattern.test(filePath))
&& this._testPathDirsRegExp.test(filePath)
);
};
TestRunner.prototype._loadConfigDependencies = function() {
var config = this._config;
if (this._configDeps === null) {
this._configDeps = {
ModuleLoader: require(config.moduleLoader),
testEnvironment: require(config.testEnvironment),
testRunner: require(config.testRunner).bind(null)
};
}
return this._configDeps;
};
TestRunner.prototype.streamTestPathsRelatedTo = function(paths) {
var pathStream = through(
function write(data) {
if (data.isError) {
this.emit('error', data);
this.emit('end');
} else {
this.emit('data', data);
}
},
function end() {
this.emit('end');
}
);
var testRunner = this;
this._constructModuleLoader().done(function(moduleLoader) {
var discoveredModules = {};
paths.forEach(function(path) {
discoveredModules[path] = true;
if (testRunner._isTestFilePath(path) && fs.existsSync(path)) {
pathStream.write(path);
}
});
var modulesToSearch = [].concat(paths);
while (modulesToSearch.length > 0) {
var modulePath = modulesToSearch.shift();
var depPaths = moduleLoader.getDependentsFromPath(modulePath);
depPaths.forEach(function(depPath) {
if (!discoveredModules.hasOwnProperty(depPath)) {
discoveredModules[depPath] = true;
modulesToSearch.push(depPath);
if (testRunner._isTestFilePath(depPath) && fs.existsSync(depPath)) {
pathStream.write(depPath);
}
}
});
}
pathStream.end();
});
return pathStream;
};
TestRunner.prototype.promiseTestPathsRelatedTo = function(paths) {
return _pathStreamToPromise(this.streamTestPathsRelatedTo(paths));
};
TestRunner.prototype.streamTestPathsMatching = function(pathPattern) {
var pathStream = through(
function write(data) {
if (data.isError) {
this.emit('error', data);
this.emit('end');
} else {
this.emit('data', data);
}
},
function end() {
this.emit('end');
}
);
this._getModuleLoaderResourceMap().then(function(resourceMap) {
var resourcePathMap = resourceMap.resourcePathMap;
for (var i in resourcePathMap) {
if (!resourcePathMap[i]) {
continue;
}
var pathStr = resourcePathMap[i].path;
if (
this._isTestFilePath(pathStr) &&
pathPattern.test(pathStr)
) {
pathStream.write(pathStr);
}
}
pathStream.end();
}.bind(this));
return pathStream;
};
TestRunner.prototype.promiseTestPathsMatching = function(pathPattern) {
return _pathStreamToPromise(this.streamTestPathsMatching(pathPattern));
};
TestRunner.prototype.preloadResourceMap = function() {
this._getModuleLoaderResourceMap().done();
};
TestRunner.prototype.preloadConfigDependencies = function() {
this._loadConfigDependencies();
};
TestRunner.prototype.runTest = function(testFilePath) {
var config = Object.create(this._config);
var configDeps = this._loadConfigDependencies();
var env = new configDeps.testEnvironment(config);
var testRunner = configDeps.testRunner;
var consoleMessages = [];
env.global.console = new Console(consoleMessages);
return this._constructModuleLoader(env, config).then(function(moduleLoader) {
if (config.collectCoverage && !config.collectCoverageOnlyFrom) {
config.collectCoverageOnlyFrom = {};
moduleLoader.getDependenciesFromPath(testFilePath)
.filter(function(depPath) {
return /^\//.test(depPath);
}).forEach(function(depPath) {
config.collectCoverageOnlyFrom[depPath] = true;
});
}
if (config.setupEnvScriptFile) {
utils.runContentWithLocalBindings(
env.runSourceText.bind(env),
utils.readAndPreprocessFileContent(config.setupEnvScriptFile, config),
config.setupEnvScriptFile,
{
__dirname: path.dirname(config.setupEnvScriptFile),
__filename: config.setupEnvScriptFile,
global: env.global,
require: moduleLoader.constructBoundRequire(
config.setupEnvScriptFile
),
jest: moduleLoader.getJestRuntime(config.setupEnvScriptFile)
}
);
}
var testExecStats = {start: Date.now()};
return testRunner(config, env, moduleLoader, testFilePath)
.then(function(results) {
testExecStats.end = Date.now();
results.logMessages = consoleMessages;
results.perfStats = testExecStats;
results.testFilePath = testFilePath;
results.coverage =
config.collectCoverage
? moduleLoader.getAllCoverageInfo()
: {};
return results;
});
}).finally(function() {
env.dispose();
});
};
TestRunner.prototype.runTests = function(testPaths, reporter) {
var config = this._config;
if (!reporter) {
var TestReporter = require(config.testReporter);
reporter = new TestReporter();
}
var aggregatedResults = {
success: null,
runTime: null,
numTotalTests: testPaths.length,
numPassedTests: 0,
numFailedTests: 0,
testResults: [],
};
reporter.onRunStart && reporter.onRunStart(config, aggregatedResults);
var onTestResult = function (testPath, testResult) {
aggregatedResults.testResults.push(testResult);
if (testResult.numFailingTests > 0) {
aggregatedResults.numFailedTests++;
} else {
aggregatedResults.numPassedTests++;
}
reporter.onTestResult && reporter.onTestResult(
config,
testResult,
aggregatedResults
);
};
var onRunFailure = function (testPath, err) {
aggregatedResults.numFailedTests++;
reporter.onTestResult && reporter.onTestResult(config, {
testFilePath: testPath,
testExecError: err,
suites: {},
tests: {},
logMessages: []
}, aggregatedResults);
};
var testRun = this._createTestRun(testPaths, onTestResult, onRunFailure);
var startTime = Date.now();
return testRun.then(function() {
aggregatedResults.runTime = (Date.now() - startTime) / 1000;
aggregatedResults.success = aggregatedResults.numFailedTests === 0;
reporter.onRunComplete && reporter.onRunComplete(config, aggregatedResults);
return aggregatedResults;
});
};
TestRunner.prototype._createTestRun = function(
testPaths, onTestResult, onRunFailure
) {
if (this._opts.runInBand || testPaths.length <= 1) {
return this._createInBandTestRun(testPaths, onTestResult, onRunFailure);
} else {
return this._createParallelTestRun(testPaths, onTestResult, onRunFailure);
}
};
TestRunner.prototype._createInBandTestRun = function(
testPaths, onTestResult, onRunFailure
) {
var testSequence = q();
testPaths.forEach(function(testPath) {
testSequence = testSequence.then(this.runTest.bind(this, testPath))
.then(function(testResult) {
onTestResult(testPath, testResult);
})
.catch(function(err) {
onRunFailure(testPath, err);
});
}, this);
return testSequence;
};
TestRunner.prototype._createParallelTestRun = function(
testPaths, onTestResult, onRunFailure
) {
var workerPool = new WorkerPool(
this._opts.maxWorkers,
this._opts.nodePath,
this._opts.nodeArgv.concat([
'--harmony',
TEST_WORKER_PATH,
'--config=' + JSON.stringify(this._config)
])
);
return this._getModuleLoaderResourceMap()
.then(function() {
return q.all(testPaths.map(function(testPath) {
return workerPool.sendMessage({testFilePath: testPath})
.then(function(testResult) {
onTestResult(testPath, testResult);
})
.catch(function(err) {
onRunFailure(testPath, err);
if (err.message
&& /Worker process exited before /.test(err.message)) {
console.error(
'A worker process has quit unexpectedly! This is bad news, ' +
'shutting down now!'
);
process.exit(1);
}
});
}));
})
.then(function() {
return workerPool.destroy();
});
};
function _pathStreamToPromise(stream) {
var defer = q.defer();
var paths = [];
stream.on('data', function(path) {
paths.push(path);
});
stream.on('error', function(err) {
defer.reject(err);
});
stream.on('end', function() {
defer.resolve(paths);
});
return defer.promise;
}
module.exports = TestRunner;