From 2a74f8077a020c15879495be291bca29fcf882c9 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 3 May 2018 09:32:20 +0200 Subject: [PATCH 01/21] why-running --- packages/jest-cli/package.json | 1 + packages/jest-cli/src/cli/index.js | 31 +++++++++++++++++++++++++++++- yarn.lock | 10 ++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/jest-cli/package.json b/packages/jest-cli/package.json index f9481087456d..ad8805653f8f 100644 --- a/packages/jest-cli/package.json +++ b/packages/jest-cli/package.json @@ -37,6 +37,7 @@ "string-length": "^2.0.0", "strip-ansi": "^4.0.0", "which": "^1.2.12", + "why-is-node-running": "^2.0.2", "yargs": "^11.0.0" }, "bin": { diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 0c428cedadff..e374e837948e 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -11,6 +11,7 @@ import type {AggregatedResult} from 'types/TestResult'; import type {Argv} from 'types/Argv'; import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; +import util from 'util'; import {Console, clearLine, createDirectory} from 'jest-util'; import {validateCLIOptions} from 'jest-validate'; import {readConfig, deprecationEntries} from 'jest-config'; @@ -37,8 +38,9 @@ export async function run(maybeArgv?: Argv, project?: Path) { const argv: Argv = buildArgv(maybeArgv, project); const projects = getProjectListFromCLIArgs(argv, project); + const whyRunning = require('why-is-node-running'); const {results, globalConfig} = await runCLI(argv, projects); - readResultsAndExit(results, globalConfig); + readResultsAndExit(results, globalConfig, whyRunning); } catch (error) { clearLine(process.stderr); clearLine(process.stdout); @@ -107,9 +109,36 @@ export const runCLI = async ( const readResultsAndExit = ( result: ?AggregatedResult, globalConfig: GlobalConfig, + whyRunning: () => void, ) => { const code = !result || result.success ? 0 : globalConfig.testFailureExitCode; + const whyRunningArray = []; + const fakeLogger = { + error(...args) { + whyRunningArray.push(util.format(...args)); + }, + }; + + whyRunning(fakeLogger); + + if (whyRunningArray.length) { + const runningResult = whyRunningArray + .join('\n') + .split('\n\n') + .filter(entry => { + if (entry.startsWith('There are') || !entry) { + return true; + } + + return entry + .split('\n') + .slice(1) + .some(l => l.includes('this._execModule(')); + }) + .join('\n'); + console.error(runningResult); + } process.on('exit', () => (process.exitCode = code)); if (globalConfig.forceExit) { diff --git a/yarn.lock b/yarn.lock index 355b5e1a297a..7efc31c1d1df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8802,6 +8802,10 @@ stack-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + stacktrace-parser@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.4.tgz#01397922e5f62ecf30845522c95c4fe1d25e7d4e" @@ -9681,6 +9685,12 @@ which@^1.2.1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0: dependencies: isexe "^2.0.0" +why-is-node-running@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.0.2.tgz#faf352f095356c8c37a28bf645f874e5648c8d02" + dependencies: + stackback "0.0.2" + wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" From 4a7f44acf9f6bfa09cbb4d0275e610d64fc7681e Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 08:08:32 +0200 Subject: [PATCH 02/21] add --detectOpenHandles flag --- TestUtils.js | 2 ++ packages/jest-cli/package.json | 4 ++- packages/jest-cli/src/cli/args.js | 7 ++++ packages/jest-cli/src/cli/index.js | 31 +---------------- packages/jest-cli/src/run_jest.js | 50 +++++++++++++++++++++++++++ packages/jest-config/src/defaults.js | 1 + packages/jest-config/src/index.js | 2 ++ packages/jest-config/src/normalize.js | 1 + types/Config.js | 4 +++ 9 files changed, 71 insertions(+), 31 deletions(-) diff --git a/TestUtils.js b/TestUtils.js index 2f98711e565d..2faf446dde4a 100644 --- a/TestUtils.js +++ b/TestUtils.js @@ -22,6 +22,7 @@ const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { coverageReporters: [], coverageThreshold: {global: {}}, detectLeaks: false, + detectOpenHandles: false, enabledTestsMap: null, expand: false, filter: null, @@ -72,6 +73,7 @@ const DEFAULT_PROJECT_CONFIG: ProjectConfig = { coveragePathIgnorePatterns: [], cwd: '/test_root_dir/', detectLeaks: false, + detectOpenHandles: false, displayName: undefined, filter: null, forceCoverageMatch: [], diff --git a/packages/jest-cli/package.json b/packages/jest-cli/package.json index ad8805653f8f..5b61b31091cf 100644 --- a/packages/jest-cli/package.json +++ b/packages/jest-cli/package.json @@ -37,9 +37,11 @@ "string-length": "^2.0.0", "strip-ansi": "^4.0.0", "which": "^1.2.12", - "why-is-node-running": "^2.0.2", "yargs": "^11.0.0" }, + "optionalDependencies": { + "why-is-node-running": "^2.0.2" + }, "bin": { "jest": "./bin/jest.js" }, diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index 679297e77527..6acf3ae16985 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -225,6 +225,13 @@ export const options = { 'if it was leaked', type: 'boolean', }, + detectOpenHandles: { + default: false, + description: + 'Print out remaining open handles preventing Jest from exiting at the ' + + 'end of a test run.', + type: 'boolean', + }, env: { description: 'The test environment used for all tests. This can point to ' + diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index e374e837948e..0c428cedadff 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -11,7 +11,6 @@ import type {AggregatedResult} from 'types/TestResult'; import type {Argv} from 'types/Argv'; import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; -import util from 'util'; import {Console, clearLine, createDirectory} from 'jest-util'; import {validateCLIOptions} from 'jest-validate'; import {readConfig, deprecationEntries} from 'jest-config'; @@ -38,9 +37,8 @@ export async function run(maybeArgv?: Argv, project?: Path) { const argv: Argv = buildArgv(maybeArgv, project); const projects = getProjectListFromCLIArgs(argv, project); - const whyRunning = require('why-is-node-running'); const {results, globalConfig} = await runCLI(argv, projects); - readResultsAndExit(results, globalConfig, whyRunning); + readResultsAndExit(results, globalConfig); } catch (error) { clearLine(process.stderr); clearLine(process.stdout); @@ -109,36 +107,9 @@ export const runCLI = async ( const readResultsAndExit = ( result: ?AggregatedResult, globalConfig: GlobalConfig, - whyRunning: () => void, ) => { const code = !result || result.success ? 0 : globalConfig.testFailureExitCode; - const whyRunningArray = []; - const fakeLogger = { - error(...args) { - whyRunningArray.push(util.format(...args)); - }, - }; - - whyRunning(fakeLogger); - - if (whyRunningArray.length) { - const runningResult = whyRunningArray - .join('\n') - .split('\n\n') - .filter(entry => { - if (entry.startsWith('There are') || !entry) { - return true; - } - - return entry - .split('\n') - .slice(1) - .some(l => l.includes('this._execModule(')); - }) - .join('\n'); - console.error(runningResult); - } process.on('exit', () => (process.exitCode = code)); if (globalConfig.forceExit) { diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index f4bd499ddbff..955227ec4f50 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -16,6 +16,7 @@ import type TestWatcher from './test_watcher'; import micromatch from 'micromatch'; import chalk from 'chalk'; import path from 'path'; +import util from 'util'; import {Console, formatTestResults} from 'jest-util'; import exit from 'exit'; import fs from 'graceful-fs'; @@ -86,6 +87,36 @@ const processResults = (runResults, options) => { process.stdout.write(JSON.stringify(formatTestResults(runResults))); } } + + if (options.whyRunning) { + const whyRunningArray = []; + const fakeLogger = { + error(...args) { + whyRunningArray.push(util.format(...args)); + }, + }; + + options.whyRunning(fakeLogger); + + if (whyRunningArray.length) { + const runningResult = whyRunningArray + .join('\n') + .split('\n\n') + .filter(entry => { + if (entry.startsWith('There are') || !entry) { + return false; + } + + return entry + .split('\n') + .slice(1) + .some(l => l.includes('this._execModule(')); + }) + .join('\n'); + console.error(runningResult); + } + } + return options.onComplete && options.onComplete(runResults); }; @@ -237,6 +268,24 @@ export default (async function runJest({ // original value of rootDir. Instead, use the {cwd: Path} property to resolve // paths when printing. setConfig(contexts, {cwd: process.cwd()}); + + let whyRunning; + + if (globalConfig.detectOpenHandles) { + try { + whyRunning = require('why-is-node-running'); + } catch (e) { + const nodeMajor = Number(process.versions.node.split('.')[0]); + if (e.code === 'MODULE_NOT_FOUND' && nodeMajor < 8) { + throw new Error( + 'You can only use --detectOpenHandles on Node 8 and newer.', + ); + } else { + throw e; + } + } + } + if (globalConfig.globalSetup) { // $FlowFixMe const globalSetup = require(globalConfig.globalSetup); @@ -279,5 +328,6 @@ export default (async function runJest({ outputFile: globalConfig.outputFile, outputStream, testResultsProcessor: globalConfig.testResultsProcessor, + whyRunning, }); }); diff --git a/packages/jest-config/src/defaults.js b/packages/jest-config/src/defaults.js index 20f1c7ddcbb5..4aab986b64d9 100644 --- a/packages/jest-config/src/defaults.js +++ b/packages/jest-config/src/defaults.js @@ -38,6 +38,7 @@ export default ({ coveragePathIgnorePatterns: [NODE_MODULES_REGEXP], coverageReporters: ['json', 'text', 'lcov', 'clover'], detectLeaks: false, + detectOpenHandles: false, expand: false, filter: null, forceCoverageMatch: [], diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index ab0821c0c2c8..8b3074957c52 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -108,6 +108,7 @@ const getConfigs = ( coverageReporters: options.coverageReporters, coverageThreshold: options.coverageThreshold, detectLeaks: options.detectLeaks, + detectOpenHandles: options.detectOpenHandles, enabledTestsMap: options.enabledTestsMap, expand: options.expand, filter: options.filter, @@ -157,6 +158,7 @@ const getConfigs = ( coveragePathIgnorePatterns: options.coveragePathIgnorePatterns, cwd: options.cwd, detectLeaks: options.detectLeaks, + detectOpenHandles: options.detectOpenHandles, displayName: options.displayName, filter: options.filter, forceCoverageMatch: options.forceCoverageMatch, diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index f70ed6f02dd5..ad848a7a2b0a 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -510,6 +510,7 @@ export default function normalize(options: InitialOptions, argv: Argv) { case 'coverageReporters': case 'coverageThreshold': case 'detectLeaks': + case 'detectOpenHandles': case 'displayName': case 'expand': case 'globals': diff --git a/types/Config.js b/types/Config.js index de8b5ff9edb4..dfafaef93e16 100644 --- a/types/Config.js +++ b/types/Config.js @@ -39,6 +39,7 @@ export type DefaultOptions = {| globalTeardown: ?string, haste: HasteConfig, detectLeaks: boolean, + detectOpenHandles: boolean, moduleDirectories: Array, moduleFileExtensions: Array, moduleNameMapper: {[key: string]: string}, @@ -89,6 +90,7 @@ export type InitialOptions = { coverageReporters?: Array, coverageThreshold?: {global: {[key: string]: number}}, detectLeaks?: boolean, + detectOpenHandles?: boolean, displayName?: string, expand?: boolean, filter?: Path, @@ -176,6 +178,7 @@ export type GlobalConfig = {| coverageReporters: Array, coverageThreshold: {global: {[key: string]: number}}, detectLeaks: boolean, + detectOpenHandles: boolean, enabledTestsMap: ?{[key: string]: {[key: string]: boolean}}, expand: boolean, filter: ?Path, @@ -226,6 +229,7 @@ export type ProjectConfig = {| coveragePathIgnorePatterns: Array, cwd: Path, detectLeaks: boolean, + detectOpenHandles: boolean, displayName: ?string, filter: ?Path, forceCoverageMatch: Array, From b6f72761b95e40373a7095b8879bf5c8143c3e46 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 08:21:37 +0200 Subject: [PATCH 03/21] move processing of w-i-n-r into separate function --- packages/jest-cli/src/run_jest.js | 88 +++++++++++++++++++------------ 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index 955227ec4f50..3a87041966e0 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -68,18 +68,65 @@ const getTestPaths = async ( }); }; +function formatWhyRunning(whyRunning) { + const whyRunningArray = []; + const fakeLogger = { + error(...args) { + whyRunningArray.push(util.format(...args)); + }, + }; + + whyRunning(fakeLogger); + + return whyRunningArray + .join('\n') + .split('\n\n') + .filter(entry => { + if (entry.startsWith('There are') || !entry) { + return false; + } + + return entry.split('\n').some(l => l.includes('this._execModule(')); + }) + .map(entry => { + const [title, ...lines] = entry.split('\n'); + + const entries = lines + .map(line => line.split(/\s+-\s+/)) + .map(([file, line]) => ({file, line})); + + return { + entries, + title: title.replace('# ', ''), + }; + }); +} + const processResults = (runResults, options) => { - const {outputFile} = options; - if (options.testResultsProcessor) { + const { + outputFile, + isJSON, + onComplete, + outputStream, + testResultsProcessor, + whyRunning, + } = options; + + if (whyRunning) { + const runningResult = formatWhyRunning(whyRunning); + console.error(runningResult); + } + + if (testResultsProcessor) { /* $FlowFixMe */ - runResults = require(options.testResultsProcessor)(runResults); + runResults = require(testResultsProcessor)(runResults); } - if (options.isJSON) { + if (isJSON) { if (outputFile) { const filePath = path.resolve(process.cwd(), outputFile); fs.writeFileSync(filePath, JSON.stringify(formatTestResults(runResults))); - options.outputStream.write( + outputStream.write( `Test results written to: ` + `${path.relative(process.cwd(), filePath)}\n`, ); @@ -88,36 +135,7 @@ const processResults = (runResults, options) => { } } - if (options.whyRunning) { - const whyRunningArray = []; - const fakeLogger = { - error(...args) { - whyRunningArray.push(util.format(...args)); - }, - }; - - options.whyRunning(fakeLogger); - - if (whyRunningArray.length) { - const runningResult = whyRunningArray - .join('\n') - .split('\n\n') - .filter(entry => { - if (entry.startsWith('There are') || !entry) { - return false; - } - - return entry - .split('\n') - .slice(1) - .some(l => l.includes('this._execModule(')); - }) - .join('\n'); - console.error(runningResult); - } - } - - return options.onComplete && options.onComplete(runResults); + return onComplete && onComplete(runResults); }; const testSchedulerContext = { From 74f00066099b7bd4ba2b75c5c703c9d5ca7df338 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 08:51:24 +0200 Subject: [PATCH 04/21] pretty-print open handles --- docs/Configuration.md | 4 ++++ packages/jest-cli/package.json | 1 + packages/jest-cli/src/cli/index.js | 28 +++++++++++++++++++++++++ packages/jest-cli/src/run_jest.js | 3 +-- packages/jest-cli/src/test_scheduler.js | 2 +- types/TestResult.js | 6 ++++++ yarn.lock | 10 ++++++++- 7 files changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 968393c34cca..437609d6f018 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -963,6 +963,10 @@ structure as the first argument and return it: "numPassedTests": number, "numFailedTests": number, "numPendingTests": number, + "openHandles": Array<{ + title: string, + entries: Array<{file: string, line: string}>, + }>, "testResults": [{ "numFailingTests": number, "numPassingTests": number, diff --git a/packages/jest-cli/package.json b/packages/jest-cli/package.json index 5b61b31091cf..e30635997199 100644 --- a/packages/jest-cli/package.json +++ b/packages/jest-cli/package.json @@ -6,6 +6,7 @@ "dependencies": { "ansi-escapes": "^3.0.0", "chalk": "^2.0.1", + "easy-table": "^1.1.1", "exit": "^0.1.2", "glob": "^7.1.2", "graceful-fs": "^4.1.11", diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 0c428cedadff..36bf1b0de129 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -31,6 +31,7 @@ import watch from '../watch'; import yargs from 'yargs'; import rimraf from 'rimraf'; import {sync as realpath} from 'realpath-native'; +import Table from 'easy-table'; export async function run(maybeArgv?: Argv, project?: Path) { try { @@ -101,6 +102,33 @@ export const runCLI = async ( ); } + const {openHandles} = results; + + if (openHandles && openHandles.length) { + const handles = openHandles + .map(({title, entries}) => { + const table = new Table(); + + entries.forEach(({file, line}) => { + table.cell('File', file); + table.cell('Line', line); + table.newRow(); + }); + + return title + '\n' + table.toString(); + }) + .join('\n\n'); + + const message = + chalk.red( + '\nJest has detected the following ' + + `${openHandles.length} open handles potentially keeping Jest from ` + + 'exiting:\n\n', + ) + handles; + + console.error(message); + } + return Promise.resolve({globalConfig, results}); }; diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index 3a87041966e0..0b7f0fe0d17e 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -113,8 +113,7 @@ const processResults = (runResults, options) => { } = options; if (whyRunning) { - const runningResult = formatWhyRunning(whyRunning); - console.error(runningResult); + runResults.openHandles = formatWhyRunning(whyRunning); } if (testResultsProcessor) { diff --git a/packages/jest-cli/src/test_scheduler.js b/packages/jest-cli/src/test_scheduler.js index ed784ca06904..324b782e294f 100644 --- a/packages/jest-cli/src/test_scheduler.js +++ b/packages/jest-cli/src/test_scheduler.js @@ -109,7 +109,7 @@ export default class TestScheduler { }); } - // Throws when the context is leaked after executinga test. + // Throws when the context is leaked after executing a test. if (testResult.leaks) { const message = chalk.red.bold('EXPERIMENTAL FEATURE!\n') + diff --git a/types/TestResult.js b/types/TestResult.js index 7a293f5862f2..159d9a300c65 100644 --- a/types/TestResult.js +++ b/types/TestResult.js @@ -139,6 +139,11 @@ export type Suite = {| tests: Array, |}; +export type OpenHandle = {| + title: string, + entries: Array<{file: string, line: string}>, +|}; + export type TestResult = {| console: ?ConsoleBuffer, coverage?: RawCoverage, @@ -149,6 +154,7 @@ export type TestResult = {| numFailingTests: number, numPassingTests: number, numPendingTests: number, + openHandles: Array, perfStats: {| end: Milliseconds, start: Milliseconds, diff --git a/yarn.lock b/yarn.lock index 7efc31c1d1df..00521d109c1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3030,6 +3030,14 @@ duplexer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" +easy-table@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/easy-table/-/easy-table-1.1.1.tgz#c1b9b9ad68a017091a1c235e4bcba277540e143f" + dependencies: + ansi-regex "^3.0.0" + optionalDependencies: + wcwidth ">=1.0.1" + ecc-jsbn@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" @@ -9624,7 +9632,7 @@ watchify@^3.9.0: through2 "^2.0.0" xtend "^4.0.0" -wcwidth@^1.0.0: +wcwidth@>=1.0.1, wcwidth@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" dependencies: From 295038316291db9b0d1a36d2564d0ecef0132163 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 08:56:28 +0200 Subject: [PATCH 05/21] docs --- CHANGELOG.md | 3 +++ docs/CLI.md | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f349fe41eb5e..9c7acc04eb1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ### Features +* `[jest-cli]` Add `--detectOpenHandles` flag which enables Jest to potentially + track down handles keeping it open after tests are complete. + ([#6130](https://github.com/facebook/jest/pull/6130)) * `[jest-jasmine2]` Add data driven testing based on `jest-each` ([#6102](https://github.com/facebook/jest/pull/6102)) * `[jest-matcher-utils]` Change "suggest to equal" message to be more advisory diff --git a/docs/CLI.md b/docs/CLI.md index 071ecba4d5e7..cbf248a951e3 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -172,6 +172,14 @@ output. Print debugging info about your Jest config. +### `--detectOpenHandles` + +Attempt to collect and print open handles preventing Jest from exiting cleanly. +Use this in cases where you need to use `--forceExit` in order for Jest to exit +to potentially track down the reason. Implemented using +[`why-is-node-running`](https://github.com/mafintosh/why-is-node-running), so it +only works in Node 8 and newer. + ### `--env=` The test environment used for all tests. This can point to any file or node @@ -196,7 +204,8 @@ resources set up by test code cannot be adequately cleaned up. _Note: This feature is an escape-hatch. If Jest doesn't exit at the end of a test run, it means external resources are still being held on to or timers are still pending in your code. It is advised to tear down external resources after each test to -make sure Jest can shut down cleanly._ +make sure Jest can shut down cleanly. You can use `--detectOpenHandles` to help +track it down._ ### `--help` From 2dfa609344ac2486359f09eb02c25c22104a1693 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 09:06:41 +0200 Subject: [PATCH 06/21] update snapshots --- .../__tests__/__snapshots__/show_config.test.js.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration-tests/__tests__/__snapshots__/show_config.test.js.snap b/integration-tests/__tests__/__snapshots__/show_config.test.js.snap index 8ab5a9ffa1e7..4a12324818a5 100644 --- a/integration-tests/__tests__/__snapshots__/show_config.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/show_config.test.js.snap @@ -13,6 +13,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` \\"/node_modules/\\" ], \\"detectLeaks\\": false, + \\"detectOpenHandles\\": false, \\"filter\\": null, \\"forceCoverageMatch\\": [], \\"globals\\": {}, @@ -79,6 +80,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` \\"clover\\" ], \\"detectLeaks\\": false, + \\"detectOpenHandles\\": false, \\"expand\\": false, \\"filter\\": null, \\"globalSetup\\": null, From 11b043488af68e0b2326b39b88d57ab8544b7d52 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 09:08:50 +0200 Subject: [PATCH 07/21] ensure empty when flag not used --- packages/jest-cli/src/run_jest.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index 0b7f0fe0d17e..585872f45266 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -114,6 +114,8 @@ const processResults = (runResults, options) => { if (whyRunning) { runResults.openHandles = formatWhyRunning(whyRunning); + } else { + runResults.openHandles = []; } if (testResultsProcessor) { From c07c9e3e34cb194a82f70c9b1413f12bde026605 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 09:11:49 +0200 Subject: [PATCH 08/21] please flow gods --- .../src/legacy_code_todo_rewrite/jest_adapter_init.js | 1 + packages/jest-cli/src/test_result_helpers.js | 2 ++ types/TestResult.js | 1 + 3 files changed, 4 insertions(+) diff --git a/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js b/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js index 78975994e767..e2f85be6e1c3 100644 --- a/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js +++ b/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js @@ -132,6 +132,7 @@ export const runAndTransformResultsToJestFormat = async ({ numFailingTests, numPassingTests, numPendingTests, + openHandles: [], perfStats: { // populated outside end: 0, diff --git a/packages/jest-cli/src/test_result_helpers.js b/packages/jest-cli/src/test_result_helpers.js index e3ef7657d6af..a10bc5acf39f 100644 --- a/packages/jest-cli/src/test_result_helpers.js +++ b/packages/jest-cli/src/test_result_helpers.js @@ -24,6 +24,7 @@ export const makeEmptyAggregatedTestResult = (): AggregatedResult => { numRuntimeErrorTestSuites: 0, numTotalTestSuites: 0, numTotalTests: 0, + openHandles: [], snapshot: { added: 0, didUpdate: false, // is set only after the full run @@ -59,6 +60,7 @@ export const buildFailureTestResult = ( numFailingTests: 0, numPassingTests: 0, numPendingTests: 0, + openHandles: [], perfStats: { end: 0, start: 0, diff --git a/types/TestResult.js b/types/TestResult.js index 159d9a300c65..6c13a1fa7056 100644 --- a/types/TestResult.js +++ b/types/TestResult.js @@ -122,6 +122,7 @@ export type AggregatedResultWithoutCoverage = { numRuntimeErrorTestSuites: number, numTotalTests: number, numTotalTestSuites: number, + openHandles: Array, snapshot: SnapshotSummary, startTime: number, success: boolean, From ce04b4de4e76d951d447eda5241e88b16b61a47d Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 09:37:30 +0200 Subject: [PATCH 09/21] add pretty colors --- packages/jest-cli/package.json | 1 + packages/jest-cli/src/cli/index.js | 22 +++++++++++++++++----- yarn.lock | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/jest-cli/package.json b/packages/jest-cli/package.json index e30635997199..4cf9f9d30e9e 100644 --- a/packages/jest-cli/package.json +++ b/packages/jest-cli/package.json @@ -4,6 +4,7 @@ "version": "22.4.2", "main": "build/jest.js", "dependencies": { + "@babel/highlight": "^7.0.0-beta.46", "ansi-escapes": "^3.0.0", "chalk": "^2.0.1", "easy-table": "^1.1.1", diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 36bf1b0de129..14971ea06bfd 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -21,6 +21,7 @@ import createContext from '../lib/create_context'; import exit from 'exit'; import getChangedFilesPromise from '../get_changed_files_promise'; import fs from 'fs'; +import path from 'path'; import handleDeprecationWarnings from '../lib/handle_deprecation_warnings'; import logDebugMessages from '../lib/log_debug_messages'; import {print as preRunMessagePrint} from '../pre_run_message'; @@ -32,6 +33,8 @@ import yargs from 'yargs'; import rimraf from 'rimraf'; import {sync as realpath} from 'realpath-native'; import Table from 'easy-table'; +import slash from 'slash'; +import highlight from '@babel/highlight'; export async function run(maybeArgv?: Argv, project?: Path) { try { @@ -109,11 +112,20 @@ export const runCLI = async ( .map(({title, entries}) => { const table = new Table(); - entries.forEach(({file, line}) => { - table.cell('File', file); - table.cell('Line', line); - table.newRow(); - }); + entries + .map(({file, line}) => { + const relativeTestPath = slash( + path.relative(globalConfig.rootDir, file), + ); + const highlightedLine = highlight(line); + + return {file: relativeTestPath, line: highlightedLine}; + }) + .forEach(({file, line}) => { + table.cell('File', file); + table.cell('Line', line); + table.newRow(); + }); return title + '\n' + table.toString(); }) diff --git a/yarn.lock b/yarn.lock index 00521d109c1a..6d5e3d4dac1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,7 +8,7 @@ dependencies: "@babel/highlight" "7.0.0-beta.46" -"@babel/highlight@7.0.0-beta.46": +"@babel/highlight@7.0.0-beta.46", "@babel/highlight@^7.0.0-beta.46": version "7.0.0-beta.46" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.46.tgz#c553c51e65f572bdedd6eff66fc0bb563016645e" dependencies: From 2e19046780ce02b1298adaef5a36161842462005 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 09:47:26 +0200 Subject: [PATCH 10/21] less if --- packages/jest-cli/src/run_jest.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index 585872f45266..2fb62db2b255 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -109,14 +109,10 @@ const processResults = (runResults, options) => { onComplete, outputStream, testResultsProcessor, - whyRunning, + whyRunning = () => {}, } = options; - if (whyRunning) { - runResults.openHandles = formatWhyRunning(whyRunning); - } else { - runResults.openHandles = []; - } + runResults.openHandles = formatWhyRunning(whyRunning); if (testResultsProcessor) { /* $FlowFixMe */ From eaaf3a5adefced7807a54571bd9ee58a842a2f5f Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 09:56:52 +0200 Subject: [PATCH 11/21] use pluralize --- packages/jest-cli/src/cli/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 14971ea06bfd..da11c4aa9203 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -29,6 +29,7 @@ import runJest from '../run_jest'; import Runtime from 'jest-runtime'; import TestWatcher from '../test_watcher'; import watch from '../watch'; +import pluralize from '../pluralize'; import yargs from 'yargs'; import rimraf from 'rimraf'; import {sync as realpath} from 'realpath-native'; @@ -131,10 +132,12 @@ export const runCLI = async ( }) .join('\n\n'); + const openHandlesString = pluralize('open handle', openHandles.length, 's'); + const message = chalk.red( '\nJest has detected the following ' + - `${openHandles.length} open handles potentially keeping Jest from ` + + `${openHandlesString} potentially keeping Jest from ` + 'exiting:\n\n', ) + handles; From fd2dd3aca9f9dfc61126c7a2d1e62fcd496b2ef7 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 10:00:32 +0200 Subject: [PATCH 12/21] Revert "less if" This reverts commit dd7154835a0453a006d88b275acefd92661b9bc2. --- packages/jest-cli/src/run_jest.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index 2fb62db2b255..585872f45266 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -109,10 +109,14 @@ const processResults = (runResults, options) => { onComplete, outputStream, testResultsProcessor, - whyRunning = () => {}, + whyRunning, } = options; - runResults.openHandles = formatWhyRunning(whyRunning); + if (whyRunning) { + runResults.openHandles = formatWhyRunning(whyRunning); + } else { + runResults.openHandles = []; + } if (testResultsProcessor) { /* $FlowFixMe */ From dc55ef1fb9f67742c9e95c69b3c1af6c60a5a9e8 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 10:04:57 +0200 Subject: [PATCH 13/21] extract formatting of why-running --- .../jest-cli/src/format_why_node_running.js | 44 +++++++++++++++++++ packages/jest-cli/src/run_jest.js | 36 +-------------- 2 files changed, 45 insertions(+), 35 deletions(-) create mode 100644 packages/jest-cli/src/format_why_node_running.js diff --git a/packages/jest-cli/src/format_why_node_running.js b/packages/jest-cli/src/format_why_node_running.js new file mode 100644 index 000000000000..84bc6ddc6415 --- /dev/null +++ b/packages/jest-cli/src/format_why_node_running.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import util from 'util'; + +export default function formatWhyRunning(whyRunning) { + const whyRunningArray = []; + const fakeLogger = { + error(...args) { + whyRunningArray.push(util.format(...args)); + }, + }; + + whyRunning(fakeLogger); + + return whyRunningArray + .join('\n') + .split('\n\n') + .filter(entry => { + if (entry.startsWith('There are') || !entry) { + return false; + } + + return entry.split('\n').some(l => l.includes('this._execModule(')); + }) + .map(entry => { + const [title, ...lines] = entry.split('\n'); + + const entries = lines + .map(line => line.split(/\s+-\s+/)) + .map(([file, line]) => ({file, line})); + + return { + entries, + title: title.replace('# ', ''), + }; + }); +} diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index 585872f45266..e545850e4409 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -16,7 +16,6 @@ import type TestWatcher from './test_watcher'; import micromatch from 'micromatch'; import chalk from 'chalk'; import path from 'path'; -import util from 'util'; import {Console, formatTestResults} from 'jest-util'; import exit from 'exit'; import fs from 'graceful-fs'; @@ -27,6 +26,7 @@ import TestSequencer from './test_sequencer'; import {makeEmptyAggregatedTestResult} from './test_result_helpers'; import FailedTestsCache from './failed_tests_cache'; import JestHooks, {type JestHookEmitter} from './jest_hooks'; +import formatWhyRunning from './format_why_node_running'; const setConfig = (contexts, newConfig) => contexts.forEach( @@ -68,40 +68,6 @@ const getTestPaths = async ( }); }; -function formatWhyRunning(whyRunning) { - const whyRunningArray = []; - const fakeLogger = { - error(...args) { - whyRunningArray.push(util.format(...args)); - }, - }; - - whyRunning(fakeLogger); - - return whyRunningArray - .join('\n') - .split('\n\n') - .filter(entry => { - if (entry.startsWith('There are') || !entry) { - return false; - } - - return entry.split('\n').some(l => l.includes('this._execModule(')); - }) - .map(entry => { - const [title, ...lines] = entry.split('\n'); - - const entries = lines - .map(line => line.split(/\s+-\s+/)) - .map(([file, line]) => ({file, line})); - - return { - entries, - title: title.replace('# ', ''), - }; - }); -} - const processResults = (runResults, options) => { const { outputFile, From 93217dc83decf134f0fca91d234e62052371c1ce Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 10:10:40 +0200 Subject: [PATCH 14/21] cmon flow, be nice --- packages/jest-cli/src/format_why_node_running.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/jest-cli/src/format_why_node_running.js b/packages/jest-cli/src/format_why_node_running.js index 84bc6ddc6415..3cf702b4f642 100644 --- a/packages/jest-cli/src/format_why_node_running.js +++ b/packages/jest-cli/src/format_why_node_running.js @@ -7,9 +7,15 @@ * @flow */ +import type {OpenHandle} from 'types/TestResult'; + import util from 'util'; -export default function formatWhyRunning(whyRunning) { +type WhyIsNodeRunningCb = ({error: (...args: Array) => void}) => void; + +export default function formatWhyRunning( + whyRunning: WhyIsNodeRunningCb, +): Array { const whyRunningArray = []; const fakeLogger = { error(...args) { From bf5e5878167e12cf21dc48460af4bb5b40514496 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 16:08:49 +0200 Subject: [PATCH 15/21] use code frame --- packages/jest-cli/package.json | 2 -- packages/jest-cli/src/cli/index.js | 41 ++++++++++-------------------- yarn.lock | 12 ++------- 3 files changed, 16 insertions(+), 39 deletions(-) diff --git a/packages/jest-cli/package.json b/packages/jest-cli/package.json index 4cf9f9d30e9e..5b61b31091cf 100644 --- a/packages/jest-cli/package.json +++ b/packages/jest-cli/package.json @@ -4,10 +4,8 @@ "version": "22.4.2", "main": "build/jest.js", "dependencies": { - "@babel/highlight": "^7.0.0-beta.46", "ansi-escapes": "^3.0.0", "chalk": "^2.0.1", - "easy-table": "^1.1.1", "exit": "^0.1.2", "glob": "^7.1.2", "graceful-fs": "^4.1.11", diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index da11c4aa9203..6b5294961dc5 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -14,6 +14,7 @@ import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; import {Console, clearLine, createDirectory} from 'jest-util'; import {validateCLIOptions} from 'jest-validate'; import {readConfig, deprecationEntries} from 'jest-config'; +import {formatStackTrace} from 'jest-message-util'; import {version as VERSION} from '../../package.json'; import * as args from './args'; import chalk from 'chalk'; @@ -21,7 +22,6 @@ import createContext from '../lib/create_context'; import exit from 'exit'; import getChangedFilesPromise from '../get_changed_files_promise'; import fs from 'fs'; -import path from 'path'; import handleDeprecationWarnings from '../lib/handle_deprecation_warnings'; import logDebugMessages from '../lib/log_debug_messages'; import {print as preRunMessagePrint} from '../pre_run_message'; @@ -33,9 +33,6 @@ import pluralize from '../pluralize'; import yargs from 'yargs'; import rimraf from 'rimraf'; import {sync as realpath} from 'realpath-native'; -import Table from 'easy-table'; -import slash from 'slash'; -import highlight from '@babel/highlight'; export async function run(maybeArgv?: Argv, project?: Path) { try { @@ -110,35 +107,25 @@ export const runCLI = async ( if (openHandles && openHandles.length) { const handles = openHandles - .map(({title, entries}) => { - const table = new Table(); - - entries - .map(({file, line}) => { - const relativeTestPath = slash( - path.relative(globalConfig.rootDir, file), - ); - const highlightedLine = highlight(line); - - return {file: relativeTestPath, line: highlightedLine}; - }) - .forEach(({file, line}) => { - table.cell('File', file); - table.cell('Line', line); - table.newRow(); - }); - - return title + '\n' + table.toString(); - }) + .map(({title, entries}) => ({ + // Fake column to make it a valid stack trace + stack: entries.map(({file}) => `at ${file}:0`).join('\n'), + title, + })) + .map( + ({title, stack}) => + title + + '\n' + + // First config should be fine + formatStackTrace(stack, configs[0], {noStackTrace: false}), + ) .join('\n\n'); const openHandlesString = pluralize('open handle', openHandles.length, 's'); const message = chalk.red( - '\nJest has detected the following ' + - `${openHandlesString} potentially keeping Jest from ` + - 'exiting:\n\n', + `\nJest has detected the following ${openHandlesString} potentially keeping Jest from exiting:\n\n`, ) + handles; console.error(message); diff --git a/yarn.lock b/yarn.lock index 6d5e3d4dac1b..7efc31c1d1df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,7 +8,7 @@ dependencies: "@babel/highlight" "7.0.0-beta.46" -"@babel/highlight@7.0.0-beta.46", "@babel/highlight@^7.0.0-beta.46": +"@babel/highlight@7.0.0-beta.46": version "7.0.0-beta.46" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.46.tgz#c553c51e65f572bdedd6eff66fc0bb563016645e" dependencies: @@ -3030,14 +3030,6 @@ duplexer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" -easy-table@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/easy-table/-/easy-table-1.1.1.tgz#c1b9b9ad68a017091a1c235e4bcba277540e143f" - dependencies: - ansi-regex "^3.0.0" - optionalDependencies: - wcwidth ">=1.0.1" - ecc-jsbn@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" @@ -9632,7 +9624,7 @@ watchify@^3.9.0: through2 "^2.0.0" xtend "^4.0.0" -wcwidth@>=1.0.1, wcwidth@^1.0.0: +wcwidth@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" dependencies: From 1b1545114cd109444325266c38ce41f4e9e2b897 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 17:26:59 +0200 Subject: [PATCH 16/21] Implement async_hooks manually --- docs/CLI.md | 4 +- docs/Configuration.md | 5 +- packages/jest-cli/package.json | 3 - packages/jest-cli/src/cli/index.js | 19 +---- .../jest-cli/src/format_why_node_running.js | 50 ------------ packages/jest-cli/src/get_node_handles.js | 77 +++++++++++++++++++ packages/jest-cli/src/run_jest.js | 25 ++---- types/TestResult.js | 9 +-- yarn.lock | 10 --- 9 files changed, 91 insertions(+), 111 deletions(-) delete mode 100644 packages/jest-cli/src/format_why_node_running.js create mode 100644 packages/jest-cli/src/get_node_handles.js diff --git a/docs/CLI.md b/docs/CLI.md index cbf248a951e3..8416dc5952b0 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -177,8 +177,8 @@ Print debugging info about your Jest config. Attempt to collect and print open handles preventing Jest from exiting cleanly. Use this in cases where you need to use `--forceExit` in order for Jest to exit to potentially track down the reason. Implemented using -[`why-is-node-running`](https://github.com/mafintosh/why-is-node-running), so it -only works in Node 8 and newer. +[`async_hooks`](https://nodejs.org/api/async_hooks.html), so it only works in +Node 8 and newer. ### `--env=` diff --git a/docs/Configuration.md b/docs/Configuration.md index 437609d6f018..85e4e7ee0ae4 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -963,10 +963,7 @@ structure as the first argument and return it: "numPassedTests": number, "numFailedTests": number, "numPendingTests": number, - "openHandles": Array<{ - title: string, - entries: Array<{file: string, line: string}>, - }>, + "openHandles": Array, "testResults": [{ "numFailingTests": number, "numPassingTests": number, diff --git a/packages/jest-cli/package.json b/packages/jest-cli/package.json index 5b61b31091cf..f9481087456d 100644 --- a/packages/jest-cli/package.json +++ b/packages/jest-cli/package.json @@ -39,9 +39,6 @@ "which": "^1.2.12", "yargs": "^11.0.0" }, - "optionalDependencies": { - "why-is-node-running": "^2.0.2" - }, "bin": { "jest": "./bin/jest.js" }, diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 6b5294961dc5..523d3bce26f7 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -14,13 +14,13 @@ import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; import {Console, clearLine, createDirectory} from 'jest-util'; import {validateCLIOptions} from 'jest-validate'; import {readConfig, deprecationEntries} from 'jest-config'; -import {formatStackTrace} from 'jest-message-util'; import {version as VERSION} from '../../package.json'; import * as args from './args'; import chalk from 'chalk'; import createContext from '../lib/create_context'; import exit from 'exit'; import getChangedFilesPromise from '../get_changed_files_promise'; +import {formatHandleErrors} from '../get_node_handles'; import fs from 'fs'; import handleDeprecationWarnings from '../lib/handle_deprecation_warnings'; import logDebugMessages from '../lib/log_debug_messages'; @@ -106,27 +106,12 @@ export const runCLI = async ( const {openHandles} = results; if (openHandles && openHandles.length) { - const handles = openHandles - .map(({title, entries}) => ({ - // Fake column to make it a valid stack trace - stack: entries.map(({file}) => `at ${file}:0`).join('\n'), - title, - })) - .map( - ({title, stack}) => - title + - '\n' + - // First config should be fine - formatStackTrace(stack, configs[0], {noStackTrace: false}), - ) - .join('\n\n'); - const openHandlesString = pluralize('open handle', openHandles.length, 's'); const message = chalk.red( `\nJest has detected the following ${openHandlesString} potentially keeping Jest from exiting:\n\n`, - ) + handles; + ) + formatHandleErrors(openHandles, configs[0]).join('\n\n'); console.error(message); } diff --git a/packages/jest-cli/src/format_why_node_running.js b/packages/jest-cli/src/format_why_node_running.js deleted file mode 100644 index 3cf702b4f642..000000000000 --- a/packages/jest-cli/src/format_why_node_running.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {OpenHandle} from 'types/TestResult'; - -import util from 'util'; - -type WhyIsNodeRunningCb = ({error: (...args: Array) => void}) => void; - -export default function formatWhyRunning( - whyRunning: WhyIsNodeRunningCb, -): Array { - const whyRunningArray = []; - const fakeLogger = { - error(...args) { - whyRunningArray.push(util.format(...args)); - }, - }; - - whyRunning(fakeLogger); - - return whyRunningArray - .join('\n') - .split('\n\n') - .filter(entry => { - if (entry.startsWith('There are') || !entry) { - return false; - } - - return entry.split('\n').some(l => l.includes('this._execModule(')); - }) - .map(entry => { - const [title, ...lines] = entry.split('\n'); - - const entries = lines - .map(line => line.split(/\s+-\s+/)) - .map(([file, line]) => ({file, line})); - - return { - entries, - title: title.replace('# ', ''), - }; - }); -} diff --git a/packages/jest-cli/src/get_node_handles.js b/packages/jest-cli/src/get_node_handles.js new file mode 100644 index 000000000000..80515355b93f --- /dev/null +++ b/packages/jest-cli/src/get_node_handles.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ProjectConfig} from 'types/Config'; + +import {formatStackTrace, separateMessageFromStack} from 'jest-message-util'; + +// Inspired by https://github.com/mafintosh/why-is-node-running/blob/master/index.js +// Extracted as we want to format the result ourselves +export default function collectHandles(): () => Array { + const activeHandles: Map = new Map(); + + function initHook(asyncId, type) { + const error = new Error(type); + + if (Error.captureStackTrace) { + Error.captureStackTrace(error, initHook); + } + + if (error.stack.includes('Runtime.requireModule')) { + activeHandles.set(asyncId, error); + } + } + + let hook; + + try { + // $FlowFixMe: Node core module + const asyncHooks = require('async_hooks'); + hook = asyncHooks.createHook({ + destroy(asyncId) { + activeHandles.delete(asyncId); + }, + init: initHook, + }); + + hook.enable(); + } catch (e) { + const nodeMajor = Number(process.versions.node.split('.')[0]); + if (e.code === 'MODULE_NOT_FOUND' && nodeMajor < 8) { + throw new Error( + 'You can only use --detectOpenHandles on Node 8 and newer.', + ); + } else { + throw e; + } + } + + return () => { + hook.disable(); + + const result = Array.from(activeHandles.values()); + + activeHandles.clear(); + + return result; + }; +} + +export function formatHandleErrors( + errors: Array, + config: ProjectConfig, +): Array { + return errors.map(err => { + const {message, stack} = separateMessageFromStack(err.stack); + + return ( + message + '\n\n' + formatStackTrace(stack, config, {noStackTrace: false}) + ); + }); +} diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index e545850e4409..3e076f81bcfa 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -26,7 +26,7 @@ import TestSequencer from './test_sequencer'; import {makeEmptyAggregatedTestResult} from './test_result_helpers'; import FailedTestsCache from './failed_tests_cache'; import JestHooks, {type JestHookEmitter} from './jest_hooks'; -import formatWhyRunning from './format_why_node_running'; +import collectNodeHandles from './get_node_handles'; const setConfig = (contexts, newConfig) => contexts.forEach( @@ -75,11 +75,11 @@ const processResults = (runResults, options) => { onComplete, outputStream, testResultsProcessor, - whyRunning, + collectHandles, } = options; - if (whyRunning) { - runResults.openHandles = formatWhyRunning(whyRunning); + if (collectHandles) { + runResults.openHandles = collectHandles(); } else { runResults.openHandles = []; } @@ -254,21 +254,10 @@ export default (async function runJest({ // paths when printing. setConfig(contexts, {cwd: process.cwd()}); - let whyRunning; + let collectHandles; if (globalConfig.detectOpenHandles) { - try { - whyRunning = require('why-is-node-running'); - } catch (e) { - const nodeMajor = Number(process.versions.node.split('.')[0]); - if (e.code === 'MODULE_NOT_FOUND' && nodeMajor < 8) { - throw new Error( - 'You can only use --detectOpenHandles on Node 8 and newer.', - ); - } else { - throw e; - } - } + collectHandles = collectNodeHandles(); } if (globalConfig.globalSetup) { @@ -308,11 +297,11 @@ export default (async function runJest({ await globalTeardown(); } return processResults(results, { + collectHandles, isJSON: globalConfig.json, onComplete, outputFile: globalConfig.outputFile, outputStream, testResultsProcessor: globalConfig.testResultsProcessor, - whyRunning, }); }); diff --git a/types/TestResult.js b/types/TestResult.js index 6c13a1fa7056..46cebb44fcc6 100644 --- a/types/TestResult.js +++ b/types/TestResult.js @@ -122,7 +122,7 @@ export type AggregatedResultWithoutCoverage = { numRuntimeErrorTestSuites: number, numTotalTests: number, numTotalTestSuites: number, - openHandles: Array, + openHandles: Array, snapshot: SnapshotSummary, startTime: number, success: boolean, @@ -140,11 +140,6 @@ export type Suite = {| tests: Array, |}; -export type OpenHandle = {| - title: string, - entries: Array<{file: string, line: string}>, -|}; - export type TestResult = {| console: ?ConsoleBuffer, coverage?: RawCoverage, @@ -155,7 +150,7 @@ export type TestResult = {| numFailingTests: number, numPassingTests: number, numPendingTests: number, - openHandles: Array, + openHandles: Array, perfStats: {| end: Milliseconds, start: Milliseconds, diff --git a/yarn.lock b/yarn.lock index 7efc31c1d1df..355b5e1a297a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8802,10 +8802,6 @@ stack-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" -stackback@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" - stacktrace-parser@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.4.tgz#01397922e5f62ecf30845522c95c4fe1d25e7d4e" @@ -9685,12 +9681,6 @@ which@^1.2.1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0: dependencies: isexe "^2.0.0" -why-is-node-running@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.0.2.tgz#faf352f095356c8c37a28bf645f874e5648c8d02" - dependencies: - stackback "0.0.2" - wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" From a3ea1aa360b053a70a67e3a13fa43a708bf1ccf8 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 21:13:00 +0200 Subject: [PATCH 17/21] nudge users towards new feature --- packages/jest-cli/src/cli/index.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 523d3bce26f7..0cc6c466238b 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -128,7 +128,26 @@ const readResultsAndExit = ( process.on('exit', () => (process.exitCode = code)); if (globalConfig.forceExit) { + if (!globalConfig.detectOpenHandles) { + console.error( + chalk.red( + 'Force exiting Jest - have you considered using `--detectOpenHandles`?', + ), + ); + } + exit(code); + } else if (!globalConfig.detectOpenHandles) { + setTimeout(() => { + const lines = [ + chalk.red.bold( + 'Jest has not exited 1000ms after the test run finished', + ), + chalk.red('Have you considered using `--detectOpenHandles`?'), + ]; + console.error(lines.join('\n\n')); + // $FlowFixMe: `unref` exists in Node + }, 1000).unref(); } }; From a67c258af82b9dbaaea6fc41059274d04df6890e Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 22:40:14 +0200 Subject: [PATCH 18/21] make handlers better match other error output --- packages/jest-cli/src/get_node_handles.js | 12 +++---- packages/jest-cli/src/test_scheduler.js | 2 +- .../src/__tests__/messages.test.js | 5 +-- packages/jest-message-util/src/index.js | 31 ++++++++++--------- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/jest-cli/src/get_node_handles.js b/packages/jest-cli/src/get_node_handles.js index 80515355b93f..f44f373281b4 100644 --- a/packages/jest-cli/src/get_node_handles.js +++ b/packages/jest-cli/src/get_node_handles.js @@ -9,7 +9,7 @@ import type {ProjectConfig} from 'types/Config'; -import {formatStackTrace, separateMessageFromStack} from 'jest-message-util'; +import {formatExecError} from 'jest-message-util'; // Inspired by https://github.com/mafintosh/why-is-node-running/blob/master/index.js // Extracted as we want to format the result ourselves @@ -67,11 +67,7 @@ export function formatHandleErrors( errors: Array, config: ProjectConfig, ): Array { - return errors.map(err => { - const {message, stack} = separateMessageFromStack(err.stack); - - return ( - message + '\n\n' + formatStackTrace(stack, config, {noStackTrace: false}) - ); - }); + return errors.map(err => + formatExecError(err, config, {noStackTrace: false}, undefined, true), + ); } diff --git a/packages/jest-cli/src/test_scheduler.js b/packages/jest-cli/src/test_scheduler.js index 324b782e294f..587757591a5a 100644 --- a/packages/jest-cli/src/test_scheduler.js +++ b/packages/jest-cli/src/test_scheduler.js @@ -137,7 +137,7 @@ export default class TestScheduler { } const testResult = buildFailureTestResult(test.path, error); testResult.failureMessage = formatExecError( - testResult, + testResult.testExecError, test.context.config, this._globalConfig, test.path, diff --git a/packages/jest-message-util/src/__tests__/messages.test.js b/packages/jest-message-util/src/__tests__/messages.test.js index a148da84b339..6f8c9964e0c6 100644 --- a/packages/jest-message-util/src/__tests__/messages.test.js +++ b/packages/jest-message-util/src/__tests__/messages.test.js @@ -81,10 +81,7 @@ it('should exclude jasmine from stack trace for Unix paths.', () => { it('.formatExecError()', () => { const message = formatExecError( { - testExecError: { - message: 'Whoops!', - }, - testFilePath: '/test/error/file/path', + message: 'Whoops!', }, { rootDir: '', diff --git a/packages/jest-message-util/src/index.js b/packages/jest-message-util/src/index.js index f9fad8f394c2..941d03c5c166 100644 --- a/packages/jest-message-util/src/index.js +++ b/packages/jest-message-util/src/index.js @@ -8,7 +8,7 @@ */ import type {Glob, Path} from 'types/Config'; -import type {AssertionResult, TestResult} from 'types/TestResult'; +import type {AssertionResult, SerializableError} from 'types/TestResult'; import fs from 'fs'; import path from 'path'; @@ -97,23 +97,26 @@ const getRenderedCallsite = ( // `before/after each` hooks). If it's thrown, none of the tests in the file // are executed. export const formatExecError = ( - testResult: TestResult, + error?: Error | SerializableError | string, config: StackTraceConfig, options: StackTraceOptions, - testPath: Path, + testPath: ?Path, + reuseMessage: ?boolean, ) => { - let error = testResult.testExecError; if (!error || typeof error === 'number') { error = new Error(`Expected an Error, but "${String(error)}" was thrown`); error.stack = ''; } - let {message, stack} = error; + let message, stack; if (typeof error === 'string' || !error) { error || (error = 'EMPTY ERROR'); message = ''; stack = error; + } else { + message = error.message; + stack = error.stack; } const separated = separateMessageFromStack(stack || ''); @@ -138,15 +141,15 @@ export const formatExecError = ( message = MESSAGE_INDENT + 'Error: No message was provided'; } - return ( - TITLE_INDENT + - TITLE_BULLET + - EXEC_ERROR_MESSAGE + - '\n\n' + - message + - stack + - '\n' - ); + let messageToUse; + + if (reuseMessage) { + messageToUse = ` ${message.trim()}`; + } else { + messageToUse = `${EXEC_ERROR_MESSAGE}\n\n${message}`; + } + + return TITLE_INDENT + TITLE_BULLET + messageToUse + stack + '\n'; }; const removeInternalStackEntries = (lines, options: StackTraceOptions) => { From 82595e3c537b0b2cd9d873fa2c388b8cb95e9511 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 4 May 2018 23:36:57 +0200 Subject: [PATCH 19/21] add tests --- integration-tests/Utils.js | 2 +- .../cli-handles-exact-filenames.test.js.snap | 3 +- ...e_log_output_when_run_in_band.test.js.snap | 3 +- .../__snapshots__/detect_open_handles.js.snap | 24 +++++++ .../__tests__/detect_open_handles.js | 62 +++++++++++++++++ .../detect-open-handles/__tests__/test.js | 5 ++ .../detect-open-handles/package.json | 5 ++ .../detect-open-handles/server.js | 7 ++ integration-tests/runJest.js | 69 ++++++++++++++++++- package.json | 1 + yarn.lock | 2 +- 11 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap create mode 100644 integration-tests/__tests__/detect_open_handles.js create mode 100644 integration-tests/detect-open-handles/__tests__/test.js create mode 100644 integration-tests/detect-open-handles/package.json create mode 100644 integration-tests/detect-open-handles/server.js diff --git a/integration-tests/Utils.js b/integration-tests/Utils.js index 372a819c9264..01c668940707 100644 --- a/integration-tests/Utils.js +++ b/integration-tests/Utils.js @@ -156,7 +156,7 @@ const extractSummary = ( let rest = cleanupStackTrace( // remove all timestamps - stdout.slice(0, -match[0].length).replace(/\s*\(\d*\.?\d+m?s\)$/gm, ''), + stdout.replace(match[0], '').replace(/\s*\(\d*\.?\d+m?s\)$/gm, ''), ); if (stripLocation) { diff --git a/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap b/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap index a884b9170f0c..aace6737b4f4 100644 --- a/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap @@ -4,7 +4,8 @@ exports[`CLI accepts exact file names if matchers matched 1`] = ` "PASS foo/bar.spec.js ✓ foo -" + +Force exiting Jest - have you considered using \`--detectOpenHandles\`?" `; exports[`CLI accepts exact file names if matchers matched 2`] = ` diff --git a/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap b/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap index 0ebbf251c966..671651f0dd26 100644 --- a/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap @@ -4,7 +4,8 @@ exports[`prints console.logs when run with forceExit 1`] = ` "PASS __tests__/a-banana.js ✓ banana -" + +Force exiting Jest - have you considered using \`--detectOpenHandles\`?" `; exports[`prints console.logs when run with forceExit 2`] = ` diff --git a/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap b/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap new file mode 100644 index 000000000000..4a07a7f50caa --- /dev/null +++ b/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`prints message about flag on forceExit 1`] = `"Force exiting Jest - have you considered using \`--detectOpenHandles\`?"`; + +exports[`prints message about flag on slow tests 1`] = ` +"Jest has not exited 1000ms after the test run finished + +Have you considered using \`--detectOpenHandles\`?" +`; + +exports[`prints out info about open handlers 1`] = ` +"Jest has detected the following 1 open handle potentially keeping Jest from exiting: + + ● GETADDRINFOREQWRAP + + 5 | const app = new http.Server(); + 6 | + > 7 | app.listen({host: 'localhost', port: 0}); + | ^ + 8 | + + at Object. (server.js:7:5) + at Object. (__tests__/test.js:3:1)" +`; diff --git a/integration-tests/__tests__/detect_open_handles.js b/integration-tests/__tests__/detect_open_handles.js new file mode 100644 index 000000000000..0276987192ab --- /dev/null +++ b/integration-tests/__tests__/detect_open_handles.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +'use strict'; + +const runJest = require('../runJest'); + +try { + // $FlowFixMe: Node core + require('async_hooks'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + // eslint-disable-next-line jest/no-focused-tests + fit('skip test for unsupported nodes', () => { + console.warn('Skipping test for node ' + process.version); + }); + } else { + throw e; + } +} + +function getTextAfterTest(stderr) { + return stderr.split('Ran all test suites.')[1].trim(); +} + +it('prints message about flag on slow tests', async () => { + const {stderr} = await runJest.until( + 'detect-open-handles', + [], + 'Jest has not exited 1000ms after the test run finished', + ); + const textAfterTest = getTextAfterTest(stderr); + + expect(textAfterTest).toMatchSnapshot(); +}); + +it('prints message about flag on forceExit', async () => { + const {stderr} = await runJest.until( + 'detect-open-handles', + ['--forceExit'], + 'Force exiting Jest', + ); + const textAfterTest = getTextAfterTest(stderr); + + expect(textAfterTest).toMatchSnapshot(); +}); + +it('prints out info about open handlers', async () => { + const {stderr} = await runJest.until( + 'detect-open-handles', + ['--detectOpenHandles'], + 'Jest has detected', + ); + const textAfterTest = getTextAfterTest(stderr); + + expect(textAfterTest).toMatchSnapshot(); +}); diff --git a/integration-tests/detect-open-handles/__tests__/test.js b/integration-tests/detect-open-handles/__tests__/test.js new file mode 100644 index 000000000000..ded7c2155c98 --- /dev/null +++ b/integration-tests/detect-open-handles/__tests__/test.js @@ -0,0 +1,5 @@ +require('../server'); + +test('something', () => { + expect(true).toBe(true); +}); diff --git a/integration-tests/detect-open-handles/package.json b/integration-tests/detect-open-handles/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/integration-tests/detect-open-handles/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/integration-tests/detect-open-handles/server.js b/integration-tests/detect-open-handles/server.js new file mode 100644 index 000000000000..b3ea1278e835 --- /dev/null +++ b/integration-tests/detect-open-handles/server.js @@ -0,0 +1,7 @@ +'use strict'; + +const http = require('http'); + +const app = new http.Server(); + +app.listen({host: 'localhost', port: 0}); diff --git a/integration-tests/runJest.js b/integration-tests/runJest.js index 8e1dba6d43a1..4bf972add1bf 100644 --- a/integration-tests/runJest.js +++ b/integration-tests/runJest.js @@ -9,9 +9,12 @@ 'use strict'; const path = require('path'); -const {sync: spawnSync} = require('execa'); +const execa = require('execa'); +const {Writable} = require('readable-stream'); const {fileExists} = require('./Utils'); +const {sync: spawnSync} = execa; + const JEST_PATH = path.resolve(__dirname, '../packages/jest-cli/bin/jest.js'); type RunJestOptions = { @@ -67,9 +70,9 @@ function runJest( // 'success', 'startTime', 'numTotalTests', 'numTotalTestSuites', // 'numRuntimeErrorTestSuites', 'numPassedTests', 'numFailedTests', // 'numPendingTests', 'testResults' -runJest.json = function(dir: string, args?: Array) { +runJest.json = function(dir: string, args?: Array, ...rest) { args = [...(args || []), '--json']; - const result = runJest(dir, args); + const result = runJest(dir, args, ...rest); try { result.json = JSON.parse((result.stdout || '').toString()); } catch (e) { @@ -85,4 +88,64 @@ runJest.json = function(dir: string, args?: Array) { return result; }; +// Runs `jest` until a given output is achieved, then kills it with `SIGTERM` +runJest.until = async function( + dir: string, + args?: Array, + text: string, + options: RunJestOptions = {}, +) { + const isRelative = dir[0] !== '/'; + + if (isRelative) { + dir = path.resolve(__dirname, dir); + } + + const localPackageJson = path.resolve(dir, 'package.json'); + if (!options.skipPkgJsonCheck && !fileExists(localPackageJson)) { + throw new Error( + ` + Make sure you have a local package.json file at + "${localPackageJson}". + Otherwise Jest will try to traverse the directory tree and find the + the global package.json, which will send Jest into infinite loop. + `, + ); + } + + const env = options.nodePath + ? Object.assign({}, process.env, { + FORCE_COLOR: 0, + NODE_PATH: options.nodePath, + }) + : process.env; + + const jestPromise = execa(JEST_PATH, args || [], { + cwd: dir, + env, + reject: false, + }); + + jestPromise.stderr.pipe( + new Writable({ + write(chunk, encoding, callback) { + const output = chunk.toString('utf8'); + + if (output.includes(text)) { + jestPromise.kill(); + } + + callback(); + }, + }), + ); + + const result = await jestPromise; + + // For compat with cross-spawn + result.status = result.code; + + return result; +}; + module.exports = runJest; diff --git a/package.json b/package.json index 96112b7653a6..2b439e279e7b 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "prettier": "^1.12.1", "prettylint": "^1.0.0", "progress": "^2.0.0", + "readable-stream": "^2.3.6", "regenerator-runtime": "^0.11.0", "resolve": "^1.4.0", "rimraf": "^2.6.2", diff --git a/yarn.lock b/yarn.lock index 355b5e1a297a..84db775f89e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7779,7 +7779,7 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9", readable-stream@^1.0.26-4, isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@2, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3: +readable-stream@2, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" dependencies: From b4cf302bd59ad23c315cc22ceb0e377e92f889f7 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 6 May 2018 20:34:35 +0200 Subject: [PATCH 20/21] PR feeback --- .../cli-handles-exact-filenames.test.js.snap | 6 +++++- ...sole_log_output_when_run_in_band.test.js.snap | 6 +++++- .../__snapshots__/detect_open_handles.js.snap | 12 +++++++++--- .../__tests__/detect_open_handles.js | 2 +- packages/jest-cli/src/cli/index.js | 16 +++++++++++----- packages/jest-cli/src/get_node_handles.js | 2 -- 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap b/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap index aace6737b4f4..20b315dd2247 100644 --- a/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap @@ -5,7 +5,11 @@ exports[`CLI accepts exact file names if matchers matched 1`] = ` ✓ foo -Force exiting Jest - have you considered using \`--detectOpenHandles\`?" +Force exiting Jest + +Have you considered using \`--detectOpenHandles\`? + + to detect async operations that kept running after all tests finished?" `; exports[`CLI accepts exact file names if matchers matched 2`] = ` diff --git a/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap b/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap index 671651f0dd26..96e648e251ff 100644 --- a/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap @@ -5,7 +5,11 @@ exports[`prints console.logs when run with forceExit 1`] = ` ✓ banana -Force exiting Jest - have you considered using \`--detectOpenHandles\`?" +Force exiting Jest + +Have you considered using \`--detectOpenHandles\`? + + to detect async operations that kept running after all tests finished?" `; exports[`prints console.logs when run with forceExit 2`] = ` diff --git a/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap b/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap index 4a07a7f50caa..949e238be70a 100644 --- a/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap +++ b/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap @@ -1,11 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`prints message about flag on forceExit 1`] = `"Force exiting Jest - have you considered using \`--detectOpenHandles\`?"`; +exports[`prints message about flag on forceExit 1`] = ` +"Force exiting Jest + +Have you considered using \`--detectOpenHandles\`? + + to detect async operations that kept running after all tests finished?" +`; exports[`prints message about flag on slow tests 1`] = ` -"Jest has not exited 1000ms after the test run finished +"Jest did not exit one second after the test run has completed. -Have you considered using \`--detectOpenHandles\`?" +This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with \`--detectOpenHandles\` to troubleshoot this issue." `; exports[`prints out info about open handlers 1`] = ` diff --git a/integration-tests/__tests__/detect_open_handles.js b/integration-tests/__tests__/detect_open_handles.js index 0276987192ab..abce6500ae7a 100644 --- a/integration-tests/__tests__/detect_open_handles.js +++ b/integration-tests/__tests__/detect_open_handles.js @@ -32,7 +32,7 @@ it('prints message about flag on slow tests', async () => { const {stderr} = await runJest.until( 'detect-open-handles', [], - 'Jest has not exited 1000ms after the test run finished', + 'Jest did not exit one second after the test run has completed.', ); const textAfterTest = getTextAfterTest(stderr); diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 0cc6c466238b..fecb2fcc151e 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -130,9 +130,11 @@ const readResultsAndExit = ( if (globalConfig.forceExit) { if (!globalConfig.detectOpenHandles) { console.error( - chalk.red( - 'Force exiting Jest - have you considered using `--detectOpenHandles`?', - ), + chalk.red.bold('Force exiting Jest\n\n') + + chalk.red( + 'Have you considered using `--detectOpenHandles`?\n\n to detect ' + + 'async operations that kept running after all tests finished?', + ), ); } @@ -141,9 +143,13 @@ const readResultsAndExit = ( setTimeout(() => { const lines = [ chalk.red.bold( - 'Jest has not exited 1000ms after the test run finished', + 'Jest did not exit one second after the test run has completed.', + ), + chalk.red( + 'This usually means that there are asynchronous operations that ' + + "weren't stopped in your tests. Consider running Jest with " + + '`--detectOpenHandles` to troubleshoot this issue.', ), - chalk.red('Have you considered using `--detectOpenHandles`?'), ]; console.error(lines.join('\n\n')); // $FlowFixMe: `unref` exists in Node diff --git a/packages/jest-cli/src/get_node_handles.js b/packages/jest-cli/src/get_node_handles.js index f44f373281b4..18ad05f097be 100644 --- a/packages/jest-cli/src/get_node_handles.js +++ b/packages/jest-cli/src/get_node_handles.js @@ -56,9 +56,7 @@ export default function collectHandles(): () => Array { hook.disable(); const result = Array.from(activeHandles.values()); - activeHandles.clear(); - return result; }; } From 1c8c356725285c974f8cece58bafc195c05c99f6 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 6 May 2018 20:41:03 +0200 Subject: [PATCH 21/21] really fic PR feedback --- .../cli-handles-exact-filenames.test.js.snap | 4 +--- ...e_log_output_when_run_in_band.test.js.snap | 4 +--- .../__snapshots__/detect_open_handles.js.snap | 4 +--- packages/jest-cli/src/cli/index.js | 21 +++++++++---------- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap b/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap index 20b315dd2247..f450eea10d7e 100644 --- a/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/cli-handles-exact-filenames.test.js.snap @@ -7,9 +7,7 @@ exports[`CLI accepts exact file names if matchers matched 1`] = ` Force exiting Jest -Have you considered using \`--detectOpenHandles\`? - - to detect async operations that kept running after all tests finished?" +Have you considered using \`--detectOpenHandles\` to detect async operations that kept running after all tests finished?" `; exports[`CLI accepts exact file names if matchers matched 2`] = ` diff --git a/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap b/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap index 96e648e251ff..128a56ecb5a4 100644 --- a/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/console_log_output_when_run_in_band.test.js.snap @@ -7,9 +7,7 @@ exports[`prints console.logs when run with forceExit 1`] = ` Force exiting Jest -Have you considered using \`--detectOpenHandles\`? - - to detect async operations that kept running after all tests finished?" +Have you considered using \`--detectOpenHandles\` to detect async operations that kept running after all tests finished?" `; exports[`prints console.logs when run with forceExit 2`] = ` diff --git a/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap b/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap index 949e238be70a..824eab153450 100644 --- a/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap +++ b/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap @@ -3,9 +3,7 @@ exports[`prints message about flag on forceExit 1`] = ` "Force exiting Jest -Have you considered using \`--detectOpenHandles\`? - - to detect async operations that kept running after all tests finished?" +Have you considered using \`--detectOpenHandles\` to detect async operations that kept running after all tests finished?" `; exports[`prints message about flag on slow tests 1`] = ` diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index fecb2fcc151e..4835ebcf49ce 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -132,7 +132,7 @@ const readResultsAndExit = ( console.error( chalk.red.bold('Force exiting Jest\n\n') + chalk.red( - 'Have you considered using `--detectOpenHandles`?\n\n to detect ' + + 'Have you considered using `--detectOpenHandles` to detect ' + 'async operations that kept running after all tests finished?', ), ); @@ -141,17 +141,16 @@ const readResultsAndExit = ( exit(code); } else if (!globalConfig.detectOpenHandles) { setTimeout(() => { - const lines = [ + console.error( chalk.red.bold( - 'Jest did not exit one second after the test run has completed.', - ), - chalk.red( - 'This usually means that there are asynchronous operations that ' + - "weren't stopped in your tests. Consider running Jest with " + - '`--detectOpenHandles` to troubleshoot this issue.', - ), - ]; - console.error(lines.join('\n\n')); + 'Jest did not exit one second after the test run has completed.\n\n', + ) + + chalk.red( + 'This usually means that there are asynchronous operations that ' + + "weren't stopped in your tests. Consider running Jest with " + + '`--detectOpenHandles` to troubleshoot this issue.', + ), + ); // $FlowFixMe: `unref` exists in Node }, 1000).unref(); }