diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f621f66b734..a56f695b53df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - `[jest-config]` [**BREAKING**] Change default file extension order by moving json behind ts and tsx ([10572](https://github.com/facebook/jest/pull/10572)) - `[jest-console]` `console.dir` now respects the second argument correctly ([#10638](https://github.com/facebook/jest/pull/10638)) - `[jest-core]` Don't report PerformanceObserver as open handle ([#11123](https://github.com/facebook/jest/pull/11123)) +- `[jest-core]` Run GC detecting open handles ([#11278](https://github.com/facebook/jest/pull/11278)) - `[jest-each]` [**BREAKING**] Ignore excess words in headings ([#8766](https://github.com/facebook/jest/pull/8766)) - `[jest-environment]` [**BREAKING**] Drop support for `runScript` for test environments ([#11155](https://github.com/facebook/jest/pull/11155)) - `[jest-environment-jsdom]` Use inner realm’s `ArrayBuffer` constructor ([#10885](https://github.com/facebook/jest/pull/10885)) diff --git a/e2e/__tests__/detectOpenHandles.ts b/e2e/__tests__/detectOpenHandles.ts index 977434965a3f..3a302f752c90 100644 --- a/e2e/__tests__/detectOpenHandles.ts +++ b/e2e/__tests__/detectOpenHandles.ts @@ -71,6 +71,17 @@ it('does not report promises', () => { expect(textAfterTest).toBe(''); }); +it('does not report crypto random data', () => { + // The test here is basically that it exits cleanly without reporting anything (does not need `until`) + const {stderr} = runJest('detect-open-handles', [ + 'crypto', + '--detectOpenHandles', + ]); + const textAfterTest = getTextAfterTest(stderr); + + expect(textAfterTest).toBe(''); +}); + onNodeVersions('>=11.10.0', () => { it('does not report ELD histograms', () => { const {stderr} = runJest('detect-open-handles', [ diff --git a/e2e/detect-open-handles/__tests__/crypto.js b/e2e/detect-open-handles/__tests__/crypto.js new file mode 100644 index 000000000000..1a4284994d8e --- /dev/null +++ b/e2e/detect-open-handles/__tests__/crypto.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. 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. + */ + +const {randomFillSync} = require('crypto'); + +test('randomFillSync()', () => { + const buf = Buffer.alloc(10); + randomFillSync(buf); +}); diff --git a/packages/jest-core/src/cli/index.ts b/packages/jest-core/src/cli/index.ts index 639b277129ce..8d57af3a0742 100644 --- a/packages/jest-core/src/cli/index.ts +++ b/packages/jest-core/src/cli/index.ts @@ -32,7 +32,7 @@ import watch from '../watch'; const {print: preRunMessagePrint} = preRunMessage; -type OnCompleteCallback = (results: AggregatedResult) => void; +type OnCompleteCallback = (results: AggregatedResult) => void | undefined; export async function runCLI( argv: Config.Argv, @@ -89,7 +89,9 @@ export async function runCLI( configsOfProjectsToRun, hasDeprecationWarnings, outputStream, - r => (results = r), + r => { + results = r; + }, ); if (argv.watch || argv.watchAll) { diff --git a/packages/jest-core/src/collectHandles.ts b/packages/jest-core/src/collectHandles.ts index 13a8181ec3b4..0ea2bd111a0c 100644 --- a/packages/jest-core/src/collectHandles.ts +++ b/packages/jest-core/src/collectHandles.ts @@ -8,11 +8,16 @@ /* eslint-disable local/ban-types-eventually */ import * as asyncHooks from 'async_hooks'; +import {promisify} from 'util'; +import {setFlagsFromString} from 'v8'; +import {runInNewContext} from 'vm'; import stripAnsi = require('strip-ansi'); import type {Config} from '@jest/types'; import {formatExecError} from 'jest-message-util'; import {ErrorWithStack} from 'jest-util'; +export type HandleCollectionResult = () => Promise>; + function stackIsFromUser(stack: string) { // Either the test file, or something required by it if (stack.includes('Runtime.requireModule')) { @@ -37,13 +42,28 @@ function stackIsFromUser(stack: string) { const alwaysActive = () => true; +const tick = promisify(setImmediate); + +function runGarbageCollector() { + const isGarbageCollectorHidden = !global.gc; + + // GC is usually hidden, so we have to expose it before running. + setFlagsFromString('--expose-gc'); + runInNewContext('gc')(); + + // The GC was not initially exposed, so let's hide it again. + if (isGarbageCollectorHidden) { + setFlagsFromString('--no-expose-gc'); + } +} + // 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< +export default function collectHandles(): HandleCollectionResult { + const activeHandles = new Map< number, {error: Error; isActive: () => boolean} - > = new Map(); + >(); const hook = asyncHooks.createHook({ destroy(asyncId) { activeHandles.delete(asyncId); @@ -88,7 +108,14 @@ export default function collectHandles(): () => Array { hook.enable(); - return (): Array => { + return async () => { + runGarbageCollector(); + + // wait some ticks to allow GC to run properly, see https://github.com/nodejs/node/issues/34636#issuecomment-669366235 + for (let i = 0; i < 10; i++) { + await tick(); + } + hook.disable(); // Get errors for every async resource still referenced at this moment diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index 008f102a4a4e..9000e6da7b5a 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -26,7 +26,7 @@ import type FailedTestsCache from './FailedTestsCache'; import SearchSource from './SearchSource'; import TestScheduler, {TestSchedulerContext} from './TestScheduler'; import type TestWatcher from './TestWatcher'; -import collectNodeHandles from './collectHandles'; +import collectNodeHandles, {HandleCollectionResult} from './collectHandles'; import getNoTestsFoundMessage from './getNoTestsFoundMessage'; import runGlobalHook from './runGlobalHook'; import type {Filter, TestRunData} from './types'; @@ -70,12 +70,12 @@ type ProcessResultOptions = Pick< Config.GlobalConfig, 'json' | 'outputFile' | 'testResultsProcessor' > & { - collectHandles?: () => Array; + collectHandles?: HandleCollectionResult; onComplete?: (result: AggregatedResult) => void; outputStream: NodeJS.WriteStream; }; -const processResults = ( +const processResults = async ( runResults: AggregatedResult, options: ProcessResultOptions, ) => { @@ -89,7 +89,7 @@ const processResults = ( } = options; if (collectHandles) { - runResults.openHandles = collectHandles(); + runResults.openHandles = await collectHandles(); } else { runResults.openHandles = []; } @@ -111,7 +111,7 @@ const processResults = ( } } - return onComplete && onComplete(runResults); + onComplete?.(runResults); }; const testSchedulerContext: TestSchedulerContext = {