diff options
author | jrw <jrw@chromium.org> | 2015-03-13 17:16:38 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-03-14 00:17:48 +0000 |
commit | 7829d55746af0b4d0889d6b65f95cff89b4ef79a (patch) | |
tree | 3fb36e39a16940f173920fc435f4aebb87c99e90 /remoting | |
parent | 76120c694d8dcdc082866a8038ffc4b0867e1903 (diff) | |
download | chromium_src-7829d55746af0b4d0889d6b65f95cff89b4ef79a.zip chromium_src-7829d55746af0b4d0889d6b65f95cff89b4ef79a.tar.gz chromium_src-7829d55746af0b4d0889d6b65f95cff89b4ef79a.tar.bz2 |
Added SpyPromise class for testing code that uses promises.
Review URL: https://codereview.chromium.org/990673003
Cr-Commit-Position: refs/heads/master@{#320614}
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/remoting_webapp_files.gypi | 3 | ||||
-rw-r--r-- | remoting/webapp/base/js/base.js | 4 | ||||
-rw-r--r-- | remoting/webapp/js_proto/qunit_proto.js | 13 | ||||
-rw-r--r-- | remoting/webapp/unittests/spy_promise.js | 294 | ||||
-rw-r--r-- | remoting/webapp/unittests/spy_promise_unittest.js | 141 |
5 files changed, 382 insertions, 73 deletions
diff --git a/remoting/remoting_webapp_files.gypi b/remoting/remoting_webapp_files.gypi index 741c062..e82ac95 100644 --- a/remoting/remoting_webapp_files.gypi +++ b/remoting/remoting_webapp_files.gypi @@ -62,6 +62,9 @@ ], # The unit test cases for the webapp 'remoting_webapp_unittests_js_files': [ + # TODO(jrw): Move spy_promise to base. + 'webapp/unittests/spy_promise.js', + 'webapp/unittests/spy_promise_unittest.js', 'webapp/base/js/base_unittest.js', 'webapp/base/js/base_event_hook_unittest.js', 'webapp/base/js/ipc_unittest.js', diff --git a/remoting/webapp/base/js/base.js b/remoting/webapp/base/js/base.js index 35012ea..51fff37 100644 --- a/remoting/webapp/base/js/base.js +++ b/remoting/webapp/base/js/base.js @@ -11,7 +11,9 @@ 'use strict'; -var base = {}; +/** @suppress {duplicate} */ +var base = base || {}; + base.debug = function() {}; /** diff --git a/remoting/webapp/js_proto/qunit_proto.js b/remoting/webapp/js_proto/qunit_proto.js index f826af8..8eb9645 100644 --- a/remoting/webapp/js_proto/qunit_proto.js +++ b/remoting/webapp/js_proto/qunit_proto.js @@ -57,9 +57,9 @@ QUnit.module = function(desc, dict) {}; /** * @param {*} a * @param {*} b - * @param {string} desc + * @param {string=} opt_desc */ -QUnit.notEqual = function(a, b, desc) {}; +QUnit.notEqual = function(a, b, opt_desc) {}; /** * @param {boolean} cond @@ -79,6 +79,15 @@ QUnit.test = function(desc, f) {}; /** @param {Function} f */ QUnit.testStart = function(f) {}; +/** + * @interface + */ +QUnit.Assert = function() {}; + +/** + * @return {function():void} + */ +QUnit.Assert.prototype.async = function() {}; var deepEqual = QUnit.deepEqual; var equal = QUnit.equal; diff --git a/remoting/webapp/unittests/spy_promise.js b/remoting/webapp/unittests/spy_promise.js new file mode 100644 index 0000000..3429d27 --- /dev/null +++ b/remoting/webapp/unittests/spy_promise.js @@ -0,0 +1,294 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** @suppress {duplicate} */ +var base = base || {}; + +(function() { +/** + * A wrapper around a Promise object that keeps track of all + * outstanding promises. This function is written to serve as a + * drop-in replacement for the native Promise constructor. To create + * a SpyPromise from an existing native Promise, use + * SpyPromise.resolve. + * + * Note that this is a pseudo-constructor that actually returns a + * regular promise with appropriate handlers attached. This detail + * should be transparent when SpyPromise.activate has been called. + * + * The normal way to use this class is within a call to + * SpyPromise.run, for example: + * + * base.SpyPromise.run(function() { + * myCodeThatUsesPromises(); + * }); + * base.SpyPromise.settleAll().then(function() { + * console.log('All promises have been settled!'); + * }); + * + * @constructor + * @extends {Promise} + * @param {function(function(?):?, function(*):?):?} func A function + * of the same type used as an argument to the native Promise + * constructor, in other words, a function which is called + * immediately, and whose arguments are a resolve function and a + * reject function. + */ +base.SpyPromise = function(func) { + var unsettled = new RealPromise(func); + var unsettledId = remember(unsettled); + return unsettled.then(function(/** * */value) { + forget(unsettledId); + return value; + }, function(error) { + forget(unsettledId); + throw error; + }); +}; + +/** + * The real promise constructor. Needed because it is normally hidden + * by SpyPromise.activate or SpyPromise.run. + * @const + */ +var RealPromise = Promise; + +/** + * The real window.setTimeout method. Needed because some test + * frameworks like to replace this method with a fake implementation. + * @const + */ +var realSetTimeout = window.setTimeout.bind(window); + +/** + * The number of unsettled promises. + * @type {number} + */ +base.SpyPromise.unsettledCount; // initialized by reset() + +/** + * A collection of all unsettled promises. + * @type {!Object<number,!Promise>} + */ +var unsettled; // initialized by reset() + +/** + * A counter used to assign ID numbers to new SpyPromise objects. + * @type {number} + */ +var nextPromiseId; // initialized by reset() + +/** + * A promise returned by SpyPromise.settleAll. + * @type {Promise<null>} + */ +var settleAllPromise; // initialized by reset() + +/** + * Records an unsettled promise. + * + * @param {!Promise} unsettledPromise + * @return {number} The ID number to be passed to forget_. + */ +function remember(unsettledPromise) { + var id = nextPromiseId++; + if (unsettled[id] != null) { + throw Error('Duplicate ID: ' + id); + } + base.SpyPromise.unsettledCount++; + unsettled[id] = unsettledPromise; + return id; +}; + +/** + * Forgets a promise. Called after the promise has been settled. + * + * @param {number} id + * @private + */ +function forget(id) { + base.debug.assert(unsettled[id] != null); + base.SpyPromise.unsettledCount--; + delete unsettled[id]; +}; + +/** + * Forgets about all unsettled promises. + */ +base.SpyPromise.reset = function() { + base.SpyPromise.unsettledCount = 0; + unsettled = {}; + nextPromiseId = 0; + settleAllPromise = null; +}; + +// Initialize static variables. +base.SpyPromise.reset(); + +/** + * Tries to wait until all promises has been settled. + * + * @param {number=} opt_maxTimeMs The maximum number of milliseconds + * (approximately) to wait (default: 1000). + * @return {!Promise<null>} A real promise that is resolved when all + * SpyPromises have been settled, or rejected after opt_maxTimeMs + * milliseconds have elapsed. + */ +base.SpyPromise.settleAll = function(opt_maxTimeMs) { + if (settleAllPromise) { + return settleAllPromise; + } + + var maxDelay = opt_maxTimeMs == null ? 1000 : opt_maxTimeMs; + + /** + * @param {number} count + * @param {number} totalDelay + * @return {!Promise<null>} + */ + function loop(count, totalDelay) { + return new RealPromise(function(resolve, reject) { + if (base.SpyPromise.unsettledCount == 0) { + settleAllPromise = null; + resolve(null); + } else if (totalDelay > maxDelay) { + settleAllPromise = null; + base.SpyPromise.reset(); + reject(new Error('base.SpyPromise.settleAll timed out')); + } else { + // This implements quadratic backoff according to Euler's + // triangular number formula. + var delay = count; + + // Must jump through crazy hoops to get a real timer in a unit test. + realSetTimeout(function() { + resolve(loop( + count + 1, + delay + totalDelay)); + }, delay); + } + }); + }; + + // An extra promise needed here to prevent the loop function from + // finishing before settleAllPromise is set. If that happens, + // settleAllPromise will never be reset to null. + settleAllPromise = RealPromise.resolve().then(function() { + return loop(0, 0); + }); + return settleAllPromise; +}; + +/** + * Only for testing this class. Do not use. + * @returns {boolean} True if settleAll is executing. + */ +base.SpyPromise.isSettleAllRunning = function() { + return settleAllPromise != null; +}; + +/** + * Wrapper for Promise.resolve. + * + * @param {*} value + * @return {!base.SpyPromise} + */ +base.SpyPromise.resolve = function(value) { + return new base.SpyPromise(function(resolve, reject) { + resolve(value); + }); +}; + +/** + * Wrapper for Promise.reject. + * + * @param {*} value + * @return {!base.SpyPromise} + */ +base.SpyPromise.reject = function(value) { + return new base.SpyPromise(function(resolve, reject) { + reject(value); + }); +}; + +/** + * Wrapper for Promise.all. + * + * @param {!Array<Promise>} promises + * @return {!base.SpyPromise} + */ +base.SpyPromise.all = function(promises) { + return base.SpyPromise.resolve(RealPromise.all(promises)); +}; + +/** + * Wrapper for Promise.race. + * + * @param {!Array<Promise>} promises + * @return {!base.SpyPromise} + */ +base.SpyPromise.race = function(promises) { + return base.SpyPromise.resolve(RealPromise.race(promises)); +}; + +/** + * Sets Promise = base.SpyPromise. Must not be called more than once + * without an intervening call to restore(). + */ +base.SpyPromise.activate = function() { + if (settleAllPromise) { + throw Error('called base.SpyPromise.activate while settleAll is running'); + } + if (Promise === base.SpyPromise) { + throw Error('base.SpyPromise is already active'); + } + Promise = base.SpyPromise; +}; + +/** + * Restores the original value of Promise. + */ +base.SpyPromise.restore = function() { + if (settleAllPromise) { + throw Error('called base.SpyPromise.restore while settleAll is running'); + } + if (Promise === base.SpyPromise) { + Promise = RealPromise; + } else if (Promise === RealPromise) { + throw new Error('base.SpyPromise is not active.'); + } else { + throw new Error('Something fishy is going on.'); + } +}; + +/** + * Calls func with Promise equal to base.SpyPromise. + * + * @param {function():void} func A function which is expected to + * create one or more promises. + * @param {number=} opt_timeoutMs An optional timeout specifying how + * long to wait for promise chains started in func to be settled. + * (default: 1000 ms) + * @return {!Promise<null>} A promise that is resolved after every + * promise chain started in func is fully settled, or rejected + * after a opt_timeoutMs. In any case, the original value of the + * Promise constructor is restored before this promise is settled. + */ +base.SpyPromise.run = function(func, opt_timeoutMs) { + base.SpyPromise.activate(); + try { + func(); + } finally { + return base.SpyPromise.settleAll(opt_timeoutMs).then(function() { + base.SpyPromise.restore(); + return null; + }, function(error) { + base.SpyPromise.restore(); + throw error; + }); + } +}; +})();
\ No newline at end of file diff --git a/remoting/webapp/unittests/spy_promise_unittest.js b/remoting/webapp/unittests/spy_promise_unittest.js index f8daada..20c201d 100644 --- a/remoting/webapp/unittests/spy_promise_unittest.js +++ b/remoting/webapp/unittests/spy_promise_unittest.js @@ -11,7 +11,7 @@ var originalGlobalPromise = Promise; QUnit.module('spy_promise', { beforeEach: function() { assertInitialState(); - SpyPromise.reset(); // Defend against broken tests. + base.SpyPromise.reset(); // Defend against broken tests. }, afterEach: function() { assertInitialState(); @@ -20,12 +20,12 @@ QUnit.module('spy_promise', { function assertInitialState() { QUnit.equal(Promise, originalGlobalPromise); - QUnit.equal( - SpyPromise['settleAllPromise_'], null, + QUnit.ok( + !base.SpyPromise.isSettleAllRunning(), 'settleAll should not be running'); QUnit.equal( - SpyPromise.unsettledCount, 0, - 'SpyPromise.unsettledCount should be zero ' + + base.SpyPromise.unsettledCount, 0, + 'base.SpyPromise.unsettledCount should be zero ' + 'before/after any test finishes'); } @@ -33,141 +33,142 @@ function assertInitialState() { * @return {!Promise} */ function finish() { - return SpyPromise.settleAll().then(function() { + return base.SpyPromise.settleAll().then(function() { QUnit.equal( - SpyPromise.unsettledCount, 0, - 'SpyPromise.unsettledCount should be zero after settleAll finishes.'); + base.SpyPromise.unsettledCount, 0, + 'base.SpyPromise.unsettledCount should be zero ' + + 'after settleAll finishes.'); }); }; -QUnit.test('run', function(assert) { +QUnit.test('run', function(/** QUnit.Assert */ assert) { var done = assert.async(); - QUnit.notEqual(SpyPromise, originalGlobalPromise); - return SpyPromise.run(function() { - QUnit.equal(Promise, SpyPromise); - QUnit.equal(SpyPromise.unsettledCount, 0); + QUnit.notEqual(base.SpyPromise, originalGlobalPromise); + return base.SpyPromise.run(function() { + QUnit.equal(Promise, base.SpyPromise); + QUnit.equal(base.SpyPromise.unsettledCount, 0); var dummy1 = new Promise(function(resolve) { resolve(null); }); - QUnit.equal(SpyPromise.unsettledCount, 1); + QUnit.equal(base.SpyPromise.unsettledCount, 1); }).then(function() { QUnit.equal(Promise, originalGlobalPromise); - QUnit.equal(SpyPromise.unsettledCount, 0); + QUnit.equal(base.SpyPromise.unsettledCount, 0); done(); }); }); QUnit.test('activate/restore', function() { - QUnit.notEqual(SpyPromise, originalGlobalPromise); - SpyPromise.activate(); - QUnit.notEqual(SpyPromise, originalGlobalPromise); - QUnit.equal(SpyPromise.unsettledCount, 0); + QUnit.notEqual(base.SpyPromise, originalGlobalPromise); + base.SpyPromise.activate(); + QUnit.notEqual(base.SpyPromise, originalGlobalPromise); + QUnit.equal(base.SpyPromise.unsettledCount, 0); var dummy1 = new Promise(function(resolve) { resolve(null); }); - QUnit.equal(SpyPromise.unsettledCount, 1); - SpyPromise.restore(); + QUnit.equal(base.SpyPromise.unsettledCount, 1); + base.SpyPromise.restore(); QUnit.equal(Promise, originalGlobalPromise); return finish(); }); -QUnit.test('new/then', function(assert) { +QUnit.test('new/then', function(/** QUnit.Assert */ assert) { var done = assert.async(); - new SpyPromise(function(resolve, reject) { + new base.SpyPromise(function(resolve, reject) { resolve('hello'); }).then(function(/**string*/ value) { - QUnit.equal(SpyPromise.unsettledCount, 0); + QUnit.equal(base.SpyPromise.unsettledCount, 0); QUnit.equal(value, 'hello'); done(); }); - QUnit.equal(SpyPromise.unsettledCount, 1); + QUnit.equal(base.SpyPromise.unsettledCount, 1); return finish(); }); -QUnit.test('new/catch', function(assert) { +QUnit.test('new/catch', function(/** QUnit.Assert */ assert) { var done = assert.async(); - new SpyPromise(function(resolve, reject) { + new base.SpyPromise(function(resolve, reject) { reject('hello'); }).catch(function(/**string*/ value) { - QUnit.equal(SpyPromise.unsettledCount, 0); + QUnit.equal(base.SpyPromise.unsettledCount, 0); QUnit.equal(value, 'hello'); done(); }); - QUnit.equal(SpyPromise.unsettledCount, 1); + QUnit.equal(base.SpyPromise.unsettledCount, 1); return finish(); }); -QUnit.test('new+throw/catch', function(assert) { +QUnit.test('new+throw/catch', function(/** QUnit.Assert */ assert) { var done = assert.async(); - new SpyPromise(function(resolve, reject) { + new base.SpyPromise(function(resolve, reject) { throw 'hello'; }).catch(function(/**string*/ value) { - QUnit.equal(SpyPromise.unsettledCount, 0); + QUnit.equal(base.SpyPromise.unsettledCount, 0); QUnit.equal(value, 'hello'); done(); }); - QUnit.equal(SpyPromise.unsettledCount, 1); + QUnit.equal(base.SpyPromise.unsettledCount, 1); return finish(); }); -QUnit.test('resolve/then', function(assert) { +QUnit.test('resolve/then', function(/** QUnit.Assert */ assert) { var done = assert.async(); - SpyPromise.resolve('hello').then(function(/**string*/ value) { - QUnit.equal(SpyPromise.unsettledCount, 0); + base.SpyPromise.resolve('hello').then(function(/**string*/ value) { + QUnit.equal(base.SpyPromise.unsettledCount, 0); QUnit.equal(value, 'hello'); done(); }); - QUnit.equal(SpyPromise.unsettledCount, 1); + QUnit.equal(base.SpyPromise.unsettledCount, 1); return finish(); }); -QUnit.test('reject/then', function(assert) { +QUnit.test('reject/then', function(/** QUnit.Assert */ assert) { var done = assert.async(); - SpyPromise.reject('hello').then(null, function(/**string*/ value) { - QUnit.equal(SpyPromise.unsettledCount, 0); + base.SpyPromise.reject('hello').then(null, function(/**string*/ value) { + QUnit.equal(base.SpyPromise.unsettledCount, 0); QUnit.equal(value, 'hello'); done(); }); - QUnit.equal(SpyPromise.unsettledCount, 1); + QUnit.equal(base.SpyPromise.unsettledCount, 1); return finish(); }); -QUnit.test('reject/catch', function(assert) { +QUnit.test('reject/catch', function(/** QUnit.Assert */ assert) { var done = assert.async(); - SpyPromise.reject('hello').catch(function(/**string*/ value) { - QUnit.equal(SpyPromise.unsettledCount, 0); + base.SpyPromise.reject('hello').catch(function(/**string*/ value) { + QUnit.equal(base.SpyPromise.unsettledCount, 0); QUnit.equal(value, 'hello'); done(); }); - QUnit.equal(SpyPromise.unsettledCount, 1); + QUnit.equal(base.SpyPromise.unsettledCount, 1); return finish(); }); -QUnit.test('all', function(assert) { +QUnit.test('all', function(/** QUnit.Assert */ assert) { var done = assert.async(); - SpyPromise.all([Promise.resolve(1), Promise.resolve(2)]). + base.SpyPromise.all([Promise.resolve(1), Promise.resolve(2)]). then(function(/**string*/ value) { - QUnit.equal(SpyPromise.unsettledCount, 0); + QUnit.equal(base.SpyPromise.unsettledCount, 0); QUnit.deepEqual(value, [1, 2]); done(); }); - QUnit.equal(SpyPromise.unsettledCount, 1); + QUnit.equal(base.SpyPromise.unsettledCount, 1); return finish(); }); -QUnit.test('race', function(assert) { +QUnit.test('race', function(/** QUnit.Assert */ assert) { var done = assert.async(); var fast = Promise.resolve('fast'); var slow = new Promise(function() {}); // never settled - SpyPromise.race([fast, slow]). + base.SpyPromise.race([fast, slow]). then(function(/**string*/ value) { - QUnit.equal(SpyPromise.unsettledCount, 0); + QUnit.equal(base.SpyPromise.unsettledCount, 0); QUnit.equal(value, 'fast'); done(); }); - QUnit.equal(SpyPromise.unsettledCount, 1); + QUnit.equal(base.SpyPromise.unsettledCount, 1); return finish(); }); -QUnit.test('resolve/then/then', function(assert) { +QUnit.test('resolve/then/then', function(/** QUnit.Assert */ assert) { var done = assert.async(); - SpyPromise.resolve('hello').then(function(/**string*/ value) { + base.SpyPromise.resolve('hello').then(function(/**string*/ value) { QUnit.equal(value, 'hello'); return 'goodbye'; }).then(function(/**string*/ value) { @@ -178,9 +179,9 @@ QUnit.test('resolve/then/then', function(assert) { }); -QUnit.test('resolve/then+throw/catch', function(assert) { +QUnit.test('resolve/then+throw/catch', function(/** QUnit.Assert */ assert) { var done = assert.async(); - SpyPromise.resolve('hello').then(function(/**string*/ value) { + base.SpyPromise.resolve('hello').then(function(/**string*/ value) { QUnit.equal(value, 'hello'); throw 'goodbye'; }).catch(function(/**string*/ value) { @@ -190,9 +191,9 @@ QUnit.test('resolve/then+throw/catch', function(assert) { return finish(); }); -QUnit.test('reject/catch/then', function(assert) { +QUnit.test('reject/catch/then', function(/** QUnit.Assert */ assert) { var done = assert.async(); - SpyPromise.reject('hello').catch(function(/**string*/ value) { + base.SpyPromise.reject('hello').catch(function(/**string*/ value) { QUnit.equal(value, 'hello'); return 'goodbye'; }).then(function(/**string*/ value) { @@ -203,9 +204,9 @@ QUnit.test('reject/catch/then', function(assert) { }); -QUnit.test('reject/catch+throw/catch', function(assert) { +QUnit.test('reject/catch+throw/catch', function(/** QUnit.Assert */ assert) { var done = assert.async(); - SpyPromise.reject('hello').catch(function(/**string*/ value) { + base.SpyPromise.reject('hello').catch(function(/**string*/ value) { QUnit.equal(value, 'hello'); throw 'goodbye'; }).catch(function(/**string*/ value) { @@ -215,32 +216,32 @@ QUnit.test('reject/catch+throw/catch', function(assert) { return finish(); }); -QUnit.test('settleAll timeout = 100', function(assert) { +QUnit.test('settleAll timeout = 100', function(/** QUnit.Assert */ assert) { var done = assert.async(); var startTime = Date.now(); - var neverResolved = new SpyPromise(function() {}); - return SpyPromise.settleAll(100).catch(function(error) { + var neverResolved = new base.SpyPromise(function() {}); + return base.SpyPromise.settleAll(100).catch(function(error) { QUnit.ok(error instanceof Error); QUnit.ok(startTime + 200 < Date.now()); done(); }); }); -QUnit.test('settleAll timeout = 500', function(assert) { +QUnit.test('settleAll timeout = 500', function(/** QUnit.Assert */ assert) { var done = assert.async(); var startTime = Date.now(); - var neverResolved = new SpyPromise(function() {}); - return SpyPromise.settleAll(500).catch(function(error) { + var neverResolved = new base.SpyPromise(function() {}); + return base.SpyPromise.settleAll(500).catch(function(error) { QUnit.ok(startTime + 750 < Date.now()); done(); }); }); -QUnit.test('settleAll timeout = 1000', function(assert) { +QUnit.test('settleAll timeout = 1000', function(/** QUnit.Assert */ assert) { var done = assert.async(); var startTime = Date.now(); - var neverResolved = new SpyPromise(function() {}); - return SpyPromise.settleAll(1000).catch(function(error) { + var neverResolved = new base.SpyPromise(function() {}); + return base.SpyPromise.settleAll(1000).catch(function(error) { QUnit.ok(startTime + 1500 < Date.now()); done(); }); |