diff --git a/CHANGELOG.md b/CHANGELOG.md index 499763ef5c2c..c11760bdec13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[@jest/core]` [**BREAKING**] Group together open handles with the same stack trace ([#13417](https://github.com/jestjs/jest/pull/13417), & [#14543](https://github.com/jestjs/jest/pull/14543)) - `[@jest/core, @jest/test-sequencer]` [**BREAKING**] Exposes `globalConfig` & `contexts` to `TestSequencer` ([#14535](https://github.com/jestjs/jest/pull/14535), & [#14543](https://github.com/jestjs/jest/pull/14543)) - `[jest-environment-jsdom]` [**BREAKING**] Upgrade JSDOM to v22 ([#13825](https://github.com/jestjs/jest/pull/13825)) - `[@jest/fake-timers]` [**BREAKING**] Upgrade `@sinonjs/fake-timers` to v11 ([#14544](https://github.com/jestjs/jest/pull/14544)) diff --git a/e2e/__tests__/__snapshots__/detectOpenHandles.ts.snap b/e2e/__tests__/__snapshots__/detectOpenHandles.ts.snap index 70946474f87a..ec2d6e1d4e8b 100644 --- a/e2e/__tests__/__snapshots__/detectOpenHandles.ts.snap +++ b/e2e/__tests__/__snapshots__/detectOpenHandles.ts.snap @@ -19,7 +19,7 @@ exports[`prints message about flag on slow tests with a custom timeout 1`] = ` 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 836e83dd058b..0c77e7783e11 100644 --- a/packages/jest-core/src/__tests__/collectHandles.test.js +++ b/packages/jest-core/src/__tests__/collectHandles.test.js @@ -31,23 +31,28 @@ describe('collectHandles', () => { it('should not collect the PerformanceObserver open handle', async () => { const handleCollector = collectHandles(); - const obs = new PerformanceObserver((list, observer) => {}); + + let obs = new PerformanceObserver((list, observer) => {}); obs.observe({entryTypes: ['mark']}); + obs.disconnect(); + obs = null; const openHandles = await handleCollector(); expect(openHandles).not.toContainEqual( expect.objectContaining({message: 'PerformanceObserver'}), ); - obs.disconnect(); }); 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 3a9172fd18f1..087255f8106b 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/jestjs/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/jestjs/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}>(); + + for (const err of errors) { + 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) { + continue; + } - stacks.add(stack); + const stackText = ansiFree.slice(ansiFree.indexOf(match[1])).trim(); + + const name = ansiFree.match(/(?<=● {2}).*$/m); + if (name == null || name.length === 0) { + continue; + } + + 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(',')), ); }