From 58718bbc10df06d84afd714a9e755739fff50b57 Mon Sep 17 00:00:00 2001 From: wtgtybhertgeghgtwtg Date: Thu, 23 Mar 2017 15:36:59 -0700 Subject: [PATCH] [jest-jasmine2] Rewrite `QueueRunner`. (#3187) * [jest-jasmine2] Rewrite `QueueRunner`. * Make `FakeTimer` test stack agnostic. * Update snapshot. Manually. * Fix `FakeTimer`. * Lint. * Do not use async. * Revert "Merge pull request #1 from wtgtybhertgeghgtwtg/wtgtybhertgeghgtwtg-patch-1" This reverts commit 9424111e5c697a677d1d301d7c49cb18ba91743b, reversing changes made to ea0bb8a18417352edc172f054e52dbc3336b0da6. --- packages/jest-jasmine2/package.json | 4 +- .../src/__tests__/p-timeout-test.js | 48 ++++++ .../src/__tests__/queueRunner-test.js | 120 +++++++++++++ packages/jest-jasmine2/src/jasmine/Env.js | 25 +-- .../jest-jasmine2/src/jasmine/QueueRunner.js | 158 ------------------ .../src/jasmine/jasmine-light.js | 2 - packages/jest-jasmine2/src/p-timeout.js | 35 ++++ packages/jest-jasmine2/src/queueRunner.js | 68 ++++++++ .../src/__tests__/FakeTimers-test.js | 2 +- 9 files changed, 280 insertions(+), 182 deletions(-) create mode 100644 packages/jest-jasmine2/src/__tests__/p-timeout-test.js create mode 100644 packages/jest-jasmine2/src/__tests__/queueRunner-test.js delete mode 100644 packages/jest-jasmine2/src/jasmine/QueueRunner.js create mode 100644 packages/jest-jasmine2/src/p-timeout.js create mode 100644 packages/jest-jasmine2/src/queueRunner.js diff --git a/packages/jest-jasmine2/package.json b/packages/jest-jasmine2/package.json index 2de098d3b083..a4220b2f6ea2 100644 --- a/packages/jest-jasmine2/package.json +++ b/packages/jest-jasmine2/package.json @@ -12,6 +12,8 @@ "jest-matcher-utils": "^19.0.0", "jest-matchers": "^19.0.0", "jest-message-util": "^19.0.0", - "jest-snapshot": "^19.0.2" + "jest-snapshot": "^19.0.2", + "once": "^1.4.0", + "p-map": "^1.1.1" } } diff --git a/packages/jest-jasmine2/src/__tests__/p-timeout-test.js b/packages/jest-jasmine2/src/__tests__/p-timeout-test.js new file mode 100644 index 000000000000..336643d6d339 --- /dev/null +++ b/packages/jest-jasmine2/src/__tests__/p-timeout-test.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails oncall+jsinfra + */ +'use strict'; + +jest.useFakeTimers(); + +const pTimeout = require('../p-timeout'); + +describe('pTimeout', () => { + it('calls `clearTimeout` and resolves when `promise` resolves.', async () => { + const onTimeout = jest.fn(); + const promise = Promise.resolve(); + await pTimeout(promise, 1000, clearTimeout, setTimeout, onTimeout); + expect(setTimeout).toHaveBeenCalled(); + expect(clearTimeout).toHaveBeenCalled(); + expect(onTimeout).not.toHaveBeenCalled(); + }); + + it('calls `clearTimeout` and rejects when `promise` rejects.', async () => { + const onTimeout = jest.fn(); + const promise = Promise.reject(); + try { + await pTimeout(promise, 1000, clearTimeout, setTimeout, onTimeout); + } catch (e) { } + expect(setTimeout).toHaveBeenCalled(); + expect(clearTimeout).toHaveBeenCalled(); + expect(onTimeout).not.toHaveBeenCalled(); + }); + + it('calls `onTimeout` on timeout.', async () => { + const onTimeout = jest.fn(); + // A Promise that never resolves or rejects. + const promise = new Promise(() => {}); + const timeoutPromise = + pTimeout(promise, 1000, clearTimeout, setTimeout, onTimeout); + jest.runAllTimers(); + await timeoutPromise; + expect(setTimeout).toHaveBeenCalled(); + expect(onTimeout).toHaveBeenCalled(); + }); +}); diff --git a/packages/jest-jasmine2/src/__tests__/queueRunner-test.js b/packages/jest-jasmine2/src/__tests__/queueRunner-test.js new file mode 100644 index 000000000000..dccbf8e5dc07 --- /dev/null +++ b/packages/jest-jasmine2/src/__tests__/queueRunner-test.js @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails oncall+jsinfra + */ +'use strict'; + +const queueRunner = require('../queueRunner'); + +describe('queueRunner', () => { + it('runs every function in the queue.', async () => { + const fnOne = jest.fn(next => next()); + const fnTwo = jest.fn(next => next()); + const onComplete = jest.fn(); + const options = { + clearTimeout, + fail: () => {}, + onComplete, + onException: () => {}, + queueableFns: [{ + fn: fnOne, + }, { + fn: fnTwo, + }], + setTimeout, + }; + await queueRunner(options); + expect(fnOne).toHaveBeenCalled(); + expect(fnTwo).toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalled(); + }); + + it('exposes `fail` to `next`.', async () => { + const fail = jest.fn(); + const fnOne = jest.fn(next => next.fail()); + const fnTwo = jest.fn(next => next()); + const onComplete = jest.fn(); + const options = { + clearTimeout, + fail, + onComplete, + onException: () => {}, + queueableFns: [{ + fn: fnOne, + }, { + fn: fnTwo, + }], + setTimeout, + }; + await queueRunner(options); + expect(fnOne).toHaveBeenCalled(); + expect(fail).toHaveBeenCalled(); + // Even if `fail` is called, the queue keeps running. + expect(fnTwo).toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalled(); + }); + + it('passes errors to `onException`.', async () => { + const error = new Error('The error a test throws.'); + const fnOne = jest.fn(() => { + throw error; + }); + const fnTwo = jest.fn(next => next()); + const onComplete = jest.fn(); + const onException = jest.fn(); + const options = { + clearTimeout, + fail: () => {}, + onComplete, + onException, + queueableFns: [{ + fn: fnOne, + }, { + fn: fnTwo, + }], + setTimeout, + }; + await queueRunner(options); + expect(fnOne).toHaveBeenCalled(); + expect(onException).toHaveBeenCalledWith(error); + // Even if one of them errors, the queue keeps running. + expect(fnTwo).toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalled(); + }); + + it('passes an error to `onException` on timeout.', async () => { + const fnOne = jest.fn(next => {}); + const fnTwo = jest.fn(next => next()); + const onComplete = jest.fn(); + const onException = jest.fn(); + const options = { + clearTimeout, + fail: () => {}, + onComplete, + onException, + queueableFns: [{ + fn: fnOne, + // It times out in zero seconds. + timeout: () => 0, + }, { + fn: fnTwo, + }], + setTimeout, + }; + await queueRunner(options); + expect(fnOne).toHaveBeenCalled(); + expect(onException).toHaveBeenCalled(); + // i.e. the `message` of the error passed to `onException`. + expect(onException.mock.calls[0][0].message).toEqual( + 'Timeout - Async callback was not invoked within timeout specified ' + + 'by jasmine.DEFAULT_TIMEOUT_INTERVAL.', + ); + expect(fnTwo).toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalled(); + }); +}); diff --git a/packages/jest-jasmine2/src/jasmine/Env.js b/packages/jest-jasmine2/src/jasmine/Env.js index 8b18a4270f94..60a08bd2a1ec 100644 --- a/packages/jest-jasmine2/src/jasmine/Env.js +++ b/packages/jest-jasmine2/src/jasmine/Env.js @@ -32,6 +32,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. /* eslint-disable sort-keys */ 'use strict'; +const queueRunner = require('../queueRunner'); + module.exports = function(j$) { function Env(options) { options = options || {}; @@ -135,19 +137,6 @@ module.exports = function(j$) { return catchExceptions; }; - const maximumSpecCallbackDepth = 20; - let currentSpecCallbackDepth = 0; - - function clearStack(fn) { - currentSpecCallbackDepth++; - if (currentSpecCallbackDepth >= maximumSpecCallbackDepth) { - currentSpecCallbackDepth = 0; - realSetTimeout(fn, 0); - } else { - fn(); - } - } - const catchException = function(e) { return j$.Spec.isPendingSpecException(e) || catchExceptions; }; @@ -177,14 +166,10 @@ module.exports = function(j$) { const queueRunnerFactory = function(options) { options.catchException = catchException; - options.clearStack = options.clearStack || clearStack; - options.timeout = { - setTimeout: realSetTimeout, - clearTimeout: realClearTimeout, - }; + options.clearTimeout = realClearTimeout; options.fail = self.fail; - - new j$.QueueRunner(options).execute(); + options.setTimeout = realSetTimeout; + queueRunner(options); }; const topSuite = new j$.Suite({ diff --git a/packages/jest-jasmine2/src/jasmine/QueueRunner.js b/packages/jest-jasmine2/src/jasmine/QueueRunner.js deleted file mode 100644 index 77f10815be95..000000000000 --- a/packages/jest-jasmine2/src/jasmine/QueueRunner.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - */ -// This file is a heavily modified fork of Jasmine. Original license: -/* -Copyright (c) 2008-2016 Pivotal Labs - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -/* eslint-disable sort-keys */ -'use strict'; - -function once(fn) { - let called = false; - return function() { - if (!called) { - called = true; - fn(); - } - return null; - }; -} - -function QueueRunner(attrs) { - this.queueableFns = attrs.queueableFns || []; - this.onComplete = attrs.onComplete || function() {}; - this.clearStack = attrs.clearStack || - function(fn) { - fn(); - }; - this.onException = attrs.onException || function() {}; - this.catchException = attrs.catchException || - function() { - return true; - }; - this.userContext = attrs.userContext || {}; - this.timeout = attrs.timeout || { - setTimeout, - clearTimeout, - }; - this.fail = attrs.fail || function() {}; -} - -QueueRunner.prototype.execute = function() { - this.run(this.queueableFns, 0); -}; - -QueueRunner.prototype.run = function(queueableFns, recursiveIndex) { - const length = queueableFns.length; - const self = this; - let iterativeIndex; - - for ( - iterativeIndex = recursiveIndex; - iterativeIndex < length; - iterativeIndex++ - ) { - const queueableFn = queueableFns[iterativeIndex]; - if (queueableFn.fn.length > 0) { - attemptAsync(queueableFn); - return; - } else { - attemptSync(queueableFn); - } - } - - const runnerDone = iterativeIndex >= length; - - if (runnerDone) { - this.clearStack(this.onComplete); - } - - function attemptSync(queueableFn) { - try { - queueableFn.fn.call(self.userContext); - } catch (e) { - handleException(e, queueableFn); - } - } - - function attemptAsync(queueableFn) { - const clearTimeout = function() { - Function.prototype.apply.apply(self.timeout.clearTimeout, [ - global, - [timeoutId], - ]); - }; - const next = once(() => { - clearTimeout(timeoutId); - self.run(queueableFns, iterativeIndex + 1); - }); - let timeoutId; - - next.fail = function() { - self.fail.apply(null, arguments); - next(); - }; - - if (queueableFn.timeout) { - timeoutId = Function.prototype.apply.apply(self.timeout.setTimeout, [ - global, - [ - function() { - const error = new Error( - 'Timeout - Async callback was not invoked within ' + - 'timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.', - ); - onException(error); - next(); - }, - queueableFn.timeout(), - ], - ]); - } - - try { - queueableFn.fn.call(self.userContext, next); - } catch (e) { - handleException(e, queueableFn); - next(); - } - } - - function onException(e) { - self.onException(e); - } - - function handleException(e, queueableFn) { - onException(e); - if (!self.catchException(e)) { - throw e; - } - } -}; - -module.exports = QueueRunner; diff --git a/packages/jest-jasmine2/src/jasmine/jasmine-light.js b/packages/jest-jasmine2/src/jasmine/jasmine-light.js index f4459072f88e..a59aa1b7a03f 100644 --- a/packages/jest-jasmine2/src/jasmine/jasmine-light.js +++ b/packages/jest-jasmine2/src/jasmine/jasmine-light.js @@ -35,7 +35,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. const createSpy = require('./createSpy'); const Env = require('./Env'); const JsApiReporter = require('./JsApiReporter'); -const QueueRunner = require('./QueueRunner'); const ReportDispatcher = require('./ReportDispatcher'); const Spec = require('./Spec'); const SpyRegistry = require('./SpyRegistry'); @@ -56,7 +55,6 @@ exports.create = function() { j$.createSpy = createSpy; j$.Env = Env(j$); j$.JsApiReporter = JsApiReporter; - j$.QueueRunner = QueueRunner; j$.ReportDispatcher = ReportDispatcher; j$.Spec = Spec; j$.SpyRegistry = SpyRegistry; diff --git a/packages/jest-jasmine2/src/p-timeout.js b/packages/jest-jasmine2/src/p-timeout.js new file mode 100644 index 000000000000..d3bf51f5549f --- /dev/null +++ b/packages/jest-jasmine2/src/p-timeout.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +// A specialized version of `p-timeout` that does not touch globals. +// It does not throw on timeout. +function pTimeout( + promise: Promise, + ms: number, + clearTimeout: (timeoutID: number) => void, + setTimeout: (func: () => void, delay: number) => number, + onTimeout: () => any, +) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve(onTimeout()), ms); + promise.then( + val => { + clearTimeout(timer); + resolve(val); + }, + err => { + clearTimeout(timer); + reject(err); + }); + }); +} + +module.exports = pTimeout; diff --git a/packages/jest-jasmine2/src/queueRunner.js b/packages/jest-jasmine2/src/queueRunner.js new file mode 100644 index 000000000000..39a9c4fe9d1c --- /dev/null +++ b/packages/jest-jasmine2/src/queueRunner.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +const once = require('once'); +const pMap = require('p-map'); +const pTimeout = require('./p-timeout'); + +type Options = { + clearTimeout: (timeoutID: number) => void, + fail: () => void, + onComplete: () => void, + onException: () => void, + queueableFns: Array, + setTimeout: (func: () => void, delay: number) => number, + userContext: any, +}; + +type QueueableFn = { + fn: (next: () => void) => void, + timeout?: () => number, +}; + +function queueRunner(options: Options) { + const mapper = ({fn, timeout}) => { + const promise = new Promise(resolve => { + const next = once(resolve); + next.fail = (...args) => { + options.fail(...args); + resolve(); + }; + try { + fn.call(options.userContext, next); + } catch (e) { + options.onException(e); + resolve(); + } + }); + if (!timeout) { + return promise; + } + return pTimeout( + promise, + timeout(), + options.clearTimeout, + options.setTimeout, + () => { + const error = new Error( + 'Timeout - Async callback was not invoked within timeout specified ' + + 'by jasmine.DEFAULT_TIMEOUT_INTERVAL.', + ); + options.onException(error); + }, + ); + }; + + return pMap(options.queueableFns, mapper, {concurrency: 1}) + .then(options.onComplete); +} + +module.exports = queueRunner; diff --git a/packages/jest-util/src/__tests__/FakeTimers-test.js b/packages/jest-util/src/__tests__/FakeTimers-test.js index 8421309444ab..afeed18f5cda 100644 --- a/packages/jest-util/src/__tests__/FakeTimers-test.js +++ b/packages/jest-util/src/__tests__/FakeTimers-test.js @@ -327,7 +327,7 @@ describe('FakeTimers', () => { const timers = new FakeTimers(global, moduleMocker, {rootDir: __dirname}); timers.runAllTimers(); expect( - console.warn.mock.calls[0][0].split('\n').slice(0, -3).join('\n'), + console.warn.mock.calls[0][0].split('\nStack Trace')[0], ).toMatchSnapshot(); console.warn = consoleWarn; });