Skip to content

Commit 57e32e9

Browse files
authored
Detect open handles with done callbacks (#11382)
1 parent a397607 commit 57e32e9

File tree

7 files changed

+141
-4
lines changed

7 files changed

+141
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
- `[jest-console]` `console.dir` now respects the second argument correctly ([#10638](https://github.com/facebook/jest/pull/10638))
6969
- `[jest-core]` Don't report PerformanceObserver as open handle ([#11123](https://github.com/facebook/jest/pull/11123))
7070
- `[jest-core]` Use `WeakRef` to hold timers when detecting open handles ([#11277](https://github.com/facebook/jest/pull/11277))
71+
- `[jest-core]` Correctly detect open handles that were created in test functions using `done` callbacks ([#11382](https://github.com/facebook/jest/pull/11382))
7172
- `[jest-diff]` [**BREAKING**] Use only named exports ([#11371](https://github.com/facebook/jest/pull/11371))
7273
- `[jest-each]` [**BREAKING**] Ignore excess words in headings ([#8766](https://github.com/facebook/jest/pull/8766))
7374
- `[jest-each]` Support array index with template strings ([#10763](https://github.com/facebook/jest/pull/10763))
@@ -82,6 +83,7 @@
8283
- `[jest-haste-map]` Vendor `NodeWatcher` from `sane` ([#10919](https://github.com/facebook/jest/pull/10919))
8384
- `[jest-jasmine2]` Fixed the issue of `beforeAll` & `afterAll` hooks getting executed even if it is inside a skipped `describe` block when it has child `tests` marked as either `only` or `todo` [#10451](https://github.com/facebook/jest/issues/10451)
8485
- `[jest-jasmine2]` Fixed the issues of child `tests` marked with `only` or `todo` getting executed even if it is inside a skipped parent `describe` block [#10451](https://github.com/facebook/jest/issues/10451)
86+
- `[jest-jasmine2]` Wrap all test functions so they open handles that were created in test functions using `done` callbacks can be detected ([#11382](https://github.com/facebook/jest/pull/11382))
8587
- `[jest-reporter]` Handle empty files when reporting code coverage with V8 ([#10819](https://github.com/facebook/jest/pull/10819))
8688
- `[jest-resolve]` Replace read-pkg-up with escalade package ([#10781](https://github.com/facebook/jest/pull/10781))
8789
- `[jest-resolve]` Disable `jest-pnp-resolver` for Yarn 2 ([#10847](https://github.com/facebook/jest/pull/10847))

e2e/__tests__/__snapshots__/detectOpenHandles.ts.snap

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,35 @@ Jest has detected the following 1 open handle potentially keeping Jest from exit
3838
3939
at Object.setTimeout (__tests__/inside.js:9:3)
4040
`;
41+
42+
exports[`prints out info about open handlers from lifecycle functions with a \`done\` callback 1`] = `
43+
Jest has detected the following 1 open handle potentially keeping Jest from exiting:
44+
45+
● Timeout
46+
47+
7 |
48+
8 | beforeAll(done => {
49+
> 9 | setTimeout(() => {}, 10000);
50+
| ^
51+
10 | done();
52+
11 | });
53+
12 |
54+
55+
at setTimeout (__tests__/in-done-lifecycle.js:9:3)
56+
`;
57+
58+
exports[`prints out info about open handlers from tests with a \`done\` callback 1`] = `
59+
Jest has detected the following 1 open handle potentially keeping Jest from exiting:
60+
61+
● Timeout
62+
63+
7 |
64+
8 | test('something', done => {
65+
> 9 | setTimeout(() => {}, 10000);
66+
| ^
67+
10 | expect(true).toBe(true);
68+
11 | done();
69+
12 | });
70+
71+
at Object.setTimeout (__tests__/in-done-function.js:9:3)
72+
`;

e2e/__tests__/detectOpenHandles.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,35 @@ it('prints out info about open handlers from inside tests', async () => {
123123

124124
expect(wrap(textAfterTest)).toMatchSnapshot();
125125
});
126+
127+
it('prints out info about open handlers from tests with a `done` callback', async () => {
128+
const run = runContinuous('detect-open-handles', [
129+
'in-done-function',
130+
'--detectOpenHandles',
131+
]);
132+
await run.waitUntil(({stderr}) => stderr.includes('Jest has detected'));
133+
const {stderr} = await run.end();
134+
const textAfterTest = getTextAfterTest(stderr);
135+
136+
expect(wrap(textAfterTest)).toMatchSnapshot();
137+
});
138+
139+
it('prints out info about open handlers from lifecycle functions with a `done` callback', async () => {
140+
const run = runContinuous('detect-open-handles', [
141+
'in-done-lifecycle',
142+
'--detectOpenHandles',
143+
]);
144+
await run.waitUntil(({stderr}) => stderr.includes('Jest has detected'));
145+
const {stderr} = await run.end();
146+
let textAfterTest = getTextAfterTest(stderr);
147+
148+
// Circus and Jasmine have different contexts, leading to slightly different
149+
// names for call stack functions. The difference shouldn't be problematic
150+
// for users, so this normalizes them so the test works in both environments.
151+
textAfterTest = textAfterTest.replace(
152+
'at Object.setTimeout',
153+
'at setTimeout',
154+
);
155+
156+
expect(wrap(textAfterTest)).toMatchSnapshot();
157+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
test('something', done => {
9+
setTimeout(() => {}, 10000);
10+
expect(true).toBe(true);
11+
done();
12+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
beforeAll(done => {
9+
setTimeout(() => {}, 10000);
10+
done();
11+
});
12+
13+
test('something', () => {
14+
expect(true).toBe(true);
15+
});

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*
77
*/
88

9+
import http from 'http';
910
import {PerformanceObserver} from 'perf_hooks';
1011
import collectHandles from '../collectHandles';
1112

@@ -33,4 +34,20 @@ describe('collectHandles', () => {
3334
expect(openHandles).toHaveLength(0);
3435
obs.disconnect();
3536
});
37+
38+
it('should collect handles opened in test functions with `done` callbacks', done => {
39+
const handleCollector = collectHandles();
40+
const server = http.createServer((_, response) => response.end('ok'));
41+
server.listen(0, () => {
42+
// Collect results while server is still open.
43+
const openHandles = handleCollector();
44+
45+
server.close(() => {
46+
expect(openHandles).toContainEqual(
47+
expect.objectContaining({message: 'TCPSERVERWRAP'}),
48+
);
49+
done();
50+
});
51+
});
52+
});
3653
});

packages/jest-jasmine2/src/jasmineAsyncInstall.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,24 @@ function promisifyLifeCycleFunction(
4444
return originalFn.call(env);
4545
}
4646

47-
const hasDoneCallback = typeof fn === 'function' && fn.length > 0;
47+
if (typeof fn !== 'function') {
48+
// Pass non-functions to Jest, which throws a nice error.
49+
return originalFn.call(env, fn, timeout);
50+
}
51+
52+
const hasDoneCallback = fn.length > 0;
4853

4954
if (hasDoneCallback) {
50-
// Jasmine will handle it
51-
return originalFn.call(env, fn, timeout);
55+
// Give the function a name so it can be detected in call stacks, but
56+
// otherwise Jasmine will handle it.
57+
const asyncJestLifecycleWithCallback = function (
58+
this: Global.TestContext,
59+
...args: Array<any>
60+
) {
61+
// @ts-expect-error: Support possible extra args at runtime
62+
return fn.apply(this, args);
63+
};
64+
return originalFn.call(env, asyncJestLifecycleWithCallback, timeout);
5265
}
5366

5467
const extraError = new Error();
@@ -106,10 +119,24 @@ function promisifyIt(
106119
return spec;
107120
}
108121

122+
if (typeof fn !== 'function') {
123+
// Pass non-functions to Jest, which throws a nice error.
124+
return originalFn.call(env, specName, fn, timeout);
125+
}
126+
109127
const hasDoneCallback = fn.length > 0;
110128

111129
if (hasDoneCallback) {
112-
return originalFn.call(env, specName, fn, timeout);
130+
// Give the function a name so it can be detected in call stacks, but
131+
// otherwise Jasmine will handle it.
132+
const asyncJestTestWithCallback = function (
133+
this: Global.TestContext,
134+
...args: Array<any>
135+
) {
136+
// @ts-expect-error: Support possible extra args at runtime
137+
return fn.apply(this, args);
138+
};
139+
return originalFn.call(env, specName, asyncJestTestWithCallback, timeout);
113140
}
114141

115142
const extraError = new Error();

0 commit comments

Comments
 (0)