diff --git a/docs/CLI.md b/docs/CLI.md index cbf248a951e3..2d9cadec97e7 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -177,7 +177,7 @@ 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 +[`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..9b3307a58a19 --- /dev/null +++ b/packages/jest-cli/src/get_node_handles.js @@ -0,0 +1,83 @@ +/** + * 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'; + +// This works for nodes without the async_hooks as the require's lazy +// $FlowFixMe: Node core module +import asyncHooks from 'async_hooks'; + +import { + formatStackTrace, + separateMessageFromStack, +} from '../../jest-message-util/build'; + +// 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) { + if (type === 'TIMERWRAP') return; + 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 { + 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 ae90611dc304..42375b3dc0b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8790,10 +8790,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" @@ -9673,12 +9669,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"