Skip to content

Commit 6c0a16a

Browse files
pimterrySimenB
authored andcommitted
Don't include unref'd timers in --detectOpenHandles results (#8941)
1 parent 5a45171 commit 6c0a16a

File tree

6 files changed

+73
-4
lines changed

6 files changed

+73
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
### Fixes
2626

2727
- `[expect]` Display expectedDiff more carefully in toBeCloseTo ([#8389](https://github.com/facebook/jest/pull/8389))
28+
- `[jest-core]` Don't include unref'd timers in --detectOpenHandles results ([#8941](https://github.com/facebook/jest/pull/8941))
2829
- `[jest-diff]` Do not inverse format if line consists of one change ([#8903](https://github.com/facebook/jest/pull/8903))
2930
- `[jest-fake-timers]` `getTimerCount` will not include cancelled immediates ([#8764](https://github.com/facebook/jest/pull/8764))
3031
- `[jest-leak-detector]` [**BREAKING**] Use `weak-napi` instead of `weak` package ([#8686](https://github.com/facebook/jest/pull/8686))

e2e/__tests__/detectOpenHandles.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import {wrap} from 'jest-snapshot-serializer-raw';
99
import runJest, {until} from '../runJest';
10+
import {onNodeVersions} from '../../packages/test-utils';
1011

1112
try {
1213
require('async_hooks');
@@ -69,6 +70,19 @@ it('does not report promises', () => {
6970
expect(textAfterTest).toBe('');
7071
});
7172

73+
onNodeVersions('>=11', () => {
74+
it('does not report timeouts using unref', () => {
75+
// The test here is basically that it exits cleanly without reporting anything (does not need `until`)
76+
const {stderr} = runJest('detect-open-handles', [
77+
'unref',
78+
'--detectOpenHandles',
79+
]);
80+
const textAfterTest = getTextAfterTest(stderr);
81+
82+
expect(textAfterTest).toBe('');
83+
});
84+
});
85+
7286
it('prints out info about open handlers from inside tests', async () => {
7387
const {stderr} = await until(
7488
'detect-open-handles',
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', () => {
9+
const timeout = setTimeout(() => {}, 30000);
10+
timeout.unref();
11+
expect(true).toBe(true);
12+
});

packages/jest-core/src/collectHandles.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,15 @@ function stackIsFromUser(stack: string) {
3434
return false;
3535
}
3636

37+
const alwaysActive = () => true;
38+
3739
// Inspired by https://github.com/mafintosh/why-is-node-running/blob/master/index.js
3840
// Extracted as we want to format the result ourselves
3941
export default function collectHandles(): () => Array<Error> {
40-
const activeHandles: Map<number, Error> = new Map();
42+
const activeHandles: Map<
43+
number,
44+
{error: Error; isActive: () => boolean}
45+
> = new Map();
4146

4247
let hook: AsyncHook;
4348

@@ -47,14 +52,34 @@ export default function collectHandles(): () => Array<Error> {
4752
destroy(asyncId) {
4853
activeHandles.delete(asyncId);
4954
},
50-
init: function initHook(asyncId, type) {
55+
init: function initHook(
56+
asyncId,
57+
type,
58+
_triggerAsyncId,
59+
resource: {} | NodeJS.Timeout,
60+
) {
5161
if (type === 'PROMISE' || type === 'TIMERWRAP') {
5262
return;
5363
}
5464
const error = new ErrorWithStack(type, initHook);
5565

5666
if (stackIsFromUser(error.stack || '')) {
57-
activeHandles.set(asyncId, error);
67+
let isActive: () => boolean;
68+
69+
if (type === 'Timeout' || type === 'Immediate') {
70+
if ('hasRef' in resource) {
71+
// Timer that supports hasRef (Node v11+)
72+
isActive = resource.hasRef.bind(resource);
73+
} else {
74+
// Timer that doesn't support hasRef
75+
isActive = alwaysActive;
76+
}
77+
} else {
78+
// Any other async resource
79+
isActive = alwaysActive;
80+
}
81+
82+
activeHandles.set(asyncId, {error, isActive});
5883
}
5984
},
6085
});
@@ -74,7 +99,11 @@ export default function collectHandles(): () => Array<Error> {
7499
return () => {
75100
hook.disable();
76101

77-
const result = Array.from(activeHandles.values());
102+
// Get errors for every async resource still referenced at this moment
103+
const result = Array.from(activeHandles.values())
104+
.filter(({isActive}) => isActive())
105+
.map(({error}) => error);
106+
78107
activeHandles.clear();
79108
return result;
80109
};

packages/test-utils/src/ConditionalTest.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
/* eslint-disable jest/no-focused-tests */
99

10+
import semver = require('semver');
11+
1012
export function isJestCircusRun() {
1113
return process.env.JEST_CIRCUS === '1';
1214
}
@@ -34,3 +36,13 @@ export function skipSuiteOnWindows() {
3436
});
3537
}
3638
}
39+
40+
export function onNodeVersions(versionRange: string, testBody: () => void) {
41+
if (!semver.satisfies(process.versions.node, versionRange)) {
42+
describe.skip(`[SKIP] tests that require node ${versionRange}`, () => {
43+
testBody();
44+
});
45+
} else {
46+
testBody();
47+
}
48+
}

packages/test-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export {
1010
skipSuiteOnJasmine,
1111
skipSuiteOnJestCircus,
1212
skipSuiteOnWindows,
13+
onNodeVersions,
1314
} from './ConditionalTest';

0 commit comments

Comments
 (0)