Skip to content

Commit

Permalink
Rewrite how common globals are installed in "global" (#4904)
Browse files Browse the repository at this point in the history
* Rewrite how common globals are installed in "global"

* Refactor code: separate deep copy, process creation, and add unit test

* Fix the fact that Jasmine requires access to the real process object
  • Loading branch information
mjesun authored and cpojer committed Nov 17, 2017
1 parent 84fe06a commit 14957ab
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 51 deletions.
1 change: 1 addition & 0 deletions packages/jest-jasmine2/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ async function jasmine2(
);
const jasmineFactory = runtime.requireInternalModule(JASMINE);
const jasmine = jasmineFactory.create({
process,
testPath,
});

Expand Down
16 changes: 8 additions & 8 deletions packages/jest-jasmine2/src/jasmine/Env.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,11 @@ export default function(j$) {
.listeners('unhandledRejection')
.slice();

process.removeAllListeners('uncaughtException');
process.removeAllListeners('unhandledRejection');
j$.process.removeAllListeners('uncaughtException');
j$.process.removeAllListeners('unhandledRejection');

process.on('uncaughtException', uncaught);
process.on('unhandledRejection', uncaught);
j$.process.on('uncaughtException', uncaught);
j$.process.on('unhandledRejection', uncaught);

reporter.jasmineStarted({totalSpecsDefined});

Expand Down Expand Up @@ -237,16 +237,16 @@ export default function(j$) {
failedExpectations: topSuite.result.failedExpectations,
});

process.removeListener('uncaughtException', uncaught);
process.removeListener('unhandledRejection', uncaught);
j$.process.removeListener('uncaughtException', uncaught);
j$.process.removeListener('unhandledRejection', uncaught);

// restore previous exception handlers
oldListenersException.forEach(listener => {
process.on('uncaughtException', listener);
j$.process.on('uncaughtException', listener);
});

oldListenersRejection.forEach(listener => {
process.on('unhandledRejection', listener);
j$.process.on('unhandledRejection', listener);
});
};

Expand Down
12 changes: 10 additions & 2 deletions packages/jest-runtime/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,7 @@ class Runtime {
}

if (moduleName && this._resolver.isCoreModule(moduleName)) {
// $FlowFixMe
return require(moduleName);
return this._requireCoreModule(moduleName);
}

if (!modulePath) {
Expand Down Expand Up @@ -558,6 +557,15 @@ class Runtime {
this._currentlyExecutingModulePath = lastExecutingModulePath;
}

_requireCoreModule(moduleName: string) {
if (moduleName === 'process') {
return this._environment.global.process;
}

// $FlowFixMe
return require(moduleName);
}

_generateMock(from: Path, moduleName: string) {
const modulePath = this._resolveModule(from, moduleName);

Expand Down
32 changes: 32 additions & 0 deletions packages/jest-util/src/__tests__/create_process_object.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import EventEmitter from 'events';
import createProcessObject from '../create_process_object';

it('creates a process object that looks like the original one', () => {
const fakeProcess = createProcessObject();

// "process" inherits from EventEmitter through the prototype chain.
expect(fakeProcess instanceof EventEmitter).toBe(true);

// They look the same, but they are NOT the same (deep copied object). The
// "_events" property is checked to ensure event emitter properties are
// properly copied.
['argv', 'env', '_events'].forEach(key => {
expect(fakeProcess[key]).toEqual(process[key]);
expect(fakeProcess[key]).not.toBe(process[key]);
});

// Check that process.stdout/stderr are the same.
expect(process.stdout).toBe(fakeProcess.stdout);
expect(process.stderr).toBe(fakeProcess.stderr);
});

it('fakes require("process") so it is equal to "global.process"', () => {
expect(require('process') === process).toBe(true);
});
63 changes: 63 additions & 0 deletions packages/jest-util/src/__tests__/deep_cyclic_copy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import deepCyclicCopy from '../deep_cyclic_copy';

it('returns the same value for primitive or function values', () => {
const fn = () => {};

expect(deepCyclicCopy(undefined)).toBe(undefined);
expect(deepCyclicCopy(null)).toBe(null);
expect(deepCyclicCopy(true)).toBe(true);
expect(deepCyclicCopy(42)).toBe(42);
expect(Number.isNaN(deepCyclicCopy(NaN))).toBe(true);
expect(deepCyclicCopy('foo')).toBe('foo');
expect(deepCyclicCopy(fn)).toBe(fn);
});

it('does not execute getters/setters, but copies them', () => {
const fn = jest.fn();
const obj = {
get foo() {
fn();
},
};
const copy = deepCyclicCopy(obj);

expect(Object.getOwnPropertyDescriptor(copy, 'foo')).toBeDefined();
expect(fn).not.toBeCalled();
});

it('copies symbols', () => {
const symbol = Symbol('foo');
const obj = {[symbol]: 42};

expect(deepCyclicCopy(obj)[symbol]).toBe(42);
});

it('copies arrays as array objects', () => {
const array = [null, 42, 'foo', 'bar', [], {}];

expect(deepCyclicCopy(array)).toEqual(array);
expect(Array.isArray(deepCyclicCopy(array))).toBe(true);
});

it('handles cyclic dependencies', () => {
const cyclic = {a: 42, subcycle: {}};

cyclic.subcycle.baz = cyclic;
cyclic.bar = cyclic;

expect(() => deepCyclicCopy(cyclic)).not.toThrow();

const copy = deepCyclicCopy(cyclic);

expect(copy.a).toBe(42);
expect(copy.bar).toEqual(copy);
expect(copy.subcycle.baz).toEqual(copy);
});
29 changes: 29 additions & 0 deletions packages/jest-util/src/create_process_object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import deepCyclicCopy from './deep_cyclic_copy';

export default function() {
const process = require('process');
const newProcess = deepCyclicCopy(process);

// $FlowFixMe: Add the symbol for toString objects.
newProcess[Symbol.toStringTag] = 'process';

// Sequentially execute all constructors over the object.
let proto = process;

while ((proto = Object.getPrototypeOf(proto))) {
if (typeof proto.constructor === 'function') {
proto.constructor.call(newProcess);
}
}

return newProcess;
}
60 changes: 60 additions & 0 deletions packages/jest-util/src/deep_cyclic_copy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

export default function deepCyclicCopy(
object: any,
cycles: WeakMap<any, any> = new WeakMap(),
) {
if (typeof object !== 'object' || object === null) {
return object;
}

let newObject;

if (Array.isArray(object)) {
newObject = [];
} else {
newObject = Object.create(Object.getPrototypeOf(object));
}

cycles.set(object, newObject);

// Copying helper function. Checks into the weak map passed to manage cycles.
const copy = (key: string | Symbol) => {
const descriptor = Object.getOwnPropertyDescriptor(object, key);
const value = descriptor.value;

if (descriptor.hasOwnProperty('value')) {
if (cycles.has(value)) {
descriptor.value = cycles.get(value);
} else {
descriptor.value = deepCyclicCopy(value, cycles);
}

// Allow tests to override whatever they need.
descriptor.writable = true;
}

// Allow tests to override whatever they need.
descriptor.configurable = true;

try {
Object.defineProperty(newObject, key, descriptor);
} catch (err) {
// Do nothing; this usually fails because a non-configurable property is
// tried to be overridden with a configurable one (e.g. "length").
}
};

// Copy string and symbol keys!
Object.getOwnPropertyNames(object).forEach(copy);
Object.getOwnPropertySymbols(object).forEach(copy);

return newObject;
}
51 changes: 10 additions & 41 deletions packages/jest-util/src/install_common_globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,16 @@
import type {ConfigGlobals} from 'types/Config';
import type {Global} from 'types/Global';

function deepCopy(obj) {
const newObj = {};
let value;
for (const key in obj) {
value = obj[key];
if (typeof value === 'object' && value !== null) {
value = deepCopy(value);
}
newObj[key] = value;
}
return newObj;
}

export default (global: Global, globals: ConfigGlobals) => {
// Forward some APIs
global.Buffer = Buffer;
import createProcesObject from './create_process_object';
import deepCyclicCopy from './deep_cyclic_copy';

// `global.process` is mutated by FakeTimers. Make a copy of the
// object for the jsdom environment to prevent memory leaks.
// Overwrite toString to make it look like the real process object
let toStringOverwrite;
if (Symbol && Symbol.toStringTag) {
// $FlowFixMe
toStringOverwrite = {
[Symbol.toStringTag]: 'process',
};
}
global.process = Object.assign({}, process, toStringOverwrite);
global.process.setMaxListeners = process.setMaxListeners.bind(process);
global.process.getMaxListeners = process.getMaxListeners.bind(process);
global.process.emit = process.emit.bind(process);
global.process.addListener = process.addListener.bind(process);
global.process.on = process.on.bind(process);
global.process.once = process.once.bind(process);
global.process.removeListener = process.removeListener.bind(process);
global.process.removeAllListeners = process.removeAllListeners.bind(process);
global.process.listeners = process.listeners.bind(process);
global.process.listenerCount = process.listenerCount.bind(process);
export default function(globalObject: Global, globals: ConfigGlobals) {
globalObject.process = createProcesObject();

global.setImmediate = setImmediate;
global.clearImmediate = clearImmediate;
// Forward some APIs.
globalObject.Buffer = global.Buffer;
globalObject.setImmediate = global.setImmediate;
globalObject.clearImmediate = global.clearImmediate;

Object.assign(global, deepCopy(globals));
};
Object.assign(global, deepCyclicCopy(globals));
}

0 comments on commit 14957ab

Please sign in to comment.