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/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/docs/CLI.md b/docs/CLI.md index 071ecba4d5e7..8416dc5952b0 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 +[`async_hooks`](https://nodejs.org/api/async_hooks.html), 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` diff --git a/docs/Configuration.md b/docs/Configuration.md index 968393c34cca..85e4e7ee0ae4 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -963,6 +963,7 @@ structure as the first argument and return it: "numPassedTests": number, "numFailedTests": number, "numPendingTests": number, + "openHandles": Array, "testResults": [{ "numFailingTests": number, "numPassingTests": number, 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..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 @@ -4,7 +4,10 @@ exports[`CLI accepts exact file names if matchers matched 1`] = ` "PASS foo/bar.spec.js ✓ foo -" + +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 0ebbf251c966..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 @@ -4,7 +4,10 @@ exports[`prints console.logs when run with forceExit 1`] = ` "PASS __tests__/a-banana.js ✓ banana -" + +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 new file mode 100644 index 000000000000..824eab153450 --- /dev/null +++ b/integration-tests/__tests__/__snapshots__/detect_open_handles.js.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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 did not exit one second after the test run has completed. + +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`] = ` +"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__/__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, diff --git a/integration-tests/__tests__/detect_open_handles.js b/integration-tests/__tests__/detect_open_handles.js new file mode 100644 index 000000000000..abce6500ae7a --- /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 did not exit one second after the test run has completed.', + ); + 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/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/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 0c428cedadff..4835ebcf49ce 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -20,6 +20,7 @@ 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'; @@ -28,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'; @@ -101,6 +103,19 @@ export const runCLI = async ( ); } + const {openHandles} = results; + + if (openHandles && openHandles.length) { + 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`, + ) + formatHandleErrors(openHandles, configs[0]).join('\n\n'); + + console.error(message); + } + return Promise.resolve({globalConfig, results}); }; @@ -113,7 +128,31 @@ const readResultsAndExit = ( process.on('exit', () => (process.exitCode = code)); if (globalConfig.forceExit) { + if (!globalConfig.detectOpenHandles) { + console.error( + chalk.red.bold('Force exiting Jest\n\n') + + chalk.red( + 'Have you considered using `--detectOpenHandles` to detect ' + + 'async operations that kept running after all tests finished?', + ), + ); + } + exit(code); + } else if (!globalConfig.detectOpenHandles) { + setTimeout(() => { + console.error( + chalk.red.bold( + '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(); } }; 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..18ad05f097be --- /dev/null +++ b/packages/jest-cli/src/get_node_handles.js @@ -0,0 +1,71 @@ +/** + * 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 {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 +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 => + formatExecError(err, config, {noStackTrace: false}, undefined, true), + ); +} diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index f4bd499ddbff..3e076f81bcfa 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -26,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 collectNodeHandles from './get_node_handles'; const setConfig = (contexts, newConfig) => contexts.forEach( @@ -68,17 +69,31 @@ const getTestPaths = async ( }; const processResults = (runResults, options) => { - const {outputFile} = options; - if (options.testResultsProcessor) { + const { + outputFile, + isJSON, + onComplete, + outputStream, + testResultsProcessor, + collectHandles, + } = options; + + if (collectHandles) { + runResults.openHandles = collectHandles(); + } else { + runResults.openHandles = []; + } + + 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`, ); @@ -86,7 +101,8 @@ const processResults = (runResults, options) => { process.stdout.write(JSON.stringify(formatTestResults(runResults))); } } - return options.onComplete && options.onComplete(runResults); + + return onComplete && onComplete(runResults); }; const testSchedulerContext = { @@ -237,6 +253,13 @@ 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 collectHandles; + + if (globalConfig.detectOpenHandles) { + collectHandles = collectNodeHandles(); + } + if (globalConfig.globalSetup) { // $FlowFixMe const globalSetup = require(globalConfig.globalSetup); @@ -274,6 +297,7 @@ export default (async function runJest({ await globalTeardown(); } return processResults(results, { + collectHandles, isJSON: globalConfig.json, onComplete, outputFile: globalConfig.outputFile, 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/packages/jest-cli/src/test_scheduler.js b/packages/jest-cli/src/test_scheduler.js index ed784ca06904..587757591a5a 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') + @@ -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-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/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) => { 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, diff --git a/types/TestResult.js b/types/TestResult.js index 7a293f5862f2..46cebb44fcc6 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, @@ -149,6 +150,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 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: