diff --git a/integration_tests/__tests__/leak_detection.test.js b/integration_tests/__tests__/leak_detection.test.js new file mode 100644 index 000000000000..d6838809f6f8 --- /dev/null +++ b/integration_tests/__tests__/leak_detection.test.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2014-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 + */ +'use strict'; + +const runJest = require('../runJest'); + +it('Makes sure that Jest does not leak the environment', () => { + const result = runJest.json('leak-detection', ['--detectLeaks']).json; + + expect(result.success).toBe(true); +}); diff --git a/integration_tests/__tests__/require_all_modules.test.js b/integration_tests/__tests__/require_all_modules.test.js new file mode 100644 index 000000000000..5012efbb064a --- /dev/null +++ b/integration_tests/__tests__/require_all_modules.test.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2014-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 + */ +'use strict'; + +const runJest = require('../runJest'); + +it('Makes sure that no native module makes Jest crash', () => { + const result = runJest.json('require-all-modules').json; + + if (!result.success) { + console.warn(result); + } + + expect(result.success).toBe(true); +}); diff --git a/integration_tests/leak-detection/package.json b/integration_tests/leak-detection/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/integration_tests/leak-detection/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/integration_tests/leak-detection/test.js b/integration_tests/leak-detection/test.js new file mode 100644 index 000000000000..c36d6aa9e22c --- /dev/null +++ b/integration_tests/leak-detection/test.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2014-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. + */ + +const fs = require('fs'); +const http = require('http'); + +it('expands a native module', () => { + fs.expandingNativeObject = () => { + console.log(global); + }; +}); + +it('expands the prototype of a native constructor', () => { + http.ServerResponse.prototype.expandingNativePrototype = () => { + console.log(global); + }; +}); + +it('adds listeners to process', () => { + process.on('foo', () => { + console.log(global); + }); +}); diff --git a/integration_tests/require-all-modules/package.json b/integration_tests/require-all-modules/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/integration_tests/require-all-modules/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/integration_tests/require-all-modules/test.js b/integration_tests/require-all-modules/test.js new file mode 100644 index 000000000000..084fb77ca1da --- /dev/null +++ b/integration_tests/require-all-modules/test.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2014-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. + */ + +it('requires all native modules to check they all work', () => { + const modules = Object.keys(process.binding('natives')).filter(module => + /^[^_][^\/]*$/.test(module) + ); + + // Node 6 has 34 native modules; so the total value has to be >= than 34. + expect(modules.length).not.toBeLessThan(34); + + // Require all modules to verify they don't throw. + modules.forEach(module => require(module)); +}); diff --git a/jest-inspect b/jest-inspect new file mode 100755 index 000000000000..94f7c764a274 --- /dev/null +++ b/jest-inspect @@ -0,0 +1,9 @@ +#!/usr/bin/env node --inspect-brk +/** + * Copyright (c) 2014-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. + */ + +require('./packages/jest-cli/bin/jest'); diff --git a/packages/jest-editor-support/src/__tests__/runner.test.js b/packages/jest-editor-support/src/__tests__/runner.test.js index 5bcfd6623d64..12e55c4a2f64 100644 --- a/packages/jest-editor-support/src/__tests__/runner.test.js +++ b/packages/jest-editor-support/src/__tests__/runner.test.js @@ -15,6 +15,10 @@ const {readFileSync} = require('fs'); const fixtures = path.resolve(__dirname, '../../../../fixtures'); import ProjectWorkspace from '../project_workspace'; +// Win32 requires to spawn a process to kill the first one, by using "taskkill". +// Mocking "child_process" avoids the async spawn. +jest.mock('child_process'); + // Replace `readFile` with `readFileSync` so we don't get multiple threads jest.doMock('fs', () => { return { diff --git a/packages/jest-jasmine2/src/index.js b/packages/jest-jasmine2/src/index.js index 724d0e03e921..e55d247aba76 100644 --- a/packages/jest-jasmine2/src/index.js +++ b/packages/jest-jasmine2/src/index.js @@ -25,10 +25,17 @@ const JASMINE = require.resolve('./jasmine/jasmine_light.js'); async function jasmine2( globalConfig: GlobalConfig, config: ProjectConfig, - environment: Environment, + environment: ?Environment, runtime: Runtime, testPath: string, ): Promise { + // The "environment" parameter is nullable just so that we can clean its + // reference after adding some variables to it; but you still need to pass + // it when calling "jasmine2". + if (!environment) { + throw new ReferenceError('Please pass a valid Jest Environment object'); + } + const reporter = new JasmineReporter( globalConfig, config, @@ -85,12 +92,17 @@ async function jasmine2( if (config.resetMocks) { runtime.resetAllMocks(); - if (config.timers === 'fake') { + if (environment && config.timers === 'fake') { environment.fakeTimers.useFakeTimers(); } } }); + // Free references to environment to avoid leaks. + env.afterAll(() => { + environment = null; + }); + env.addReporter(reporter); runtime @@ -114,7 +126,9 @@ async function jasmine2( if (config.setupTestFramework && config.setupTestFramework.length) { config.setupTestFramework.forEach(module => { - require(module)(environment.global); + if (environment) { + require(module)(environment.global); + } }); } diff --git a/packages/jest-message-util/src/index.js b/packages/jest-message-util/src/index.js index 3ed544fdffbb..296040b8795c 100644 --- a/packages/jest-message-util/src/index.js +++ b/packages/jest-message-util/src/index.js @@ -40,6 +40,8 @@ type StackTraceOptions = { // filter for noisy stack trace lines const JASMINE_IGNORE = /^\s+at(?:(?:.*?vendor\/|jasmine\-)|\s+jasmine\.buildExpectationResult)/; const JEST_INTERNALS_IGNORE = /^\s+at.*?jest(-.*?)?(\/|\\)(build|node_modules|packages)(\/|\\)/; + +const JEST_NODE_NATIVE_IGNORE = /^\s+at.*?jest-node-native-/; const ANONYMOUS_FN_IGNORE = /^\s+at .*$/; const ANONYMOUS_PROMISE_IGNORE = /^\s+at (new )?Promise \(\).*$/; const ANONYMOUS_GENERATOR_IGNORE = /^\s+at Generator.next \(\).*$/; @@ -138,6 +140,10 @@ const removeInternalStackEntries = (lines, options: StackTraceOptions) => { return false; } + if (JEST_NODE_NATIVE_IGNORE.test(line)) { + return false; + } + if (!STACK_PATH_REGEXP.test(line)) { return true; } diff --git a/packages/jest-runner/src/run_test.js b/packages/jest-runner/src/run_test.js index 9c54916c522a..94642ad4edf8 100644 --- a/packages/jest-runner/src/run_test.js +++ b/packages/jest-runner/src/run_test.js @@ -74,7 +74,8 @@ async function runTestInternal( RuntimeClass, >); - const environment = new TestEnvironment(config); + let environment = new TestEnvironment(config); + const leakDetector = config.detectLeaks ? new LeakDetector(environment) : null; @@ -98,15 +99,24 @@ async function runTestInternal( testConsole = new BufferedConsole(); } - const cacheFS = {[path]: testSource}; + let cacheFS = {[path]: testSource}; setGlobal(environment.global, 'console', testConsole); - const runtime = new Runtime(config, environment, resolver, cacheFS, { + const coverageOptions = { collectCoverage: globalConfig.collectCoverage, collectCoverageFrom: globalConfig.collectCoverageFrom, collectCoverageOnlyFrom: globalConfig.collectCoverageOnlyFrom, mapCoverage: globalConfig.mapCoverage, - }); + }; + + let runtime = new Runtime( + config, + environment, + resolver, + cacheFS, + coverageOptions, + path, + ); const start = Date.now(); await environment.setup(); @@ -129,19 +139,23 @@ async function runTestInternal( result.skipped = testCount === result.numPendingTests; result.displayName = config.displayName; - if (globalConfig.logHeapUsage) { - if (global.gc) { - global.gc(); - } - result.memoryUsage = process.memoryUsage().heapUsed; - } - // Delay the resolution to allow log messages to be output. return new Promise(resolve => { setImmediate(() => resolve({leakDetector, result})); }); } finally { - await environment.teardown(); + if (environment.teardown) { + await environment.teardown(); + } + + if (runtime.reset) { + await runtime.reset(); + } + + // Free references to environment to avoid leaks. + cacheFS = null; + environment = null; + runtime = null; } } @@ -158,6 +172,11 @@ export default async function runTest( resolver, ); + if (globalConfig.logHeapUsage) { + global.gc && global.gc(); + result.memoryUsage = process.memoryUsage().heapUsed; + } + // Resolve leak detector, outside the "runTestInternal" closure. result.leaks = leakDetector ? leakDetector.isLeaking() : false; diff --git a/packages/jest-runtime/src/index.js b/packages/jest-runtime/src/index.js index 9a322f6fc32d..4ece57b268ca 100644 --- a/packages/jest-runtime/src/index.js +++ b/packages/jest-runtime/src/index.js @@ -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'; @@ -49,6 +49,7 @@ type HasteMapOptions = {| type InternalModuleOptions = {| isInternalModule: boolean, + isNativeModule: boolean, |}; type CoverageOptions = { @@ -96,6 +97,8 @@ class Runtime { _mockRegistry: {[key: string]: any, __proto__: null}; _moduleMocker: ModuleMocker; _moduleRegistry: ModuleRegistry; + _nativeModuleRegistry: ModuleRegistry; + _path: Path; _resolver: Resolver; _shouldAutoMock: boolean; _shouldMockModuleCache: BooleanObject; @@ -112,7 +115,10 @@ class Runtime { resolver: Resolver, cacheFS?: CacheFS, coverageOptions?: CoverageOptions, + path?: Path, ) { + this.reset(); + this._cacheFS = cacheFS || Object.create(null); this._config = config; this._coverageOptions = coverageOptions || { @@ -121,22 +127,18 @@ class Runtime { collectCoverageOnlyFrom: null, mapCoverage: false, }; + this._currentlyExecutingModulePath = ''; this._environment = environment; this._explicitShouldMock = Object.create(null); - this._internalModuleRegistry = Object.create(null); this._isCurrentlyExecutingManualMock = null; - this._mockFactories = Object.create(null); - this._mockRegistry = Object.create(null); this._moduleMocker = this._environment.moduleMocker; - this._moduleRegistry = Object.create(null); + this._path = path || ''; this._resolver = resolver; this._scriptTransformer = new ScriptTransformer(config); this._shouldAutoMock = config.automock; - this._sourceMapRegistry = Object.create(null); this._virtualMocks = Object.create(null); - this._mockMetaDataCache = Object.create(null); this._shouldMockModuleCache = Object.create(null); this._shouldUnmockTransitiveDependenciesCache = Object.create(null); this._transitiveShouldMock = Object.create(null); @@ -267,40 +269,69 @@ class Runtime { return cliOptions; } + reset() { + // Clean mock data. + this._mockFactories = Object.create(null); + this._mockMetaDataCache = Object.create(null); + this._mockRegistry = Object.create(null); + + // Clean registry data. + this._internalModuleRegistry = Object.create(null); + this._moduleRegistry = Object.create(null); + this._nativeModuleRegistry = Object.create(null); + + // Clean other registries. + this._cacheFS = Object.create(null); + this._sourceMapRegistry = Object.create(null); + + // $FlowFixMe: de-reference environment. + this._environment = null; + } + requireModule( from: Path, moduleName?: string, options: ?InternalModuleOptions, ) { - const moduleID = this._resolver.getModuleID( - this._virtualMocks, - from, - moduleName, - ); + const isNativeModule = + moduleName && + ((options && options.isNativeModule) || + this._resolver.isCoreModule(moduleName)); + + const moduleID = + !isNativeModule && + this._resolver.getModuleID(this._virtualMocks, from, moduleName); + + let moduleRegistry; let modulePath; - const moduleRegistry = - !options || !options.isInternalModule - ? this._moduleRegistry - : this._internalModuleRegistry; + if (isNativeModule) { + moduleRegistry = this._nativeModuleRegistry; + } else if (options && options.isInternalModule) { + moduleRegistry = this._internalModuleRegistry; + } else { + moduleRegistry = this._moduleRegistry; + } // Some old tests rely on this mocking behavior. Ideally we'll change this // to be more explicit. const moduleResource = moduleName && this._resolver.getModule(moduleName); const manualMock = moduleName && this._resolver.getMockModule(from, moduleName); + if ( (!options || !options.isInternalModule) && !moduleResource && manualMock && manualMock !== this._isCurrentlyExecutingManualMock && + moduleID && this._explicitShouldMock[moduleID] !== false ) { modulePath = manualMock; } - if (moduleName && this._resolver.isCoreModule(moduleName)) { - return this._requireCoreModule(moduleName); + if (isNativeModule) { + modulePath = moduleName; } if (!modulePath) { @@ -318,7 +349,9 @@ class Runtime { id: modulePath, loaded: false, }; + moduleRegistry[modulePath] = localModule; + if (path.extname(modulePath) === '.json') { localModule.exports = this._environment.global.JSON.parse( stripBOM(fs.readFileSync(modulePath, 'utf8')), @@ -326,17 +359,38 @@ class Runtime { } else if (path.extname(modulePath) === '.node') { // $FlowFixMe localModule.exports = require(modulePath); + } else if (moduleName && isNativeModule) { + // Use a special resolution when requiring Node's internal modules. For + // instance, the "util" module requires "internal/util" which refers to + // an internal file and not to the NPM "internal" module. + this._requireNativeModule( + localModule, + moduleName, + moduleRegistry, + from, + ); } else { this._execModule(localModule, options, moduleRegistry, from); } localModule.loaded = true; } + return moduleRegistry[modulePath].exports; } requireInternalModule(from: Path, to?: string) { - return this.requireModule(from, to, {isInternalModule: true}); + return this.requireModule(from, to, { + isInternalModule: true, + isNativeModule: false, + }); + } + + requireNativeModule(from: Path, to?: string) { + return this.requireModule(from, to, { + isInternalModule: true, + isNativeModule: true, + }); } requireMock(from: Path, moduleName: string) { @@ -487,19 +541,30 @@ class Runtime { moduleRegistry: ModuleRegistry, from: Path, ) { + if (!this._environment) { + throw new Error( + `A module was required after the test suite ${this._path} finished.\n` + + `In most cases this is because an async operation was not cleaned ` + + `up or mocked properly.`, + ); + } + // If the environment was disposed, prevent this module from being executed. if (!this._environment.global) { return; } const isInternalModule = !!(options && options.isInternalModule); + const isNativeModule = !!(options && options.isNativeModule); const filename = localModule.filename; + const lastExecutingModulePath = this._currentlyExecutingModulePath; this._currentlyExecutingModulePath = filename; + const origCurrExecutingManualMock = this._isCurrentlyExecutingManualMock; this._isCurrentlyExecutingManualMock = filename; - const dirname = path.dirname(filename); + const dirname = isNativeModule ? '' : path.dirname(filename); localModule.children = []; Object.defineProperty( @@ -515,10 +580,16 @@ class Runtime { ); localModule.paths = this._resolver.getModulePaths(dirname); + Object.defineProperty(localModule, 'require', { value: this._createRequireImplementation(filename, options), }); + const fileSource = isNativeModule + ? // $FlowFixMe: process.binding exists. + process.binding('natives')[filename] + : this._cacheFS[filename]; + const transformedFile = this._scriptTransformer.transform( filename, { @@ -526,9 +597,10 @@ class Runtime { collectCoverageFrom: this._coverageOptions.collectCoverageFrom, collectCoverageOnlyFrom: this._coverageOptions.collectCoverageOnlyFrom, isInternalModule, + isNativeModule, mapCoverage: this._coverageOptions.mapCoverage, }, - this._cacheFS[filename], + fileSource, ); if (transformedFile.sourceMapPath) { @@ -538,6 +610,7 @@ class Runtime { const wrapper = this._environment.runScript(transformedFile.script)[ ScriptTransformer.EVAL_RESULT_VARIABLE ]; + wrapper.call( localModule.exports, // module context localModule, // module object @@ -557,13 +630,41 @@ class Runtime { this._currentlyExecutingModulePath = lastExecutingModulePath; } - _requireCoreModule(moduleName: string) { - if (moduleName === 'process') { - return this._environment.global.process; + _requireNativeModule( + localModule: Module, + moduleName: string, + moduleRegistry: ModuleRegistry, + from: Path, + ) { + switch (moduleName) { + case 'async_hooks': // Pure native module. + case 'buffer': // Causes issues when passing buffers to another context. + case 'module': // Calls into native_module, which is not mockable. + // $FlowFixMe: dynamic require needed. + localModule.exports = require(moduleName); + break; + + case 'os': // Pure native module. + case 'v8': // Contains invalid references. + // $FlowFixMe: dynamic require needed. + localModule.exports = deepCyclicCopy(require(moduleName)); + break; + + case 'process': // Make sure that the returned reference is consistent. + localModule.exports = this._environment.global.process; + break; + + default: + this._execModule( + localModule, + { + isInternalModule: true, + isNativeModule: true, + }, + moduleRegistry, + from, + ); } - - // $FlowFixMe - return require(moduleName); } _generateMock(from: Path, moduleName: string) { @@ -676,15 +777,22 @@ class Runtime { from: Path, options: ?InternalModuleOptions, ): LocalModuleRequire { - const moduleRequire = - options && options.isInternalModule - ? (moduleName: string) => this.requireInternalModule(from, moduleName) - : this.requireModuleOrMock.bind(this, from); + let moduleRequire; + + if (options && options.isNativeModule) { + moduleRequire = this.requireNativeModule.bind(this, from); + } else if (options && options.isInternalModule) { + moduleRequire = this.requireInternalModule.bind(this, from); + } else { + moduleRequire = this.requireModuleOrMock.bind(this, from); + } + moduleRequire.cache = Object.create(null); moduleRequire.extensions = Object.create(null); moduleRequire.requireActual = this.requireModule.bind(this, from); moduleRequire.requireMock = this.requireMock.bind(this, from); moduleRequire.resolve = moduleName => this._resolveModule(from, moduleName); + return moduleRequire; } diff --git a/packages/jest-runtime/src/script_transformer.js b/packages/jest-runtime/src/script_transformer.js index 482cc2b0d1e3..0c97ea28f935 100644 --- a/packages/jest-runtime/src/script_transformer.js +++ b/packages/jest-runtime/src/script_transformer.js @@ -33,7 +33,7 @@ export type Options = {| collectCoverage: boolean, collectCoverageFrom: Array, collectCoverageOnlyFrom: ?{[key: string]: boolean, __proto__: null}, - isCoreModule?: boolean, + isNativeModule?: boolean, isInternalModule?: boolean, mapCoverage: boolean, |}; @@ -276,7 +276,7 @@ export default class ScriptTransformer { fileSource?: string, ): TransformResult { const isInternalModule = !!(options && options.isInternalModule); - const isCoreModule = !!(options && options.isCoreModule); + const isNativeModule = !!(options && options.isNativeModule); const content = stripShebang( fileSource || fs.readFileSync(filename, 'utf8'), ); @@ -286,7 +286,7 @@ export default class ScriptTransformer { const willTransform = !isInternalModule && - !isCoreModule && + !isNativeModule && (shouldTransform(filename, this._config) || instrument); try { @@ -307,7 +307,7 @@ export default class ScriptTransformer { return { script: new vm.Script(wrappedCode, { displayErrors: true, - filename: isCoreModule ? 'jest-nodejs-core-' + filename : filename, + filename: isNativeModule ? 'jest-node-native-' + filename : filename, }), sourceMapPath, }; @@ -329,7 +329,7 @@ export default class ScriptTransformer { let instrument = false; let result = ''; - if (!options.isCoreModule) { + if (!options.isNativeModule) { instrument = shouldInstrument(filename, options, this._config); scriptCacheKey = getScriptCacheKey(filename, this._config, instrument); result = cache.get(scriptCacheKey); diff --git a/packages/jest-util/src/__tests__/create_process_object.test.js b/packages/jest-util/src/__tests__/create_process_object.test.js index 1417c5fed9d4..798260a01413 100644 --- a/packages/jest-util/src/__tests__/create_process_object.test.js +++ b/packages/jest-util/src/__tests__/create_process_object.test.js @@ -5,14 +5,14 @@ * 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); + // "process" should expose EventEmitter methods through the prototype chain. + expect(typeof fakeProcess.on).toBe('function'); + expect(typeof fakeProcess.removeListener).toBe('function'); // They look the same, but they are NOT the same (deep copied object). The // "_events" property is checked to ensure event emitter properties are diff --git a/packages/jest-util/src/index.js b/packages/jest-util/src/index.js index 859398dd5bc4..12c8a3a51590 100644 --- a/packages/jest-util/src/index.js +++ b/packages/jest-util/src/index.js @@ -12,6 +12,7 @@ import mkdirp from 'mkdirp'; import BufferedConsole from './buffered_console'; import clearLine from './clear_line'; import Console from './Console'; +import deepCyclicCopy from './deep_cyclic_copy'; import FakeTimers from './fake_timers'; import formatTestResults from './format_test_results'; import getConsoleOutput from './get_console_output'; @@ -37,6 +38,7 @@ module.exports = { NullConsole, clearLine, createDirectory, + deepCyclicCopy, formatTestResults, getConsoleOutput, installCommonGlobals, diff --git a/packages/jest-util/src/install_common_globals.js b/packages/jest-util/src/install_common_globals.js index feba4a9f9bf9..93ab37445fca 100644 --- a/packages/jest-util/src/install_common_globals.js +++ b/packages/jest-util/src/install_common_globals.js @@ -13,15 +13,18 @@ import type {Global} from 'types/Global'; import createProcesObject from './create_process_object'; import deepCyclicCopy from './deep_cyclic_copy'; -const DTRACE = Object.keys(global).filter(key => key.startsWith('DTRACE')); +// Matches macros referenced in Node repository, under the "src" folder. +const MACROS = Object.keys(global).filter(key => { + return /^(?:DTRACE|LTTNG|COUNTER)_/.test(key); +}); export default function(globalObject: Global, globals: ConfigGlobals) { globalObject.process = createProcesObject(); // Forward some APIs. - DTRACE.forEach(dtrace => { - globalObject[dtrace] = function(...args) { - return global[dtrace].apply(this, args); + MACROS.forEach(macro => { + globalObject[macro] = function(...args) { + return global[macro].apply(this, args); }; });