Skip to content

Commit 4727f80

Browse files
authored
Improve detectOpenHandles (#13417)
1 parent bd2305b commit 4727f80

File tree

4 files changed

+55
-43
lines changed

4 files changed

+55
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### Features
44

5+
- `[@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))
56
- `[@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))
67
- `[jest-environment-jsdom]` [**BREAKING**] Upgrade JSDOM to v22 ([#13825](https://github.com/jestjs/jest/pull/13825))
78
- `[@jest/fake-timers]` [**BREAKING**] Upgrade `@sinonjs/fake-timers` to v11 ([#14544](https://github.com/jestjs/jest/pull/14544))

e2e/__tests__/__snapshots__/detectOpenHandles.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ exports[`prints message about flag on slow tests with a custom timeout 1`] = `
1919
exports[`prints out info about open handlers 1`] = `
2020
"Jest has detected the following 1 open handle potentially keeping Jest from exiting:
2121
22-
● TCPSERVERWRAP
22+
DNSCHANNEL,TCPSERVERWRAP
2323
2424
12 | const app = new Server();
2525
13 |

packages/jest-core/src/__tests__/collectHandles.test.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,28 @@ describe('collectHandles', () => {
3131

3232
it('should not collect the PerformanceObserver open handle', async () => {
3333
const handleCollector = collectHandles();
34-
const obs = new PerformanceObserver((list, observer) => {});
34+
35+
let obs = new PerformanceObserver((list, observer) => {});
3536
obs.observe({entryTypes: ['mark']});
37+
obs.disconnect();
38+
obs = null;
3639

3740
const openHandles = await handleCollector();
3841

3942
expect(openHandles).not.toContainEqual(
4043
expect.objectContaining({message: 'PerformanceObserver'}),
4144
);
42-
obs.disconnect();
4345
});
4446

4547
it('should not collect the DNSCHANNEL open handle', async () => {
4648
const handleCollector = collectHandles();
4749

48-
const resolver = new dns.Resolver();
50+
let resolver = new dns.Resolver();
4951
resolver.getServers();
5052

53+
// We must drop references to it
54+
resolver = null;
55+
5156
const openHandles = await handleCollector();
5257

5358
expect(openHandles).not.toContainEqual(

packages/jest-core/src/collectHandles.ts

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,7 @@ export default function collectHandles(): HandleCollectionResult {
8383
// Skip resources that should not generally prevent the process from
8484
// exiting, not last a meaningfully long time, or otherwise shouldn't be
8585
// tracked.
86-
if (
87-
type === 'PROMISE' ||
88-
type === 'TIMERWRAP' ||
89-
type === 'ELDHISTOGRAM' ||
90-
type === 'PerformanceObserver' ||
91-
type === 'RANDOMBYTESREQUEST' ||
92-
type === 'DNSCHANNEL' ||
93-
type === 'ZLIB' ||
94-
type === 'SIGNREQUEST'
95-
) {
86+
if (type === 'PROMISE') {
9687
return;
9788
}
9889
const error = new ErrorWithStack(type, initHook, 100);
@@ -141,14 +132,18 @@ export default function collectHandles(): HandleCollectionResult {
141132
// For example, Node.js TCP Servers are not destroyed until *after* their
142133
// `close` callback runs. If someone finishes a test from the `close`
143134
// callback, we will not yet have seen the resource be destroyed here.
144-
await asyncSleep(100);
135+
await asyncSleep(0);
145136

146137
if (activeHandles.size > 0) {
147-
// For some special objects such as `TLSWRAP`.
148-
// Ref: https://github.com/jestjs/jest/issues/11665
149-
runGC();
138+
await asyncSleep(30);
150139

151-
await asyncSleep(0);
140+
if (activeHandles.size > 0) {
141+
// For some special objects such as `TLSWRAP`.
142+
// Ref: https://github.com/jestjs/jest/issues/11665
143+
runGC();
144+
145+
await asyncSleep(0);
146+
}
152147
}
153148

154149
hook.disable();
@@ -167,33 +162,44 @@ export function formatHandleErrors(
167162
errors: Array<Error>,
168163
config: Config.ProjectConfig,
169164
): Array<string> {
170-
const stacks = new Set();
171-
172-
return (
173-
errors
174-
.map(err =>
175-
formatExecError(err, config, {noStackTrace: false}, undefined, true),
176-
)
177-
// E.g. timeouts might give multiple traces to the same line of code
178-
// This hairy filtering tries to remove entries with duplicate stack traces
179-
.filter(handle => {
180-
const ansiFree: string = stripAnsi(handle);
181-
182-
const match = ansiFree.match(/\s+at(.*)/);
183-
184-
if (!match || match.length < 2) {
185-
return true;
186-
}
165+
const stacks = new Map<string, {stack: string; names: Set<string>}>();
166+
167+
for (const err of errors) {
168+
const formatted = formatExecError(
169+
err,
170+
config,
171+
{noStackTrace: false},
172+
undefined,
173+
true,
174+
);
187175

188-
const stack = ansiFree.substr(ansiFree.indexOf(match[1])).trim();
176+
// E.g. timeouts might give multiple traces to the same line of code
177+
// This hairy filtering tries to remove entries with duplicate stack traces
189178

190-
if (stacks.has(stack)) {
191-
return false;
192-
}
179+
const ansiFree: string = stripAnsi(formatted);
180+
const match = ansiFree.match(/\s+at(.*)/);
181+
if (!match || match.length < 2) {
182+
continue;
183+
}
193184

194-
stacks.add(stack);
185+
const stackText = ansiFree.slice(ansiFree.indexOf(match[1])).trim();
186+
187+
const name = ansiFree.match(/(?<= {2}).*$/m);
188+
if (name == null || name.length === 0) {
189+
continue;
190+
}
191+
192+
const stack = stacks.get(stackText) || {
193+
names: new Set(),
194+
stack: formatted.replace(name[0], '%%OBJECT_NAME%%'),
195+
};
196+
197+
stack.names.add(name[0]);
198+
199+
stacks.set(stackText, stack);
200+
}
195201

196-
return true;
197-
})
202+
return Array.from(stacks.values()).map(({stack, names}) =>
203+
stack.replace('%%OBJECT_NAME%%', Array.from(names).join(',')),
198204
);
199205
}

0 commit comments

Comments
 (0)