From 93d32efb66876344593cdccb7b6bc5863be6d370 Mon Sep 17 00:00:00 2001 From: Dmitrii Abramov Date: Mon, 19 Jun 2017 09:56:05 -0700 Subject: [PATCH] refactor CLI --- .../__snapshots__/showConfig-test.js.snap | 3 - packages/jest-cli/src/SearchSource.js | 27 +- packages/jest-cli/src/TestRunner.js | 125 +------ packages/jest-cli/src/cli/index.js | 318 ++++++++++++++++-- packages/jest-cli/src/cli/runCLI.js | 136 -------- .../jest-cli/src/lib/getTestPathPattern.js | 4 +- packages/jest-cli/src/lib/logDebugMessages.js | 4 +- .../jest-cli/src/reporters/SummaryReporter.js | 10 +- packages/jest-cli/src/runJest.js | 73 ++-- packages/jest-cli/src/testResultHelpers.js | 136 ++++++++ 10 files changed, 509 insertions(+), 327 deletions(-) delete mode 100644 packages/jest-cli/src/cli/runCLI.js create mode 100644 packages/jest-cli/src/testResultHelpers.js diff --git a/integration_tests/__tests__/__snapshots__/showConfig-test.js.snap b/integration_tests/__tests__/__snapshots__/showConfig-test.js.snap index 8b4a627a5406..7875beebe54d 100644 --- a/integration_tests/__tests__/__snapshots__/showConfig-test.js.snap +++ b/integration_tests/__tests__/__snapshots__/showConfig-test.js.snap @@ -72,9 +72,6 @@ exports[`jest --showConfig outputs config info and exits 1`] = ` "mapCoverage": false, "noStackTrace": false, "notify": false, - "projects": [ - "/mocked/root/path/jest/integration_tests/verbose_reporter" - ], "rootDir": "/mocked/root/path/jest/integration_tests/verbose_reporter", "testPathPattern": "", "testResultsProcessor": null, diff --git a/packages/jest-cli/src/SearchSource.js b/packages/jest-cli/src/SearchSource.js index cadf8989e770..c71b2456e90b 100644 --- a/packages/jest-cli/src/SearchSource.js +++ b/packages/jest-cli/src/SearchSource.js @@ -33,7 +33,7 @@ type Options = {| withAncestor?: boolean, |}; -export type PathPattern = {| +export type TestSelectionConfig = {| input?: string, findRelatedTests?: boolean, lastCommit?: boolean, @@ -216,13 +216,24 @@ class SearchSource { }); } - getTestPaths(pattern: PathPattern): Promise { - if (pattern.onlyChanged) { - return this.findChangedTests({lastCommit: pattern.lastCommit}); - } else if (pattern.findRelatedTests && pattern.paths) { - return Promise.resolve(this.findRelatedTestsFromPattern(pattern.paths)); - } else if (pattern.testPathPattern != null) { - return Promise.resolve(this.findMatchingTests(pattern.testPathPattern)); + getTestPaths( + testSelectionConfig: TestSelectionConfig, + ): Promise { + if (testSelectionConfig.onlyChanged) { + return this.findChangedTests({ + lastCommit: testSelectionConfig.lastCommit, + }); + } else if ( + testSelectionConfig.findRelatedTests && + testSelectionConfig.paths + ) { + return Promise.resolve( + this.findRelatedTestsFromPattern(testSelectionConfig.paths), + ); + } else if (testSelectionConfig.testPathPattern != null) { + return Promise.resolve( + this.findMatchingTests(testSelectionConfig.testPathPattern), + ); } else { return Promise.resolve({tests: []}); } diff --git a/packages/jest-cli/src/TestRunner.js b/packages/jest-cli/src/TestRunner.js index 8f5317959cb8..3fa0665bd0d3 100644 --- a/packages/jest-cli/src/TestRunner.js +++ b/packages/jest-cli/src/TestRunner.js @@ -16,9 +16,14 @@ import type { import type {GlobalConfig, ReporterConfig} from 'types/Config'; import type {Context} from 'types/Context'; import type {Reporter, Test} from 'types/TestRunner'; -import type {PathPattern} from './SearchSource'; +import type {TestSelectionConfig} from './SearchSource'; import {formatExecError} from 'jest-message-util'; +import { + addResult, + buildFailureTestResult, + makeEmptyAggregatedTestResult, +} from './testResultHelpers'; import snapshot from 'jest-snapshot'; import pify from 'pify'; import throat from 'throat'; @@ -43,7 +48,7 @@ class CancelRun extends Error { export type TestRunnerOptions = {| maxWorkers: number, - pattern: PathPattern, + pattern: TestSelectionConfig, startRun: () => *, testNamePattern: string, testPathPattern: string, @@ -378,117 +383,11 @@ class TestRunner { } const createAggregatedResults = (numTotalTestSuites: number) => { - return { - numFailedTestSuites: 0, - numFailedTests: 0, - numPassedTestSuites: 0, - numPassedTests: 0, - numPendingTestSuites: 0, - numPendingTests: 0, - numRuntimeErrorTestSuites: 0, - numTotalTestSuites, - numTotalTests: 0, - 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: AggregatedResult, - testResult: TestResult, -): void => { - 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: string, - err: TestError, -): TestResult => { - 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, - }, - sourceMaps: {}, - testExecError: err, - testFilePath: testPath, - testResults: [], - }; + const result = makeEmptyAggregatedTestResult(); + result.numTotalTestSuites = numTotalTestSuites; + result.startTime = Date.now(); + result.success = false; + return result; }; const getEstimatedTime = (timings, workers) => { diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 1b5d4d718ccc..5b4a8d0a6045 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -8,17 +8,92 @@ * @flow */ -import type {Path} from 'types/Config'; +import type {AggregatedResult} from 'types/TestResult'; import type {Argv} from 'types/Argv'; +import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; -import {validateCLIOptions} from 'jest-util'; -import yargs from 'yargs'; +import { + Console, + clearLine, + createDirectory, + validateCLIOptions, +} from 'jest-util'; +import {readConfig} from 'jest-config'; +import {version as VERSION} from '../../package.json'; import args from './args'; +import chalk from 'chalk'; +import createContext from '../lib/createContext'; import getJest from './getJest'; -import runCLI from './runCLI'; +import getMaxWorkers from '../lib/getMaxWorkers'; +import handleDeprecationWarnings from '../lib/handleDeprecationWarnings'; +import logDebugMessages from '../lib/logDebugMessages'; +import preRunMessage from '../preRunMessage'; +import runJest from '../runJest'; +import Runtime from 'jest-runtime'; +import TestWatcher from '../TestWatcher'; +import watch from '../watch'; +import yargs from 'yargs'; + +function run(maybeArgv?: Argv, project?: Path) { + const argv: Argv = _buildArgv(maybeArgv, project); + const projects = _getProjectListFromCLIArgs(argv, project); + // If we're running a single Jest project, we might want to use another + // version of Jest (the one that is specified in this project's package.json) + const runCLIFn = _getRunCLIFn(projects); + + runCLIFn(argv, projects, result => _readResultsAndExit(argv, result)); +} + +const runCLI = async ( + argv: Argv, + projects: Array, + onComplete: (results: ?AggregatedResult) => void, +) => { + // Optimize 'fs' module and make it more compatible with multiple platforms. + _patchGlobalFSModule(); -function run(argv?: Argv, project?: Path) { - argv = yargs(argv || process.argv.slice(2)) + // If we output a JSON object, we can't write anything to stdout, since + // it'll break the JSON structure and it won't be valid. + const outputStream = argv.json ? process.stderr : process.stdout; + + argv.version && _printVersionAndExit(outputStream, onComplete); + + try { + const {globalConfig, configs, hasDeprecationWarnings} = _getConfigs( + projects, + argv, + outputStream, + ); + await _run( + globalConfig, + configs, + hasDeprecationWarnings, + outputStream, + argv, + onComplete, + ); + } catch (error) { + _printErrorAndExit(error); + } +}; + +const _printErrorAndExit = error => { + clearLine(process.stderr); + clearLine(process.stdout); + console.error(chalk.red(error.stack)); + process.exit(1); +}; + +const _readResultsAndExit = (argv: Argv, result: ?AggregatedResult) => { + const code = !result || result.success ? 0 : 1; + process.on('exit', () => process.exit(code)); + if (argv && argv.forceExit) { + process.exit(code); + } +}; + +const _buildArgv = (maybeArgv: ?Argv, project: ?Path) => { + const argv: Argv = yargs(maybeArgv || process.argv.slice(2)) .usage(args.usage) .help() .alias('help', 'h') @@ -28,23 +103,226 @@ function run(argv?: Argv, project?: Path) { validateCLIOptions(argv, args.options); - if (!project) { - project = process.cwd(); + return argv; +}; + +const _getProjectListFromCLIArgs = (argv, project: ?Path) => { + const projects = argv.projects ? argv.projects : []; + + if (project) { + projects.push(project); + } + + if (!projects.length) { + projects.push(process.cwd()); + } + + return projects; +}; + +const _getRunCLIFn = (projects: Array) => + projects.length === 1 ? getJest(projects[0]).runCLI : runCLI; + +const _printDebugInfoAndExitIfNeeded = ( + argv, + globalConfig, + config, + outputStream, +) => { + if (argv.debug || argv.showConfig) { + logDebugMessages(globalConfig, config, outputStream); + } + if (argv.showConfig) { + process.exit(0); + } +}; + +const _printVersionAndExit = (outputStream, onComplete) => { + outputStream.write(`v${VERSION}\n`); + onComplete && onComplete(); + return; +}; + +// Possible scenarios: +// 1. jest --config config.json +// 2. jest --projects p1 p2 +// 3. jest --projects p1 p2 --config config.json +// 4. jest --projects p1 +// 5. jest +// +// If no projects are specified, process.cwd() will be used as the default +// (and only) project. +const _getConfigs = ( + projectsFromCLIArgs: Array, + argv: Argv, + outputStream, +): { + globalConfig: GlobalConfig, + configs: Array, + hasDeprecationWarnings: boolean, +} => { + let globalConfig; + let hasDeprecationWarnings; + let configs: Array = []; + let projects = projectsFromCLIArgs; + + if (projectsFromCLIArgs.length === 1) { + const parsedConfig = readConfig(argv, projects[0]); + + if (parsedConfig.globalConfig.projects) { + // If this was a single project, and its config has `projects` + // settings, use that value instead. + projects = parsedConfig.globalConfig.projects; + } + + hasDeprecationWarnings = parsedConfig.hasDeprecationWarnings; + globalConfig = parsedConfig.globalConfig; + configs = [parsedConfig.config]; + if (globalConfig.projects && globalConfig.projects.length) { + // Even though we had one project in CLI args, there might be more + // projects defined in the config. + projects = globalConfig.projects; + } + } + + if (projects.length > 1) { + const parsedConfigs = projects.map(root => readConfig(argv, root)); + configs = parsedConfigs.map(({config}) => config); + if (!hasDeprecationWarnings) { + hasDeprecationWarnings = parsedConfigs.some( + ({hasDeprecationWarnings}) => !!hasDeprecationWarnings, + ); + } + // If no config was passed initially, use the one from the first project + if (!globalConfig) { + globalConfig = parsedConfigs[0].globalConfig; + } } - if (!argv.projects) { - argv.projects = [project]; + if (!globalConfig || !configs.length) { + throw new Error('jest: No configuration found for any project.'); } - const execute = argv.projects.length === 1 ? getJest(project).runCLI : runCLI; - execute(argv, argv.projects, result => { - const code = !result || result.success ? 0 : 1; - process.on('exit', () => process.exit(code)); - if (argv && argv.forceExit) { - process.exit(code); + _printDebugInfoAndExitIfNeeded(argv, globalConfig, configs[0], outputStream); + + return { + configs, + globalConfig, + hasDeprecationWarnings: !!hasDeprecationWarnings, + }; +}; + +const _patchGlobalFSModule = () => { + const realFs = require('fs'); + const fs = require('graceful-fs'); + fs.gracefulify(realFs); +}; + +const _buildContextsAndHasteMaps = async ( + configs, + globalConfig, + outputStream, + argv, +) => { + const hasteMapInstances = Array(configs.length); + const contexts = await Promise.all( + configs.map(async (config, index) => { + createDirectory(config.cacheDirectory); + const hasteMapInstance = Runtime.createHasteMap(config, { + console: new Console(outputStream, outputStream), + maxWorkers: getMaxWorkers(argv), + resetCache: !config.cache, + watch: globalConfig.watch, + watchman: globalConfig.watchman, + }); + hasteMapInstances[index] = hasteMapInstance; + return createContext(config, await hasteMapInstance.build()); + }), + ); + + return {contexts, hasteMapInstances}; +}; + +const _run = async ( + globalConfig, + configs, + hasDeprecationWarnings, + outputStream, + argv, + onComplete, +) => { + const {contexts, hasteMapInstances} = await _buildContextsAndHasteMaps( + configs, + globalConfig, + outputStream, + argv, + ); + + argv.watch || argv.watchAll + ? _runWatch( + argv, + contexts, + configs, + hasDeprecationWarnings, + globalConfig, + outputStream, + hasteMapInstances, + ) + : _runWithoutWatch(globalConfig, contexts, argv, outputStream, onComplete); +}; + +const _runWatch = async ( + argv, + contexts, + configs, + hasDeprecationWarnings, + globalConfig, + outputStream, + hasteMapInstances, +) => { + if (hasDeprecationWarnings) { + try { + await handleDeprecationWarnings(outputStream, process.stdin); + return watch( + globalConfig, + contexts, + argv, + outputStream, + hasteMapInstances, + ); + } catch (e) { + process.exit(0); } - }); -} + } + + return watch(globalConfig, contexts, argv, outputStream, hasteMapInstances); +}; + +const _runWithoutWatch = async ( + globalConfig, + contexts, + argv, + outputStream, + onComplete, +) => { + const startRun = () => { + if (!argv.listTests) { + preRunMessage.print(outputStream); + } + runJest( + globalConfig, + contexts, + argv, + outputStream, + new TestWatcher({isWatchMode: false}), + startRun, + onComplete, + ); + }; + return startRun(); +}; -exports.run = run; -exports.runCLI = runCLI; +module.exports = { + run, + runCLI, +}; diff --git a/packages/jest-cli/src/cli/runCLI.js b/packages/jest-cli/src/cli/runCLI.js deleted file mode 100644 index 62e93ef97d90..000000000000 --- a/packages/jest-cli/src/cli/runCLI.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * 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. - * - * @flow - */ - -import type {Argv} from 'types/Argv'; -import type {AggregatedResult} from 'types/TestResult'; -import type {Path} from 'types/Config'; - -import Runtime from 'jest-runtime'; -import {Console, clearLine, createDirectory} from 'jest-util'; -import {readConfig} from 'jest-config'; -import chalk from 'chalk'; -import createContext from '../lib/createContext'; -import getMaxWorkers from '../lib/getMaxWorkers'; -import handleDeprecationWarnings from '../lib/handleDeprecationWarnings'; -import logDebugMessages from '../lib/logDebugMessages'; -import preRunMessage from '../preRunMessage'; -import runJest from '../runJest'; -import TestWatcher from '../TestWatcher'; -import watch from '../watch'; -import {version as VERSION} from '../../package.json'; - -module.exports = async ( - argv: Argv, - projects: Array, - onComplete: (results: ?AggregatedResult) => void, -) => { - const realFs = require('fs'); - const fs = require('graceful-fs'); - fs.gracefulify(realFs); - - const pipe = argv.json ? process.stderr : process.stdout; - if (argv.version) { - pipe.write(`v${VERSION}\n`); - onComplete && onComplete(); - return; - } - - const _run = async (globalConfig, configs) => { - const hasteMapInstances = Array(configs.length); - const contexts = await Promise.all( - configs.map(async ({config}, index) => { - createDirectory(config.cacheDirectory); - const hasteMapInstance = Runtime.createHasteMap(config, { - console: new Console(pipe, pipe), - maxWorkers: getMaxWorkers(argv), - resetCache: !config.cache, - watch: globalConfig.watch, - watchman: globalConfig.watchman, - }); - hasteMapInstances[index] = hasteMapInstance; - return createContext(config, await hasteMapInstance.build()); - }), - ); - - if (argv.watch || argv.watchAll) { - if (configs.some(({hasDeprecationWarnings}) => hasDeprecationWarnings)) { - try { - await handleDeprecationWarnings(pipe, process.stdin); - return watch(globalConfig, contexts, argv, pipe, hasteMapInstances); - } catch (e) { - process.exit(0); - } - } - - return watch(globalConfig, contexts, argv, pipe, hasteMapInstances); - } else { - const startRun = () => { - if (!argv.listTests) { - preRunMessage.print(pipe); - } - runJest( - globalConfig, - contexts, - argv, - pipe, - new TestWatcher({isWatchMode: false}), - startRun, - onComplete, - ); - }; - return startRun(); - } - }; - - try { - let globalConfig; - let hasDeprecationWarnings; - let configs = []; - let config; - if (projects.length === 1) { - ({config, globalConfig, hasDeprecationWarnings} = readConfig( - argv, - projects[0], - )); - configs = [{config, globalConfig, hasDeprecationWarnings}]; - if (globalConfig.projects && globalConfig.projects.length) { - projects = globalConfig.projects; - } - } - - if (projects.length > 1) { - configs = projects.map(root => readConfig(argv, root)); - // If no config was passed initially, use the one from the first project - if (!globalConfig && !config) { - globalConfig = configs[0].globalConfig; - config = configs[0].config; - } - } - - if (!config || !globalConfig || !configs.length) { - throw new Error('jest: No configuration found for any project.'); - } - - if (argv.debug || argv.showConfig) { - logDebugMessages(globalConfig, config, pipe); - } - - if (argv.showConfig) { - process.exit(0); - } - - await _run(globalConfig, configs); - } catch (error) { - clearLine(process.stderr); - clearLine(process.stdout); - console.error(chalk.red(error.stack)); - process.exit(1); - } -}; diff --git a/packages/jest-cli/src/lib/getTestPathPattern.js b/packages/jest-cli/src/lib/getTestPathPattern.js index 6463e726f66d..1ce1a4402699 100644 --- a/packages/jest-cli/src/lib/getTestPathPattern.js +++ b/packages/jest-cli/src/lib/getTestPathPattern.js @@ -9,7 +9,7 @@ */ import type {Argv} from 'types/Argv'; -import type {PathPattern} from '../SearchSource'; +import type {TestSelectionConfig} from '../SearchSource'; import {clearLine} from 'jest-util'; import chalk from 'chalk'; @@ -32,7 +32,7 @@ const showTestPathPatternError = (testPathPattern: string) => { ); }; -module.exports = (argv: Argv): PathPattern => { +module.exports = (argv: Argv): TestSelectionConfig => { if (argv.onlyChanged) { return { input: '', diff --git a/packages/jest-cli/src/lib/logDebugMessages.js b/packages/jest-cli/src/lib/logDebugMessages.js index ab06e8ebee32..eb967c9396a3 100644 --- a/packages/jest-cli/src/lib/logDebugMessages.js +++ b/packages/jest-cli/src/lib/logDebugMessages.js @@ -16,7 +16,7 @@ import {version as VERSION} from '../../package.json'; const logDebugMessages = ( globalConfig: GlobalConfig, config: ProjectConfig, - pipe: stream$Writable | tty$WriteStream, + outputStream: stream$Writable | tty$WriteStream, ): void => { /* $FlowFixMe */ const testFramework = (require(config.testRunner): TestFramework); @@ -26,7 +26,7 @@ const logDebugMessages = ( globalConfig, version: VERSION, }; - pipe.write(JSON.stringify(output, null, ' ') + '\n'); + outputStream.write(JSON.stringify(output, null, ' ') + '\n'); }; module.exports = logDebugMessages; diff --git a/packages/jest-cli/src/reporters/SummaryReporter.js b/packages/jest-cli/src/reporters/SummaryReporter.js index dd4e7e3d7708..215c11a9f6ae 100644 --- a/packages/jest-cli/src/reporters/SummaryReporter.js +++ b/packages/jest-cli/src/reporters/SummaryReporter.js @@ -12,8 +12,8 @@ import type {AggregatedResult, SnapshotSummary} from 'types/TestResult'; import type {GlobalConfig} from 'types/Config'; import type {Context} from 'types/Context'; import type {ReporterOnStartOptions} from 'types/Reporters'; -import type {PathPattern} from '../SearchSource'; import type {TestRunnerOptions} from '../TestRunner'; +import type {TestSelectionConfig} from '../SearchSource'; import chalk from 'chalk'; import BaseReporter from './BaseReporter'; @@ -246,13 +246,15 @@ class SummaryReporter extends BaseReporter { _getTestSummary( contexts: Set, - pattern: PathPattern, + testSelectionConfig: TestSelectionConfig, testNamePattern: string, testPathPattern: string, ) { - const testInfo = pattern.onlyChanged + const testInfo = testSelectionConfig.onlyChanged ? chalk.dim(' related to changed files') - : pattern.input !== '' ? chalk.dim(' matching ') + testPathPattern : ''; + : testSelectionConfig.input !== '' + ? chalk.dim(' matching ') + testPathPattern + : ''; const nameInfo = testNamePattern ? chalk.dim(' with tests matching ') + `"${testNamePattern}"` diff --git a/packages/jest-cli/src/runJest.js b/packages/jest-cli/src/runJest.js index 1d656b817fa6..8f3f11e6c0b3 100644 --- a/packages/jest-cli/src/runJest.js +++ b/packages/jest-cli/src/runJest.js @@ -11,6 +11,7 @@ import type {Argv} from 'types/Argv'; import type {Context} from 'types/Context'; import type {GlobalConfig} from 'types/Config'; +import type {TestSelectionConfig} from './SearchSource'; import type {AggregatedResult} from 'types/TestResult'; import type TestWatcher from './TestWatcher'; @@ -24,6 +25,7 @@ import SearchSource from './SearchSource'; import updateArgv from './lib/updateArgv'; import TestRunner from './TestRunner'; import TestSequencer from './TestSequencer'; +import {makeEmptyAggregatedTestResult} from './testResultHelpers'; const setConfig = (contexts, newConfig) => contexts.forEach( @@ -97,7 +99,13 @@ const getNoTestsFoundMessage = (testRunData, pattern) => { ); }; -const getTestPaths = async (globalConfig, context, pattern, argv, pipe) => { +const getTestPaths = async ( + globalConfig, + context, + pattern, + argv, + outputStream, +) => { const source = new SearchSource(context); let data = await source.getTestPaths(pattern); if (!data.tests.length) { @@ -108,7 +116,7 @@ const getTestPaths = async (globalConfig, context, pattern, argv, pipe) => { pattern = getTestPathPattern(argv); data = await source.getTestPaths(pattern); } else { - new Console(pipe, pipe).log( + new Console(outputStream, outputStream).log( 'Jest can only find uncommitted changed files in a git or hg ' + 'repository. If you make your project a git or hg ' + 'repository (`git init` or `hg init`), Jest will be able ' + @@ -149,13 +157,23 @@ const runJest = async ( globalConfig: GlobalConfig, contexts: Array, argv: Argv, - pipe: stream$Writable | tty$WriteStream, + outputStream: stream$Writable | tty$WriteStream, testWatcher: TestWatcher, startRun: () => *, onComplete: (testResults: AggregatedResult) => any, + // We use this internaly at FB. Since we run multiple processes and most + // of them don't match any tests, we don't want to print 'no tests found' + // message for all of them. + // This will no longer be needed when we complete this: + // https://github.com/facebook/jest/issues/3768 + printNoTestsMessage?: ( + outputStream: stream$Writable, + testRunData: Array<*>, + testSelectionConfig: TestSelectionConfig, + ) => void, ) => { const maxWorkers = getMaxWorkers(argv); - const pattern = getTestPathPattern(argv); + const testSelectionConfig = getTestPathPattern(argv); const sequencer = new TestSequencer(); let allTests = []; const testRunData = await Promise.all( @@ -163,9 +181,9 @@ const runJest = async ( const matches = await getTestPaths( globalConfig, context, - pattern, + testSelectionConfig, argv, - pipe, + outputStream, ); allTests = allTests.concat(matches.tests); return {context, matches}; @@ -176,41 +194,18 @@ const runJest = async ( if (argv.listTests) { console.log(JSON.stringify(allTests.map(test => test.path))); - onComplete && - onComplete({ - numFailedTestSuites: 0, - numFailedTests: 0, - numPassedTestSuites: 0, - numPassedTests: 0, - numPendingTestSuites: 0, - numPendingTests: 0, - numRuntimeErrorTestSuites: 0, - numTotalTestSuites: 0, - numTotalTests: 0, - snapshot: { - added: 0, - didUpdate: false, - failure: false, - filesAdded: 0, - filesRemoved: 0, - filesUnmatched: 0, - filesUpdated: 0, - matched: 0, - total: 0, - unchecked: 0, - unmatched: 0, - updated: 0, - }, - startTime: 0, - success: true, - testResults: [], - wasInterrupted: false, - }); + onComplete && onComplete(makeEmptyAggregatedTestResult()); return null; } if (!allTests.length) { - new Console(pipe, pipe).log(getNoTestsFoundMessage(testRunData, pattern)); + if (printNoTestsMessage) { + printNoTestsMessage(outputStream, testRunData, testSelectionConfig); + } else { + new Console(outputStream, outputStream).log( + getNoTestsFoundMessage(testRunData, testSelectionConfig), + ); + } } else if ( allTests.length === 1 && globalConfig.silent !== true && @@ -230,10 +225,10 @@ const runJest = async ( const results = await new TestRunner(globalConfig, { maxWorkers, - pattern, + pattern: testSelectionConfig, startRun, testNamePattern: argv.testNamePattern, - testPathPattern: formatTestPathPattern(pattern), + testPathPattern: formatTestPathPattern(testSelectionConfig), }).runTests(allTests, testWatcher); sequencer.cacheResults(allTests, results); diff --git a/packages/jest-cli/src/testResultHelpers.js b/packages/jest-cli/src/testResultHelpers.js new file mode 100644 index 000000000000..80738c26f82d --- /dev/null +++ b/packages/jest-cli/src/testResultHelpers.js @@ -0,0 +1,136 @@ +/** + * 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. + * + * @flow + */ + +import type { + AggregatedResult, + SerializableError, + TestResult, +} from 'types/TestResult'; + +const makeEmptyAggregatedTestResult = (): AggregatedResult => { + return { + numFailedTestSuites: 0, + numFailedTests: 0, + numPassedTestSuites: 0, + numPassedTests: 0, + numPendingTestSuites: 0, + numPendingTests: 0, + numRuntimeErrorTestSuites: 0, + numTotalTestSuites: 0, + numTotalTests: 0, + snapshot: { + added: 0, + didUpdate: false, // is set only after the full run + failure: false, + filesAdded: 0, + // combines individual test results + removed files after the full run + filesRemoved: 0, + filesUnmatched: 0, + filesUpdated: 0, + matched: 0, + total: 0, + unchecked: 0, + unmatched: 0, + updated: 0, + }, + startTime: 0, + success: true, + testResults: [], + wasInterrupted: false, + }; +}; + +const buildFailureTestResult = ( + testPath: string, + err: SerializableError, +): TestResult => { + 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, + }, + sourceMaps: {}, + testExecError: err, + testFilePath: testPath, + testResults: [], + }; +}; + +// Add individual test result to an aggregated test result +const addResult = ( + aggregatedResults: AggregatedResult, + testResult: TestResult, +): void => { + 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; +}; + +module.exports = { + addResult, + buildFailureTestResult, + makeEmptyAggregatedTestResult, +};