Skip to content

Commit

Permalink
Fix a leak in coverage (#5289)
Browse files Browse the repository at this point in the history
  • Loading branch information
rickhanlonii authored and mjesun committed Jan 13, 2018
1 parent c799a02 commit 6d2394f
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 16 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## master

## jest 22.0.7

### Fixes

* `[jest-runner]` Fix memory leak in coverage reporting ([#5289](https://github.com/facebook/jest/pull/5289))

### Features

* `[jest-cli]` Make Jest exit without an error when no tests are found in
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-runner/src/run_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ async function runTestInternal(

result.perfStats = {end: Date.now(), start};
result.testFilePath = path;
result.coverage = runtime.getAllCoverageInfo();
result.coverage = runtime.getAllCoverageInfoCopy();
result.sourceMaps = runtime.getSourceMapInfo();
result.console = testConsole.getBuffer();
result.skipped = testCount === result.numPendingTests;
Expand Down
6 changes: 3 additions & 3 deletions packages/jest-runtime/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {MockFunctionMetadata, ModuleMocker} from 'types/Mock';
import path from 'path';
import HasteMap from 'jest-haste-map';
import Resolver from 'jest-resolve';
import {createDirectory} from 'jest-util';
import {createDirectory, deepCyclicCopy} from 'jest-util';
import {escapePathForRegex} from 'jest-regex-util';
import fs from 'graceful-fs';
import stripBOM from 'strip-bom';
Expand Down Expand Up @@ -433,8 +433,8 @@ class Runtime {
}
}

getAllCoverageInfo() {
return this._environment.global.__coverage__;
getAllCoverageInfoCopy() {
return deepCyclicCopy(this._environment.global.__coverage__);
}

getSourceMapInfo() {
Expand Down
137 changes: 136 additions & 1 deletion packages/jest-util/src/__tests__/deep_cyclic_copy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,144 @@ it('uses the blacklist to avoid copying properties on the first level', () => {
},
};

expect(deepCyclicCopy(obj, new Set(['blacklisted']))).toEqual({
expect(deepCyclicCopy(obj, {blacklist: new Set(['blacklisted'])})).toEqual({
subObj: {
blacklisted: 42,
},
});
});

it('does not keep the prototype by default when top level is object', () => {
const sourceObject = new function() {}();
sourceObject.nestedObject = new function() {}();
sourceObject.nestedArray = new function() {
this.length = 0;
}();

const spy = jest.spyOn(Array, 'isArray').mockImplementation(object => {
return object === sourceObject.nestedArray;
});

const copy = deepCyclicCopy(sourceObject, {keepPrototype: false});

expect(Object.getPrototypeOf(copy)).not.toBe(
Object.getPrototypeOf(sourceObject),
);
expect(Object.getPrototypeOf(copy.nestedObject)).not.toBe(
Object.getPrototypeOf(sourceObject.nestedObject),
);
expect(Object.getPrototypeOf(copy.nestedArray)).not.toBe(
Object.getPrototypeOf(sourceObject.nestedArray),
);

expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf({}));
expect(Object.getPrototypeOf(copy.nestedObject)).toBe(
Object.getPrototypeOf({}),
);
expect(Object.getPrototypeOf(copy.nestedArray)).toBe(
Object.getPrototypeOf([]),
);

spy.mockRestore();
});

it('does not keep the prototype by default when top level is array', () => {
const spy = jest.spyOn(Array, 'isArray').mockImplementation(() => true);

const sourceArray = new function() {
this.length = 0;
}();

const copy = deepCyclicCopy(sourceArray);
expect(Object.getPrototypeOf(copy)).not.toBe(
Object.getPrototypeOf(sourceArray),
);

expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf([]));
spy.mockRestore();
});

it('does not keep the prototype of arrays when keepPrototype = false', () => {
const spy = jest.spyOn(Array, 'isArray').mockImplementation(() => true);

const sourceArray = new function() {
this.length = 0;
}();

const copy = deepCyclicCopy(sourceArray);
expect(Object.getPrototypeOf(copy)).not.toBe(
Object.getPrototypeOf(sourceArray),
);

expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf([]));
spy.mockRestore();
});

it('keeps the prototype of arrays when keepPrototype = true', () => {
const spy = jest.spyOn(Array, 'isArray').mockImplementation(() => true);

const sourceArray = new function() {
this.length = 0;
}();

const copy = deepCyclicCopy(sourceArray, {keepPrototype: true});
expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf(sourceArray));

spy.mockRestore();
});

it('does not keep the prototype for objects when keepPrototype = false', () => {
const sourceobject = new function() {}();
sourceobject.nestedObject = new function() {}();
sourceobject.nestedArray = new function() {
this.length = 0;
}();

const spy = jest.spyOn(Array, 'isArray').mockImplementation(object => {
return object === sourceobject.nestedArray;
});

const copy = deepCyclicCopy(sourceobject, {keepPrototype: false});

expect(Object.getPrototypeOf(copy)).not.toBe(
Object.getPrototypeOf(sourceobject),
);
expect(Object.getPrototypeOf(copy.nestedObject)).not.toBe(
Object.getPrototypeOf(sourceobject.nestedObject),
);
expect(Object.getPrototypeOf(copy.nestedArray)).not.toBe(
Object.getPrototypeOf(sourceobject.nestedArray),
);
expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf({}));
expect(Object.getPrototypeOf(copy.nestedObject)).toBe(
Object.getPrototypeOf({}),
);
expect(Object.getPrototypeOf(copy.nestedArray)).toBe(
Object.getPrototypeOf([]),
);

spy.mockRestore();
});

it('keeps the prototype for objects when keepPrototype = true', () => {
const sourceObject = new function() {}();
sourceObject.nestedObject = new function() {}();
sourceObject.nestedArray = new function() {
this.length = 0;
}();

const spy = jest.spyOn(Array, 'isArray').mockImplementation(object => {
return object === sourceObject.nestedArray;
});

const copy = deepCyclicCopy(sourceObject, {keepPrototype: true});

expect(Object.getPrototypeOf(copy)).toBe(Object.getPrototypeOf(sourceObject));
expect(Object.getPrototypeOf(copy.nestedObject)).toBe(
Object.getPrototypeOf(sourceObject.nestedObject),
);
expect(Object.getPrototypeOf(copy.nestedArray)).toBe(
Object.getPrototypeOf(sourceObject.nestedArray),
);
spy.mockRestore();
});
5 changes: 4 additions & 1 deletion packages/jest-util/src/create_process_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ const BLACKLIST = new Set(['mainModule', '_events']);

export default function() {
const process = require('process');
const newProcess = deepCyclicCopy(process, BLACKLIST);
const newProcess = deepCyclicCopy(process, {
blacklist: BLACKLIST,
keepPrototype: true,
});

newProcess[Symbol.toStringTag] = 'process';

Expand Down
33 changes: 23 additions & 10 deletions packages/jest-util/src/deep_cyclic_copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

const EMPTY = new Set();

export type DeepCyclicCopyOptions = {|
blacklist: Set<string>,
keepPrototype: boolean,
|};

// Node 6 does not have gOPDs, so we define a simple polyfill for it.
if (!Object.getOwnPropertyDescriptors) {
// $FlowFixMe: polyfill
Expand All @@ -26,41 +31,45 @@ if (!Object.getOwnPropertyDescriptors) {

export default function deepCyclicCopy(
value: any,
blacklist: Set<string> = EMPTY,
options?: DeepCyclicCopyOptions = {blacklist: EMPTY, keepPrototype: false},
cycles: WeakMap<any, any> = new WeakMap(),
): any {
if (typeof value !== 'object' || value === null) {
return value;
} else if (cycles.has(value)) {
return cycles.get(value);
} else if (Array.isArray(value)) {
return deepCyclicCopyArray(value, blacklist, cycles);
return deepCyclicCopyArray(value, options, cycles);
} else {
return deepCyclicCopyObject(value, blacklist, cycles);
return deepCyclicCopyObject(value, options, cycles);
}
}

function deepCyclicCopyObject(
object: Object,
blacklist: Set<string>,
options: DeepCyclicCopyOptions,
cycles: WeakMap<any, any>,
): Object {
const newObject = Object.create(Object.getPrototypeOf(object));
const newObject = options.keepPrototype
? Object.create(Object.getPrototypeOf(object))
: {};

// $FlowFixMe: Object.getOwnPropertyDescriptors is polyfilled above.
const descriptors = Object.getOwnPropertyDescriptors(object);

cycles.set(object, newObject);

Object.keys(descriptors).forEach(key => {
if (blacklist.has(key)) {
if (options.blacklist && options.blacklist.has(key)) {
delete descriptors[key];
return;
}

const descriptor = descriptors[key];

if (typeof descriptor.value !== 'undefined') {
descriptor.value = deepCyclicCopy(descriptor.value, EMPTY, cycles);
delete options.blacklist;
descriptor.value = deepCyclicCopy(descriptor.value, options, cycles);
}

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

function deepCyclicCopyArray(
array: Array<any>,
blacklist: Set<string>,
options: DeepCyclicCopyOptions,
cycles: WeakMap<any, any>,
): Array<any> {
const newArray = [];
const newArray = options.keepPrototype
? // $FlowFixMe: getPrototypeOf an array is OK.
new (Object.getPrototypeOf(array)).constructor(array.length)
: [];
const length = array.length;

cycles.set(array, newArray);

for (let i = 0; i < length; i++) {
newArray[i] = deepCyclicCopy(array[i], EMPTY, cycles);
delete options.blacklist;
newArray[i] = deepCyclicCopy(array[i], options, cycles);
}

return newArray;
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-util/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import NullConsole from './null_console';
import isInteractive from './is_interative';
import setGlobal from './set_global';
import validateCLIOptions from './validate_cli_options';
import deepCyclicCopy from './deep_cyclic_copy';

const createDirectory = (path: string) => {
try {
Expand All @@ -39,6 +40,7 @@ module.exports = {
NullConsole,
clearLine,
createDirectory,
deepCyclicCopy,
formatTestResults,
getConsoleOutput,
getFailedSnapshotTests,
Expand Down

0 comments on commit 6d2394f

Please sign in to comment.