🐛 Bug Report
The --detectOpenHandles option fails to detect/warn about handles that were opened in test functions with a done callback. (It seems to work fine for promise-returning or async test functions.) I noticed this while looking into issues with Supertest in #8554 (comment)
To Reproduce
Given the following test file:
const http = require("http");
describe("detectOpenHandles", () => {
it("should detect an open server when using a `done` function", (done) => {
const server = http.createServer((_request, response) => response.end("ok"));
server.listen(0, () => done());
});
});
Running Jest with --detectOpenHandles hangs, but does not print any information about the open TCPSERVERWRAP handle:
$ jest --detectOpenHandles open-handles.test.js
PASS ./open-handles.test.js
detectOpenHandles
✓ should detect an open server when using a `done` function (7 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.297 s
Ran all test suites matching /open-handles.test.js/i
Expected Behavior
In contrast, using an async function or returning a promise works as expected. Given the following test file:
const http = require("http");
describe("detectOpenHandles", () => {
it("should detect an open server when using a promise", () => {
const server = http.createServer((_request, response) => response.end("ok"));
return new Promise(resolve => server.listen(0, resolve));
});
});
Running it successfully detects and prints information about the open server:
$ jest --detectOpenHandles open-handles.test.js
PASS ./open-handles.test.js
detectOpenHandles
✓ should detect an open server when using a promise (6 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.956 s
Ran all test suites matching /open-handles.test.js/i.
Jest has detected the following 1 open handle potentially keeping Jest from exiting:
● TCPSERVERWRAP
89 | it("should detect an open server when using a promise", () => {
90 | const server = http.createServer((_request, response) => response.end("ok"));
> 91 | return new Promise(resolve => server.listen(0, resolve));
| ^
92 | });
93 | });
at open-handles.test.js:91:42
at Object.<anonymous> (open-handles.test.js:91:12)
Cause
It looks like the issue is in how Jest decides which handles to track. In jest-core/src/collectHandles.ts, handles are only tracked if stackIsFromUser() returns true. It checks for the names of wrappers that run the test functions: https://github.com/facebook/jest/blob/ba84480a5603aeeb94184ffc26f9b39024cdcd6c/packages/jest-core/src/collectHandles.ts#L18-L38
That works great for functions that don’t take a done callback. They get wrapped with a function named asyncJestTest, which stackIsFromUser() looks for: https://github.com/facebook/jest/blob/ba84480a5603aeeb94184ffc26f9b39024cdcd6c/packages/jest-jasmine2/src/jasmineAsyncInstall.ts#L123-L153
But functions that take a done callback don’t get wrapped at all, and are anonymous or have no name that stackIsFromUser() knows to look for: https://github.com/facebook/jest/blob/ba84480a5603aeeb94184ffc26f9b39024cdcd6c/packages/jest-jasmine2/src/jasmineAsyncInstall.ts#L111-L113
It looks like lifecycle functions will experience the same issue (although I haven’t tested), given the similar code in promisifyLifeCycleFunction().
The simplest fix here might be to simply wrap functions that take a done callback with a function, too (e.g. asyncJestTestWithCallback) and add that to the list of things to watch for in stackIsFromUser().
envinfo
System:
OS: macOS 10.15.7
CPU: (4) x64 Intel(R) Core(TM) i7-5557U CPU @ 3.10GHz
Binaries:
Node: 16.1.0 - ~/.nvm/versions/node/v16.1.0/bin/node
Yarn: 1.22.10 - /usr/local/bin/yarn
npm: 7.11.2 - ~/.nvm/versions/node/v16.1.0/bin/npm
npmPackages:
jest: ^26.6.3 => 26.6.3
🐛 Bug Report
The
--detectOpenHandlesoption fails to detect/warn about handles that were opened in test functions with adonecallback. (It seems to work fine for promise-returning orasynctest functions.) I noticed this while looking into issues with Supertest in #8554 (comment)To Reproduce
Given the following test file:
Running Jest with
--detectOpenHandleshangs, but does not print any information about the openTCPSERVERWRAPhandle:$ jest --detectOpenHandles open-handles.test.js PASS ./open-handles.test.js detectOpenHandles ✓ should detect an open server when using a `done` function (7 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.297 s Ran all test suites matching /open-handles.test.js/iExpected Behavior
In contrast, using an async function or returning a promise works as expected. Given the following test file:
Running it successfully detects and prints information about the open server:
$ jest --detectOpenHandles open-handles.test.js PASS ./open-handles.test.js detectOpenHandles ✓ should detect an open server when using a promise (6 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.956 s Ran all test suites matching /open-handles.test.js/i. Jest has detected the following 1 open handle potentially keeping Jest from exiting: ● TCPSERVERWRAP 89 | it("should detect an open server when using a promise", () => { 90 | const server = http.createServer((_request, response) => response.end("ok")); > 91 | return new Promise(resolve => server.listen(0, resolve)); | ^ 92 | }); 93 | }); at open-handles.test.js:91:42 at Object.<anonymous> (open-handles.test.js:91:12)Cause
It looks like the issue is in how Jest decides which handles to track. In
jest-core/src/collectHandles.ts, handles are only tracked ifstackIsFromUser()returnstrue. It checks for the names of wrappers that run the test functions: https://github.com/facebook/jest/blob/ba84480a5603aeeb94184ffc26f9b39024cdcd6c/packages/jest-core/src/collectHandles.ts#L18-L38That works great for functions that don’t take a
donecallback. They get wrapped with a function namedasyncJestTest, whichstackIsFromUser()looks for: https://github.com/facebook/jest/blob/ba84480a5603aeeb94184ffc26f9b39024cdcd6c/packages/jest-jasmine2/src/jasmineAsyncInstall.ts#L123-L153But functions that take a
donecallback don’t get wrapped at all, and are anonymous or have no name thatstackIsFromUser()knows to look for: https://github.com/facebook/jest/blob/ba84480a5603aeeb94184ffc26f9b39024cdcd6c/packages/jest-jasmine2/src/jasmineAsyncInstall.ts#L111-L113It looks like lifecycle functions will experience the same issue (although I haven’t tested), given the similar code in
promisifyLifeCycleFunction().The simplest fix here might be to simply wrap functions that take a
donecallback with a function, too (e.g.asyncJestTestWithCallback) and add that to the list of things to watch for instackIsFromUser().envinfo