diff --git a/CHANGELOG.md b/CHANGELOG.md index 8db5b756640b..102e9f3337b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes - `[babel-plugin-jest-hoist]` Ignore `TSTypeQuery` when checking for hoisted references ([#13367](https://github.com/facebook/jest/pull/13367)) +- `[jest-core]` Fix `detectOpenHandles` false positives for some special objects such as `TLSWRAP`. ([#13414](https://github.com/facebook/jest/pull/13414)) - `[jest-mock]` Fix mocking of getters and setters on classes ([#13398](https://github.com/facebook/jest/pull/13398)) - `[jest-reporters]` Revert: Transform file paths into hyperlinks ([#13399](https://github.com/facebook/jest/pull/13399)) - `[@jest/types]` Infer type of `each` table correctly when the table is a tuple or array ([#13381](https://github.com/facebook/jest/pull/13381)) diff --git a/packages/jest-core/src/__tests__/collectHandles.test.js b/packages/jest-core/src/__tests__/collectHandles.test.js index 229ccb9a2c7a..db56aeff2184 100644 --- a/packages/jest-core/src/__tests__/collectHandles.test.js +++ b/packages/jest-core/src/__tests__/collectHandles.test.js @@ -10,6 +10,7 @@ import * as crypto from 'crypto'; import {promises as dns} from 'dns'; import http from 'http'; import {PerformanceObserver} from 'perf_hooks'; +import {TLSSocket} from 'tls'; import zlib from 'zlib'; import collectHandles from '../collectHandles'; @@ -134,4 +135,15 @@ describe('collectHandles', () => { expect.objectContaining({message: 'TCPSERVERWRAP'}), ); }); + + it('should not be false positives for some special objects such as `TLSWRAP`', async () => { + const handleCollector = collectHandles(); + + const socket = new TLSSocket(); + socket.destroy(); + + const openHandles = await handleCollector(); + + expect(openHandles).toHaveLength(0); + }); }); diff --git a/packages/jest-core/src/collectHandles.ts b/packages/jest-core/src/collectHandles.ts index 5e69fd920e71..f5e79eafed9e 100644 --- a/packages/jest-core/src/collectHandles.ts +++ b/packages/jest-core/src/collectHandles.ts @@ -9,6 +9,8 @@ import * as asyncHooks from 'async_hooks'; import {promisify} from 'util'; +import * as v8 from 'v8'; +import * as vm from 'vm'; import stripAnsi = require('strip-ansi'); import type {Config} from '@jest/types'; import {formatExecError} from 'jest-message-util'; @@ -45,6 +47,22 @@ const hasWeakRef = typeof WeakRef === 'function'; const asyncSleep = promisify(setTimeout); +let gcFunc: (() => void) | undefined = (globalThis as any).gc; +function runGC() { + if (!gcFunc) { + v8.setFlagsFromString('--expose-gc'); + gcFunc = vm.runInNewContext('gc'); + v8.setFlagsFromString('--no-expose-gc'); + if (!gcFunc) { + throw new Error( + 'Cannot find `global.gc` function. Please run node with `--expose-gc` and report this issue in jest repo.', + ); + } + } + + gcFunc(); +} + // 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(): HandleCollectionResult { @@ -125,6 +143,14 @@ export default function collectHandles(): HandleCollectionResult { // callback, we will not yet have seen the resource be destroyed here. await asyncSleep(100); + if (activeHandles.size > 0) { + // For some special objects such as `TLSWRAP`. + // Ref: https://github.com/facebook/jest/issues/11665 + runGC(); + + await asyncSleep(0); + } + hook.disable(); // Get errors for every async resource still referenced at this moment