diff --git a/e2e/__tests__/__snapshots__/detectOpenHandles.ts.snap b/e2e/__tests__/__snapshots__/detectOpenHandles.ts.snap index c6f4f2ec3518..f9da6c8306a0 100644 --- a/e2e/__tests__/__snapshots__/detectOpenHandles.ts.snap +++ b/e2e/__tests__/__snapshots__/detectOpenHandles.ts.snap @@ -13,7 +13,7 @@ This usually means that there are asynchronous operations that weren't stopped i exports[`prints out info about open handlers 1`] = ` "Jest has detected the following 1 open handle potentially keeping Jest from exiting: - ● TCPSERVERWRAP + ● DNSCHANNEL,TCPSERVERWRAP 12 | const app = new Server(); 13 | diff --git a/packages/jest-core/src/__tests__/collectHandles.test.js b/packages/jest-core/src/__tests__/collectHandles.test.js index db56aeff2184..f98fe5d41ce4 100644 --- a/packages/jest-core/src/__tests__/collectHandles.test.js +++ b/packages/jest-core/src/__tests__/collectHandles.test.js @@ -45,9 +45,12 @@ describe('collectHandles', () => { it('should not collect the DNSCHANNEL open handle', async () => { const handleCollector = collectHandles(); - const resolver = new dns.Resolver(); + let resolver = new dns.Resolver(); resolver.getServers(); + // We must drop references to it + resolver = null; + const openHandles = await handleCollector(); expect(openHandles).not.toContainEqual( diff --git a/packages/jest-core/src/collectHandles.ts b/packages/jest-core/src/collectHandles.ts index f5e79eafed9e..16a727832569 100644 --- a/packages/jest-core/src/collectHandles.ts +++ b/packages/jest-core/src/collectHandles.ts @@ -83,16 +83,7 @@ export default function collectHandles(): HandleCollectionResult { // Skip resources that should not generally prevent the process from // exiting, not last a meaningfully long time, or otherwise shouldn't be // tracked. - if ( - type === 'PROMISE' || - type === 'TIMERWRAP' || - type === 'ELDHISTOGRAM' || - type === 'PerformanceObserver' || - type === 'RANDOMBYTESREQUEST' || - type === 'DNSCHANNEL' || - type === 'ZLIB' || - type === 'SIGNREQUEST' - ) { + if (type === 'PROMISE') { return; } const error = new ErrorWithStack(type, initHook, 100); @@ -141,14 +132,18 @@ export default function collectHandles(): HandleCollectionResult { // For example, Node.js TCP Servers are not destroyed until *after* their // `close` callback runs. If someone finishes a test from the `close` // callback, we will not yet have seen the resource be destroyed here. - await asyncSleep(100); + await asyncSleep(0); if (activeHandles.size > 0) { - // For some special objects such as `TLSWRAP`. - // Ref: https://github.com/facebook/jest/issues/11665 - runGC(); + await asyncSleep(30); - await asyncSleep(0); + 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(); @@ -167,33 +162,44 @@ export function formatHandleErrors( errors: Array, config: Config.ProjectConfig, ): Array { - const stacks = new Set(); - - return ( - errors - .map(err => - formatExecError(err, config, {noStackTrace: false}, undefined, true), - ) - // E.g. timeouts might give multiple traces to the same line of code - // This hairy filtering tries to remove entries with duplicate stack traces - .filter(handle => { - const ansiFree: string = stripAnsi(handle); - - const match = ansiFree.match(/\s+at(.*)/); - - if (!match || match.length < 2) { - return true; - } + const stacks = new Map}>(); + + errors.forEach(err => { + const formatted = formatExecError( + err, + config, + {noStackTrace: false}, + undefined, + true, + ); - const stack = ansiFree.substr(ansiFree.indexOf(match[1])).trim(); + // E.g. timeouts might give multiple traces to the same line of code + // This hairy filtering tries to remove entries with duplicate stack traces - if (stacks.has(stack)) { - return false; - } + const ansiFree: string = stripAnsi(formatted); + const match = ansiFree.match(/\s+at(.*)/); + if (!match || match.length < 2) { + return; + } - stacks.add(stack); + const stackText = ansiFree.substr(ansiFree.indexOf(match[1])).trim(); + + const name = ansiFree.match(/(?<=● {2}).*$/m); + if (!name?.length) { + return; + } + + const stack = stacks.get(stackText) || { + names: new Set(), + stack: formatted.replace(name[0], '%%OBJECT_NAME%%'), + }; + + stack.names.add(name[0]); + + stacks.set(stackText, stack); + }); - return true; - }) + return Array.from(stacks.values()).map(({stack, names}) => + stack.replace('%%OBJECT_NAME%%', Array.from(names).join(',')), ); }