orionsoft-react-scripts
Version:
Orionsoft Configuration and scripts for Create React App.
606 lines (475 loc) • 15.3 kB
JavaScript
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*
*/
'use strict';var _require =
require('jest-util');const formatExecError = _require.formatExecError;
const fs = require('graceful-fs');
const getCacheFilePath = require('jest-haste-map').getCacheFilePath;
const DefaultReporter = require('./reporters/DefaultReporter');
const NotifyReporter = require('./reporters/NotifyReporter');
const SummaryReporter = require('./reporters/SummaryReporter');
const VerboseReporter = require('./reporters/VerboseReporter');
const promisify = require('./lib/promisify');
const runTest = require('./runTest');
const snapshot = require('jest-snapshot');
const throat = require('throat');
const workerFarm = require('worker-farm');
const TestWatcher = require('./TestWatcher');
const FAIL = 0;
const SLOW_TEST_TIME = 3000;
const SUCCESS = 1;
class CancelRun extends Error {
constructor(message) {
super(message);
this.name = 'CancelRun';
}}
const TEST_WORKER_PATH = require.resolve('./TestWorker');
class TestRunner {
constructor(
hasteContext,
config,
options)
{
this._config = config;
this._dispatcher = new ReporterDispatcher(
hasteContext.hasteFS,
options.getTestSummary);
this._hasteContext = hasteContext;
this._options = options;
this._setupReporters();
// Map from testFilePath -> time it takes to run the test. Used to
// optimally schedule bigger test runs.
this._testPerformanceCache = {};
}
addReporter(reporter) {
this._dispatcher.register(reporter);
}
removeReporter(ReporterClass) {
this._dispatcher.unregister(ReporterClass);
}
_getTestPerformanceCachePath() {
const config = this._config;
return getCacheFilePath(config.cacheDirectory, 'perf-cache-' + config.name);
}
_sortTests(testPaths) {
// When running more tests than we have workers available, sort the tests
// by size - big test files usually take longer to complete, so we run
// them first in an effort to minimize worker idle time at the end of a
// long test run.
//
// After a test run we store the time it took to run a test and on
// subsequent runs we use that to run the slowest tests first, yielding the
// fastest results.
try {
if (this._config.cache) {
this._testPerformanceCache = JSON.parse(fs.readFileSync(
this._getTestPerformanceCachePath(),
'utf8'));
} else {
this._testPerformanceCache = {};
}
} catch (e) {
this._testPerformanceCache = {};
}
const cache = this._testPerformanceCache;
const timings = [];
const stats = {};
const getFileSize = filePath =>
stats[filePath] || (stats[filePath] = fs.statSync(filePath).size);
const getTestRunTime = filePath => {
if (cache[filePath]) {
return cache[filePath][0] === FAIL ? Infinity : cache[filePath][1];
}
return null;
};
testPaths = testPaths.
sort((pathA, pathB) => {
const timeA = getTestRunTime(pathA);
const timeB = getTestRunTime(pathB);
if (timeA != null && timeB != null) {
return timeA < timeB ? 1 : -1;
}
return getFileSize(pathA) < getFileSize(pathB) ? 1 : -1;
});
testPaths.forEach(filePath => {
const timing = cache[filePath] && cache[filePath][1];
if (timing) {
timings.push(timing);
}
});
return { testPaths, timings };
}
_cacheTestResults(aggregatedResults) {
const cache = this._testPerformanceCache;
aggregatedResults.testResults.forEach(test => {
if (test && !test.skipped) {
const perf = test.perfStats;
cache[test.testFilePath] = [
test.numFailingTests ? FAIL : SUCCESS,
perf.end - perf.start || 0];
}
});
fs.writeFileSync(
this._getTestPerformanceCachePath(),
JSON.stringify(cache));
}
runTests(paths, watcher) {
const config = this._config;var _sortTests =
this._sortTests(paths);const testPaths = _sortTests.testPaths;const timings = _sortTests.timings;
const aggregatedResults = createAggregatedResults(testPaths.length);
const estimatedTime =
Math.ceil(getEstimatedTime(timings, this._options.maxWorkers) / 1000);
const onResult = (testPath, testResult) => {
if (watcher.isInterrupted()) {
return;
}
if (testResult.testResults.length === 0) {
const message = 'Your test suite must contain at least one test.';
onFailure(testPath, {
message,
stack: new Error(message).stack });
return;
}
addResult(aggregatedResults, testResult);
this._dispatcher.onTestResult(
config,
testResult,
aggregatedResults);
this._bailIfNeeded(aggregatedResults, watcher);
};
const onFailure = (testPath, error) => {
if (watcher.isInterrupted()) {
return;
}
const testResult = buildFailureTestResult(testPath, error);
testResult.failureMessage = formatExecError(testResult, config, testPath);
addResult(aggregatedResults, testResult);
this._dispatcher.onTestResult(
config,
testResult,
aggregatedResults);
};
// Run in band if we only have one test or one worker available.
// If we are confident from previous runs that the tests will finish quickly
// we also run in band to reduce the overhead of spawning workers.
const shouldRunInBand = () =>
this._options.maxWorkers <= 1 ||
testPaths.length <= 1 ||
testPaths.length <= 20 &&
timings.every(timing => timing < SLOW_TEST_TIME);
const finalizeResults = () => {
// Update snapshot state.
const status =
snapshot.cleanup(this._hasteContext.hasteFS, config.updateSnapshot);
aggregatedResults.snapshot.filesRemoved += status.filesRemoved;
aggregatedResults.snapshot.didUpdate = config.updateSnapshot;
aggregatedResults.snapshot.failure = !!(
!aggregatedResults.snapshot.didUpdate && (
aggregatedResults.snapshot.unchecked ||
aggregatedResults.snapshot.unmatched ||
aggregatedResults.snapshot.filesRemoved));
aggregatedResults.wasInterrupted = watcher.isInterrupted();
// Check if the test run was successful or not.
const anyTestFailures = !(
aggregatedResults.numFailedTests === 0 &&
aggregatedResults.numRuntimeErrorTestSuites === 0);
const anyReporterErrors = this._dispatcher.hasErrors();
aggregatedResults.success = !(
anyTestFailures ||
aggregatedResults.snapshot.failure ||
anyReporterErrors);
};
const runInBand = shouldRunInBand();
this._dispatcher.onRunStart(
config,
aggregatedResults,
{
estimatedTime,
showStatus: !runInBand });
const testRun =
runInBand ?
this._createInBandTestRun(testPaths, watcher, onResult, onFailure) :
this._createParallelTestRun(testPaths, watcher, onResult, onFailure);
return testRun.
catch(error => {
if (!watcher.isInterrupted()) {
throw error;
}
}).
then(() => {
finalizeResults();
this._dispatcher.onRunComplete(config, aggregatedResults);
this._cacheTestResults(aggregatedResults);
return aggregatedResults;
});
}
_createInBandTestRun(
testPaths,
watcher,
onResult,
onFailure)
{
const mutex = throat(1);
return testPaths.reduce(
(promise, path) =>
mutex(() =>
promise.
then(() => {
if (watcher.isInterrupted()) {
throw new CancelRun();
}
this._dispatcher.onTestStart(this._config, path);
return runTest(
path,
this._config,
this._hasteContext.resolver);
}).
then(result => onResult(path, result)).
catch(err => onFailure(path, err))),
Promise.resolve());
}
_createParallelTestRun(
testPaths,
watcher,
onResult,
onFailure)
{
const config = this._config;
const farm = workerFarm({
autoStart: true,
maxConcurrentCallsPerWorker: 1,
maxRetries: 2, // Allow for a couple of transient errors.
maxConcurrentWorkers: this._options.maxWorkers },
TEST_WORKER_PATH);
const mutex = throat(this._options.maxWorkers);
// Send test suites to workers continuously instead of all at once to track
// the start time of individual tests.
const runTestInWorker = (_ref) => {let path = _ref.path;let config = _ref.config;return mutex(() => {
if (watcher.isInterrupted()) {
return Promise.reject();
}
this._dispatcher.onTestStart(config, path);
return promisify(farm)({ path, config });
});};
const onError = (err, path) => {
onFailure(path, err);
if (err.type === 'ProcessTerminatedError') {
console.error(
'A worker process has quit unexpectedly! ' +
'Most likely this an initialization error.');
process.exit(1);
}
};
const onInterrupt = new Promise((_, reject) => {
watcher.on('change', state => {
if (state.interrupted) {
reject(new CancelRun());
}
});
});
const runAllTests = Promise.all(testPaths.map(path => {
return runTestInWorker({ path, config }).
then(testResult => onResult(path, testResult)).
catch(error => onError(error, path));
}));
return Promise.race([
runAllTests,
onInterrupt]).
then(() => workerFarm.end(farm));
}
_setupReporters() {
this.addReporter(
this._config.verbose ?
new VerboseReporter() :
new DefaultReporter());
if (this._config.collectCoverage) {
// coverage reporter dependency graph is pretty big and we don't
// want to require it if we're not in the `--coverage` mode
const CoverageReporter = require('./reporters/CoverageReporter');
this.addReporter(new CoverageReporter());
}
this.addReporter(new SummaryReporter());
if (this._config.notify) {
this.addReporter(new NotifyReporter());
}
}
_bailIfNeeded(aggregatedResults, watcher) {
if (this._config.bail && aggregatedResults.numFailedTests !== 0) {
if (watcher.isWatchMode()) {
watcher.setState({ interrupted: true });
} else {
this._dispatcher.onRunComplete(this._config, aggregatedResults);
process.exit(1);
}
}
}}
const createAggregatedResults = numTotalTestSuites => {
return {
numFailedTests: 0,
numFailedTestSuites: 0,
numPassedTests: 0,
numPassedTestSuites: 0,
numPendingTests: 0,
numPendingTestSuites: 0,
numRuntimeErrorTestSuites: 0,
numTotalTests: 0,
numTotalTestSuites,
snapshot: {
added: 0,
didUpdate: false, // is set only after the full run
failure: false,
filesAdded: 0,
// combines individual test results + results after full run
filesRemoved: 0,
filesUnmatched: 0,
filesUpdated: 0,
matched: 0,
total: 0,
unchecked: 0,
unmatched: 0,
updated: 0 },
startTime: Date.now(),
success: false,
testResults: [],
wasInterrupted: false };
};
const addResult = (
aggregatedResults,
testResult) =>
{
aggregatedResults.testResults.push(testResult);
aggregatedResults.numTotalTests +=
testResult.numPassingTests +
testResult.numFailingTests +
testResult.numPendingTests;
aggregatedResults.numFailedTests += testResult.numFailingTests;
aggregatedResults.numPassedTests += testResult.numPassingTests;
aggregatedResults.numPendingTests += testResult.numPendingTests;
if (testResult.testExecError) {
aggregatedResults.numRuntimeErrorTestSuites++;
}
if (testResult.skipped) {
aggregatedResults.numPendingTestSuites++;
} else if (testResult.numFailingTests > 0 || testResult.testExecError) {
aggregatedResults.numFailedTestSuites++;
} else {
aggregatedResults.numPassedTestSuites++;
}
// Snapshot data
if (testResult.snapshot.added) {
aggregatedResults.snapshot.filesAdded++;
}
if (testResult.snapshot.fileDeleted) {
aggregatedResults.snapshot.filesRemoved++;
}
if (testResult.snapshot.unmatched) {
aggregatedResults.snapshot.filesUnmatched++;
}
if (testResult.snapshot.updated) {
aggregatedResults.snapshot.filesUpdated++;
}
aggregatedResults.snapshot.added += testResult.snapshot.added;
aggregatedResults.snapshot.matched += testResult.snapshot.matched;
aggregatedResults.snapshot.unchecked += testResult.snapshot.unchecked;
aggregatedResults.snapshot.unmatched += testResult.snapshot.unmatched;
aggregatedResults.snapshot.updated += testResult.snapshot.updated;
aggregatedResults.snapshot.total +=
testResult.snapshot.added +
testResult.snapshot.matched +
testResult.snapshot.unmatched +
testResult.snapshot.updated;
};
const buildFailureTestResult = (
testPath,
err) =>
{
return {
console: null,
failureMessage: null,
numFailingTests: 0,
numPassingTests: 0,
numPendingTests: 0,
perfStats: {
end: 0,
start: 0 },
skipped: false,
snapshot: {
added: 0,
fileDeleted: false,
matched: 0,
unchecked: 0,
unmatched: 0,
updated: 0 },
testExecError: err,
testFilePath: testPath,
testResults: [] };
};
// Proxy class that holds all reporter and dispatchers events to each
// of them.
class ReporterDispatcher {
constructor(hasteFS, getTestSummary) {
this._runnerContext = { hasteFS, getTestSummary };
this._reporters = [];
}
register(reporter) {
this._reporters.push(reporter);
}
unregister(ReporterClass) {
this._reporters = this._reporters.filter(
reporter => !(reporter instanceof ReporterClass));
}
onTestResult(config, testResult, results) {
this._reporters.forEach(reporter =>
reporter.onTestResult(
config,
testResult,
results,
this._runnerContext));
}
onTestStart(config, path) {
this._reporters.forEach(reporter =>
reporter.onTestStart(config, path, this._runnerContext));
}
onRunStart(config, results, options) {
this._reporters.forEach(
reporter => reporter.onRunStart(
config,
results,
this._runnerContext,
options));
}
onRunComplete(config, results) {
this._reporters.forEach(reporter =>
reporter.onRunComplete(config, results, this._runnerContext));
}
// Return a list of last errors for every reporter
getErrors() {
return this._reporters.reduce(
(list, reporter) => {
const error = reporter.getLastError();
return error ? list.concat(error) : list;
},
[]);
}
hasErrors() {
return this.getErrors().length !== 0;
}}
const getEstimatedTime = (timings, workers) => {
if (!timings.length) {
return 0;
}
const max = Math.max.apply(null, timings);
if (timings.length <= workers) {
return max;
}
return Math.max(
timings.reduce((sum, time) => sum + time) / workers,
max);
};
module.exports = TestRunner;