UNPKG

orionsoft-react-scripts

Version:

Orionsoft Configuration and scripts for Create React App.

606 lines (475 loc) 15.3 kB
/** * 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;