Skip to content

Commit 6d2394f

Browse files
rickhanloniimjesun
authored andcommitted
Fix a leak in coverage (#5289)
1 parent c799a02 commit 6d2394f

7 files changed

Lines changed: 175 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
## master
22

3+
## jest 22.0.7
4+
5+
### Fixes
6+
7+
* `[jest-runner]` Fix memory leak in coverage reporting ([#5289](https://github.com/facebook/jest/pull/5289))
8+
39
### Features
410

511
* `[jest-cli]` Make Jest exit without an error when no tests are found in

packages/jest-runner/src/run_test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ async function runTestInternal(
123123

124124
result.perfStats = {end: Date.now(), start};
125125
result.testFilePath = path;
126-
result.coverage = runtime.getAllCoverageInfo();
126+
result.coverage = runtime.getAllCoverageInfoCopy();
127127
result.sourceMaps = runtime.getSourceMapInfo();
128128
result.console = testConsole.getBuffer();
129129
result.skipped = testCount === result.numPendingTests;

packages/jest-runtime/src/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type {MockFunctionMetadata, ModuleMocker} from 'types/Mock';
1919
import path from 'path';
2020
import HasteMap from 'jest-haste-map';
2121
import Resolver from 'jest-resolve';
22-
import {createDirectory} from 'jest-util';
22+
import {createDirectory, deepCyclicCopy} from 'jest-util';
2323
import {escapePathForRegex} from 'jest-regex-util';
2424
import fs from 'graceful-fs';
2525
import stripBOM from 'strip-bom';
@@ -433,8 +433,8 @@ class Runtime {
433433
}
434434
}
435435

436-
getAllCoverageInfo() {
437-
return this._environment.global.__coverage__;
436+
getAllCoverageInfoCopy() {
437+
return deepCyclicCopy(this._environment.global.__coverage__);
438438
}
439439

440440
getSourceMapInfo() {

packages/jest-util/src/__tests__/deep_cyclic_copy.test.js

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,144 @@ it('uses the blacklist to avoid copying properties on the first level', () => {
7070
},
7171
};
7272

73-
expect(deepCyclicCopy(obj, new Set(['blacklisted']))).toEqual({
73+
expect(deepCyclicCopy(obj, {blacklist: new Set(['blacklisted'])})).toEqual({
7474
subObj: {
7575
blacklisted: 42,
7676
},
7777
});
7878
});
79+
80+
it('does not keep the prototype by default when top level is object', () => {
81+
const sourceObject = new function() {}();
82+
sourceObject.nestedObject = new function() {}();
83+
sourceObject.nestedArray = new function() {
84+
this.length = 0;
85+
}();
86+
87+
const spy = jest.spyOn(Array, 'isArray').mockImplementation(object => {
88+
return object === sourceObject.nestedArray;
89+
});
90+
91+
const copy = deepCyclicCopy(sourceObject, {keepPrototype: false});
92+
93+
expect(Object.getPrototypeOf(copy)).not.toBe(
94+
Object.getPrototypeOf(sourceObject),
95+
);
96+
expect(Object.getPrototypeOf(copy.nestedObject)).not.toBe(
97+
Object.getPrototypeOf(sourceObject.nestedObject),
98+
);
99+
expect(Object.getPrototypeOf(copy.nestedArray)).not.toBe(
100+
Object.getPrototypeOf(sourceObject.nestedArray),
101+
);
102+
103+
expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf({}));
104+
expect(Object.getPrototypeOf(copy.nestedObject)).toBe(
105+
Object.getPrototypeOf({}),
106+
);
107+
expect(Object.getPrototypeOf(copy.nestedArray)).toBe(
108+
Object.getPrototypeOf([]),
109+
);
110+
111+
spy.mockRestore();
112+
});
113+
114+
it('does not keep the prototype by default when top level is array', () => {
115+
const spy = jest.spyOn(Array, 'isArray').mockImplementation(() => true);
116+
117+
const sourceArray = new function() {
118+
this.length = 0;
119+
}();
120+
121+
const copy = deepCyclicCopy(sourceArray);
122+
expect(Object.getPrototypeOf(copy)).not.toBe(
123+
Object.getPrototypeOf(sourceArray),
124+
);
125+
126+
expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf([]));
127+
spy.mockRestore();
128+
});
129+
130+
it('does not keep the prototype of arrays when keepPrototype = false', () => {
131+
const spy = jest.spyOn(Array, 'isArray').mockImplementation(() => true);
132+
133+
const sourceArray = new function() {
134+
this.length = 0;
135+
}();
136+
137+
const copy = deepCyclicCopy(sourceArray);
138+
expect(Object.getPrototypeOf(copy)).not.toBe(
139+
Object.getPrototypeOf(sourceArray),
140+
);
141+
142+
expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf([]));
143+
spy.mockRestore();
144+
});
145+
146+
it('keeps the prototype of arrays when keepPrototype = true', () => {
147+
const spy = jest.spyOn(Array, 'isArray').mockImplementation(() => true);
148+
149+
const sourceArray = new function() {
150+
this.length = 0;
151+
}();
152+
153+
const copy = deepCyclicCopy(sourceArray, {keepPrototype: true});
154+
expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf(sourceArray));
155+
156+
spy.mockRestore();
157+
});
158+
159+
it('does not keep the prototype for objects when keepPrototype = false', () => {
160+
const sourceobject = new function() {}();
161+
sourceobject.nestedObject = new function() {}();
162+
sourceobject.nestedArray = new function() {
163+
this.length = 0;
164+
}();
165+
166+
const spy = jest.spyOn(Array, 'isArray').mockImplementation(object => {
167+
return object === sourceobject.nestedArray;
168+
});
169+
170+
const copy = deepCyclicCopy(sourceobject, {keepPrototype: false});
171+
172+
expect(Object.getPrototypeOf(copy)).not.toBe(
173+
Object.getPrototypeOf(sourceobject),
174+
);
175+
expect(Object.getPrototypeOf(copy.nestedObject)).not.toBe(
176+
Object.getPrototypeOf(sourceobject.nestedObject),
177+
);
178+
expect(Object.getPrototypeOf(copy.nestedArray)).not.toBe(
179+
Object.getPrototypeOf(sourceobject.nestedArray),
180+
);
181+
expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf({}));
182+
expect(Object.getPrototypeOf(copy.nestedObject)).toBe(
183+
Object.getPrototypeOf({}),
184+
);
185+
expect(Object.getPrototypeOf(copy.nestedArray)).toBe(
186+
Object.getPrototypeOf([]),
187+
);
188+
189+
spy.mockRestore();
190+
});
191+
192+
it('keeps the prototype for objects when keepPrototype = true', () => {
193+
const sourceObject = new function() {}();
194+
sourceObject.nestedObject = new function() {}();
195+
sourceObject.nestedArray = new function() {
196+
this.length = 0;
197+
}();
198+
199+
const spy = jest.spyOn(Array, 'isArray').mockImplementation(object => {
200+
return object === sourceObject.nestedArray;
201+
});
202+
203+
const copy = deepCyclicCopy(sourceObject, {keepPrototype: true});
204+
205+
expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf(sourceObject));
206+
expect(Object.getPrototypeOf(copy.nestedObject)).toBe(
207+
Object.getPrototypeOf(sourceObject.nestedObject),
208+
);
209+
expect(Object.getPrototypeOf(copy.nestedArray)).toBe(
210+
Object.getPrototypeOf(sourceObject.nestedArray),
211+
);
212+
spy.mockRestore();
213+
});

packages/jest-util/src/create_process_object.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ const BLACKLIST = new Set(['mainModule', '_events']);
1313

1414
export default function() {
1515
const process = require('process');
16-
const newProcess = deepCyclicCopy(process, BLACKLIST);
16+
const newProcess = deepCyclicCopy(process, {
17+
blacklist: BLACKLIST,
18+
keepPrototype: true,
19+
});
1720

1821
newProcess[Symbol.toStringTag] = 'process';
1922

packages/jest-util/src/deep_cyclic_copy.js

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99

1010
const EMPTY = new Set();
1111

12+
export type DeepCyclicCopyOptions = {|
13+
blacklist: Set<string>,
14+
keepPrototype: boolean,
15+
|};
16+
1217
// Node 6 does not have gOPDs, so we define a simple polyfill for it.
1318
if (!Object.getOwnPropertyDescriptors) {
1419
// $FlowFixMe: polyfill
@@ -26,41 +31,45 @@ if (!Object.getOwnPropertyDescriptors) {
2631

2732
export default function deepCyclicCopy(
2833
value: any,
29-
blacklist: Set<string> = EMPTY,
34+
options?: DeepCyclicCopyOptions = {blacklist: EMPTY, keepPrototype: false},
3035
cycles: WeakMap<any, any> = new WeakMap(),
3136
): any {
3237
if (typeof value !== 'object' || value === null) {
3338
return value;
3439
} else if (cycles.has(value)) {
3540
return cycles.get(value);
3641
} else if (Array.isArray(value)) {
37-
return deepCyclicCopyArray(value, blacklist, cycles);
42+
return deepCyclicCopyArray(value, options, cycles);
3843
} else {
39-
return deepCyclicCopyObject(value, blacklist, cycles);
44+
return deepCyclicCopyObject(value, options, cycles);
4045
}
4146
}
4247

4348
function deepCyclicCopyObject(
4449
object: Object,
45-
blacklist: Set<string>,
50+
options: DeepCyclicCopyOptions,
4651
cycles: WeakMap<any, any>,
4752
): Object {
48-
const newObject = Object.create(Object.getPrototypeOf(object));
53+
const newObject = options.keepPrototype
54+
? Object.create(Object.getPrototypeOf(object))
55+
: {};
56+
4957
// $FlowFixMe: Object.getOwnPropertyDescriptors is polyfilled above.
5058
const descriptors = Object.getOwnPropertyDescriptors(object);
5159

5260
cycles.set(object, newObject);
5361

5462
Object.keys(descriptors).forEach(key => {
55-
if (blacklist.has(key)) {
63+
if (options.blacklist && options.blacklist.has(key)) {
5664
delete descriptors[key];
5765
return;
5866
}
5967

6068
const descriptor = descriptors[key];
6169

6270
if (typeof descriptor.value !== 'undefined') {
63-
descriptor.value = deepCyclicCopy(descriptor.value, EMPTY, cycles);
71+
delete options.blacklist;
72+
descriptor.value = deepCyclicCopy(descriptor.value, options, cycles);
6473
}
6574

6675
descriptor.configurable = true;
@@ -71,16 +80,20 @@ function deepCyclicCopyObject(
7180

7281
function deepCyclicCopyArray(
7382
array: Array<any>,
74-
blacklist: Set<string>,
83+
options: DeepCyclicCopyOptions,
7584
cycles: WeakMap<any, any>,
7685
): Array<any> {
77-
const newArray = [];
86+
const newArray = options.keepPrototype
87+
? // $FlowFixMe: getPrototypeOf an array is OK.
88+
new (Object.getPrototypeOf(array)).constructor(array.length)
89+
: [];
7890
const length = array.length;
7991

8092
cycles.set(array, newArray);
8193

8294
for (let i = 0; i < length; i++) {
83-
newArray[i] = deepCyclicCopy(array[i], EMPTY, cycles);
95+
delete options.blacklist;
96+
newArray[i] = deepCyclicCopy(array[i], options, cycles);
8497
}
8598

8699
return newArray;

packages/jest-util/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import NullConsole from './null_console';
2121
import isInteractive from './is_interative';
2222
import setGlobal from './set_global';
2323
import validateCLIOptions from './validate_cli_options';
24+
import deepCyclicCopy from './deep_cyclic_copy';
2425

2526
const createDirectory = (path: string) => {
2627
try {
@@ -39,6 +40,7 @@ module.exports = {
3940
NullConsole,
4041
clearLine,
4142
createDirectory,
43+
deepCyclicCopy,
4244
formatTestResults,
4345
getConsoleOutput,
4446
getFailedSnapshotTests,

0 commit comments

Comments
 (0)