From b47eef370e66f6adfddb8b611af6b18672df326a Mon Sep 17 00:00:00 2001 From: jamiewalch Date: Wed, 14 Jan 2015 14:59:14 -0800 Subject: [PATCH] Implement FallbackSignalStrategy. This allows a preferred signal strategy (currently always XMPP) to be used speculatively, falling back on a back-up strategy (currently always WCS) if that fails or times out. There is also a logging hook that will allow us to log which strategy is in use, as well as corner cases such as the preferred strategy succeeding after the time-out has expired (which would imply that the time-out is too short). The actual logging will be a follow-up CL. BUG=432171 Review URL: https://codereview.chromium.org/815943004 Cr-Commit-Position: refs/heads/master@{#311567} --- remoting/remoting_webapp_files.gypi | 2 + remoting/webapp/crd/js/fallback_signal_strategy.js | 329 +++++++++++++++++++++ remoting/webapp/crd/js/signal_strategy.js | 27 +- .../unittests/fallback_signal_strategy_unittest.js | 296 ++++++++++++++++++ 4 files changed, 653 insertions(+), 1 deletion(-) create mode 100644 remoting/webapp/crd/js/fallback_signal_strategy.js create mode 100644 remoting/webapp/unittests/fallback_signal_strategy_unittest.js diff --git a/remoting/remoting_webapp_files.gypi b/remoting/remoting_webapp_files.gypi index 92e45a5a1407..61668bf03937 100644 --- a/remoting/remoting_webapp_files.gypi +++ b/remoting/remoting_webapp_files.gypi @@ -118,6 +118,7 @@ ], # Remoting signaling files. 'remoting_webapp_js_signaling_files': [ + 'webapp/crd/js/fallback_signal_strategy.js', 'webapp/crd/js/signal_strategy.js', 'webapp/crd/js/wcs_adapter.js', 'webapp/crd/js/wcs_sandbox_container.js', @@ -169,6 +170,7 @@ 'webapp/unittests/chrome_mocks.js', 'webapp/js_proto/chrome_proto.js', 'webapp/unittests/base_unittest.js', + 'webapp/unittests/fallback_signal_strategy_unittest.js', 'webapp/unittests/it2me_helpee_channel_unittest.js', 'webapp/unittests/it2me_helper_channel_unittest.js', 'webapp/unittests/it2me_service_unittest.js', diff --git a/remoting/webapp/crd/js/fallback_signal_strategy.js b/remoting/webapp/crd/js/fallback_signal_strategy.js new file mode 100644 index 000000000000..bb58e4d45ae6 --- /dev/null +++ b/remoting/webapp/crd/js/fallback_signal_strategy.js @@ -0,0 +1,329 @@ +// 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 remoting = remoting || {}; + +/** + * A signal strategy encapsulating a primary and a back-up strategy. If the + * primary fails or times out, then the secondary is used. Information about + * which strategy was used, and why, is returned via |onProgressCallback|. + * + * @param {function( + * function(remoting.SignalStrategy.State) + * ):remoting.SignalStrategy} primaryFactory + * @param {function( + * function(remoting.SignalStrategy.State) + * ):remoting.SignalStrategy} secondaryFactory + * @param {function(remoting.SignalStrategy.State):void} onStateChangedCallback + * @param {function(remoting.FallbackSignalStrategy.Progress)} + * onProgressCallback + * + * @implements {remoting.SignalStrategy} + * @constructor + */ +remoting.FallbackSignalStrategy = function( + primaryFactory, secondaryFactory, + onStateChangedCallback, onProgressCallback) { + /** + * @type {remoting.SignalStrategy} + * @private + */ + this.primary_ = primaryFactory(this.onPrimaryStateChanged_.bind(this)); + + /** + * @type {remoting.SignalStrategy} + * @private + */ + this.secondary_ = secondaryFactory(this.onSecondaryStateChanged_.bind(this)); + + /** + * @type {function(remoting.SignalStrategy.State)} + * @private + */ + this.onStateChangedCallback_ = onStateChangedCallback; + + /** + * @type {function(remoting.FallbackSignalStrategy.Progress)} + * @private + */ + this.onProgressCallback_ = onProgressCallback; + + /** + * @type {?function(Element):void} + * @private + */ + this.onIncomingStanzaCallback_ = null; + + /** + * @type {number} + * @private + * @const + */ + this.PRIMARY_CONNECT_TIMEOUT_MS_ = 10 * 1000; + + /** + * @enum {string} + * @private + */ + this.State = { + NOT_CONNECTED: 'not-connected', + PRIMARY_PENDING: 'primary-pending', + PRIMARY_SUCCEEDED: 'primary-succeeded', + SECONDARY_PENDING: 'secondary-pending', + SECONDARY_SUCCEEDED: 'secondary-succeeded', + SECONDARY_FAILED: 'secondary-failed', + CLOSED: 'closed' + }; + + /** + * @type {string} + * @private + */ + this.state_ = this.State.NOT_CONNECTED; + + /** + * @type {?remoting.SignalStrategy.State} + * @private + */ + this.externalState_ = null; + + /** + * @type {string} + * @private + */ + this.server_ = ''; + + /** + * @type {string} + * @private + */ + this.username_ = ''; + + /** + * @type {string} + * @private + */ + this.authToken_ = ''; + + /** + * @type {number} + * @private + */ + this.primaryConnectTimerId_ = 0; +}; + +/** + * @enum {string} + */ +remoting.FallbackSignalStrategy.Progress = { + PRIMARY_SUCCEEDED: 'primary-succeeded', + PRIMARY_FAILED: 'primary-failed', + PRIMARY_TIMED_OUT: 'primary-timed-out', + PRIMARY_SUCCEEDED_LATE: 'primary-succeeded-late', + PRIMARY_FAILED_LATE: 'primary-failed-late', + SECONDARY_SUCCEEDED: 'secondary-succeeded', + SECONDARY_FAILED: 'secondary-failed' +}; + +remoting.FallbackSignalStrategy.prototype.dispose = function() { + this.primary_.dispose(); + this.secondary_.dispose(); +}; + +/** + * @param {?function(Element):void} onIncomingStanzaCallback Callback to call on + * incoming messages. + */ +remoting.FallbackSignalStrategy.prototype.setIncomingStanzaCallback = + function(onIncomingStanzaCallback) { + this.onIncomingStanzaCallback_ = onIncomingStanzaCallback; + if (this.state_ == this.State.PRIMARY_PENDING || + this.state_ == this.State.PRIMARY_SUCCEEDED) { + this.primary_.setIncomingStanzaCallback(onIncomingStanzaCallback); + } else if (this.state_ == this.State.SECONDARY_PENDING || + this.state_ == this.State.SECONDARY_SUCCEEDED) { + this.secondary_.setIncomingStanzaCallback(onIncomingStanzaCallback); + } +}; + +/** + * @param {string} server + * @param {string} username + * @param {string} authToken + */ +remoting.FallbackSignalStrategy.prototype.connect = + function(server, username, authToken) { + base.debug.assert(this.state_ == this.State.NOT_CONNECTED); + this.server_ = server; + this.username_ = username; + this.authToken_ = authToken; + this.state_ = this.State.PRIMARY_PENDING; + this.primary_.setIncomingStanzaCallback(this.onIncomingStanzaCallback_); + this.primary_.connect(server, username, authToken); + this.primaryConnectTimerId_ = + window.setTimeout(this.onPrimaryTimeout_.bind(this), + this.PRIMARY_CONNECT_TIMEOUT_MS_); +}; + +/** + * Sends a message. Can be called only in CONNECTED state. + * @param {string} message + */ +remoting.FallbackSignalStrategy.prototype.sendMessage = function(message) { + this.getConnectedSignalStrategy_().sendMessage(message); +}; + +/** @return {remoting.SignalStrategy.State} Current state */ +remoting.FallbackSignalStrategy.prototype.getState = function() { + return (this.externalState_ === null) + ? remoting.SignalStrategy.State.NOT_CONNECTED + : this.externalState_; +}; + +/** @return {remoting.Error} Error when in FAILED state. */ +remoting.FallbackSignalStrategy.prototype.getError = function() { + base.debug.assert(this.state_ == this.State.SECONDARY_FAILED); + base.debug.assert( + this.secondary_.getState() == remoting.SignalStrategy.State.FAILED); + return this.secondary_.getError(); +}; + +/** @return {string} Current JID when in CONNECTED state. */ +remoting.FallbackSignalStrategy.prototype.getJid = function() { + return this.getConnectedSignalStrategy_().getJid(); +}; + +/** + * @return {remoting.SignalStrategy} The active signal strategy, if the + * connection has succeeded. + * @private + */ +remoting.FallbackSignalStrategy.prototype.getConnectedSignalStrategy_ = + function() { + if (this.state_ == this.State.PRIMARY_SUCCEEDED) { + base.debug.assert( + this.primary_.getState() == remoting.SignalStrategy.State.CONNECTED); + return this.primary_; + } else if (this.state_ == this.State.SECONDARY_SUCCEEDED) { + base.debug.assert( + this.secondary_.getState() == remoting.SignalStrategy.State.CONNECTED); + return this.secondary_; + } else { + base.debug.assert( + false, + 'getConnectedSignalStrategy called in unconnected state'); + return null; + } +}; + +/** + * @param {remoting.SignalStrategy.State} state + * @private + */ +remoting.FallbackSignalStrategy.prototype.onPrimaryStateChanged_ = + function(state) { + switch (state) { + case remoting.SignalStrategy.State.CONNECTED: + if (this.state_ == this.State.PRIMARY_PENDING) { + window.clearTimeout(this.primaryConnectTimerId_); + this.onProgressCallback_( + remoting.FallbackSignalStrategy.Progress.PRIMARY_SUCCEEDED); + this.state_ = this.State.PRIMARY_SUCCEEDED; + } else { + this.onProgressCallback_( + remoting.FallbackSignalStrategy.Progress.PRIMARY_SUCCEEDED_LATE); + } + break; + + case remoting.SignalStrategy.State.FAILED: + if (this.state_ == this.State.PRIMARY_PENDING) { + window.clearTimeout(this.primaryConnectTimerId_); + this.onProgressCallback_( + remoting.FallbackSignalStrategy.Progress.PRIMARY_FAILED); + this.connectSecondary_(); + } else { + this.onProgressCallback_( + remoting.FallbackSignalStrategy.Progress.PRIMARY_FAILED_LATE); + } + return; // Don't notify the external callback + + case remoting.SignalStrategy.State.CLOSED: + this.state_ = this.State.CLOSED; + break; + } + + this.notifyExternalCallback_(state); +}; + +/** + * @param {remoting.SignalStrategy.State} state + * @private + */ +remoting.FallbackSignalStrategy.prototype.onSecondaryStateChanged_ = + function(state) { + switch (state) { + case remoting.SignalStrategy.State.CONNECTED: + this.onProgressCallback_( + remoting.FallbackSignalStrategy.Progress.SECONDARY_SUCCEEDED); + this.state_ = this.State.SECONDARY_SUCCEEDED; + break; + + case remoting.SignalStrategy.State.FAILED: + this.onProgressCallback_( + remoting.FallbackSignalStrategy.Progress.SECONDARY_FAILED); + this.state_ = this.State.SECONDARY_FAILED; + break; + + case remoting.SignalStrategy.State.CLOSED: + this.state_ = this.State.CLOSED; + break; + } + + this.notifyExternalCallback_(state); +}; + +/** + * Notify the external callback of a change in state if it's consistent with + * the allowed state transitions (ie, if it represents a later stage in the + * connection process). Suppress state transitions that would violate this, + * for example a CONNECTING -> NOT_CONNECTED transition when we switch from + * the primary to the secondary signal strategy. + * + * @param {remoting.SignalStrategy.State} state + * @private + */ +remoting.FallbackSignalStrategy.prototype.notifyExternalCallback_ = + function(state) { + if (this.externalState_ === null || state > this.externalState_) { + this.externalState_ = state; + this.onStateChangedCallback_(state); + } +}; + +/** + * @private + */ +remoting.FallbackSignalStrategy.prototype.connectSecondary_ = function() { + base.debug.assert(this.state_ == this.State.PRIMARY_PENDING); + base.debug.assert(this.server_ != ''); + base.debug.assert(this.username_ != ''); + base.debug.assert(this.authToken_ != ''); + + this.state_ = this.State.SECONDARY_PENDING; + this.primary_.setIncomingStanzaCallback(null); + this.secondary_.setIncomingStanzaCallback(this.onIncomingStanzaCallback_); + this.secondary_.connect(this.server_, this.username_, this.authToken_); +}; + +/** + * @private + */ +remoting.FallbackSignalStrategy.prototype.onPrimaryTimeout_ = function() { + this.onProgressCallback_( + remoting.FallbackSignalStrategy.Progress.PRIMARY_TIMED_OUT); + this.connectSecondary_(); +}; diff --git a/remoting/webapp/crd/js/signal_strategy.js b/remoting/webapp/crd/js/signal_strategy.js index 02c8acbd5f0c..f07613f9bd3a 100644 --- a/remoting/webapp/crd/js/signal_strategy.js +++ b/remoting/webapp/crd/js/signal_strategy.js @@ -23,6 +23,8 @@ remoting.SignalStrategy = function() {}; * CONNECTING -> FAILED (connection failed). * HANDSHAKE -> FAILED (authentication failed). * * -> CLOSED (dispose() called). + * + * Do not re-order these values without updating fallback_signal_strategy.js. */ remoting.SignalStrategy.State = { NOT_CONNECTED: 0, @@ -76,7 +78,30 @@ remoting.SignalStrategy.create = function(onStateChangedCallback) { // not the case for V1 app (socket API is available only to platform apps) // and for Chrome releases before 38. if (chrome.socket && chrome.socket.secure) { - return new remoting.XmppConnection(onStateChangedCallback); + /** + * @param {function(remoting.SignalStrategy.State): void} onStateChanged + */ + var xmppFactory = function(onStateChanged) { + return new remoting.XmppConnection(onStateChanged); + }; + + /** + * @param {function(remoting.SignalStrategy.State): void} onStateChanged + */ + var wcsFactory = function(onStateChanged) { + return new remoting.WcsAdapter(onStateChanged); + }; + + /** + * @param {remoting.FallbackSignalStrategy.Progress} progress + */ + var progressCallback = function(progress) { + console.log('FallbackSignalStrategy progress: ' + progress); + }; + + return new remoting.FallbackSignalStrategy( + xmppFactory, wcsFactory, onStateChangedCallback, progressCallback); + } else { return new remoting.WcsAdapter(onStateChangedCallback); } diff --git a/remoting/webapp/unittests/fallback_signal_strategy_unittest.js b/remoting/webapp/unittests/fallback_signal_strategy_unittest.js new file mode 100644 index 000000000000..18122fd23fff --- /dev/null +++ b/remoting/webapp/unittests/fallback_signal_strategy_unittest.js @@ -0,0 +1,296 @@ +// 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. + +(function() { + +'use strict'; + +var ControllableSignalStrategy = function(jid, stateChangeCallback) { + this.jid = jid; + this.stateChangeCallback_ = stateChangeCallback; + this.state_ = null; + this.onIncomingStanzaCallback = function() {}; + this.dispose = sinon.spy(); + this.connect = sinon.spy(); + this.sendMessage = sinon.spy(); +}; + +ControllableSignalStrategy.prototype.setIncomingStanzaCallback = + function(onIncomingStanzaCallback) { + this.onIncomingStanzaCallback = + onIncomingStanzaCallback ? onIncomingStanzaCallback + : function() {}; +} + +ControllableSignalStrategy.prototype.getState = function(message) { + return this.state_; +}; + +ControllableSignalStrategy.prototype.getError = function(message) { + return remoting.Error.UNKNOWN; +}; + +ControllableSignalStrategy.prototype.getJid = function(message) { + return this.jid; +}; + +ControllableSignalStrategy.prototype.setExternalCallbackForTesting = + function(externalCallback) { + this.externalCallback_ = externalCallback; +}; + +ControllableSignalStrategy.prototype.setStateForTesting = + function(state, expectExternalCallback) { + this.state_ = state; + this.externalCallback_.reset(); + this.stateChangeCallback_(state); + if (expectExternalCallback) { + equal(this.externalCallback_.callCount, 1); + ok(this.externalCallback_.calledWith(state)); + equal(strategy.getState(), state); + } else { + ok(!this.externalCallback_.called); + } +}; + +var createControllableSignalStrategy = function(jid, callback) { + return new ControllableSignalStrategy(jid, callback); +}; + +var onStateChange = null; +var onProgressCallback = null; +var onIncomingStanzaCallback = null; +var strategy = null; +var primary = null; +var secondary = null; + +module('fallback_signal_strategy', { + setup: function() { + onStateChange = sinon.spy(); + onProgressCallback = sinon.spy(); + onIncomingStanzaCallback = sinon.spy(); + strategy = new remoting.FallbackSignalStrategy( + createControllableSignalStrategy.bind(null, 'primary-jid'), + createControllableSignalStrategy.bind(null, 'secondary-jid'), + onStateChange, + onProgressCallback); + strategy.setIncomingStanzaCallback(onIncomingStanzaCallback); + primary = strategy.primary_; + secondary = strategy.secondary_; + primary.setExternalCallbackForTesting(onStateChange); + secondary.setExternalCallbackForTesting(onStateChange); + }, + teardown: function() { + onStateChange = null; + onProgressCallback = null; + onIncomingStanzaCallback = null; + strategy = null; + primary = null; + secondary = null; + }, + // Assert that the progress callback has been called exactly once + // since the last call, and with the specified state. + assertProgress: function(state) { + ok(onProgressCallback.calledOnce); + ok(onProgressCallback.calledWith(state)); + onProgressCallback.reset(); + } +}); + +test('primary succeeds; send & receive routed to it', + function() { + ok(!onStateChange.called); + ok(!primary.connect.called); + strategy.connect('server', 'username', 'authToken'); + ok(primary.connect.calledWith('server', 'username', 'authToken')); + + primary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + true); + primary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, true); + primary.setStateForTesting(remoting.SignalStrategy.State.HANDSHAKE, true); + + ok(!onProgressCallback.called); + primary.setStateForTesting(remoting.SignalStrategy.State.CONNECTED, true); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.PRIMARY_SUCCEEDED); + equal(strategy.getJid(), 'primary-jid'); + + ok(!onIncomingStanzaCallback.called); + primary.onIncomingStanzaCallback('test-receive-primary'); + secondary.onIncomingStanzaCallback('test-receive-secondary'); + ok(onIncomingStanzaCallback.calledOnce); + ok(onIncomingStanzaCallback.calledWith('test-receive-primary')); + + ok(!primary.sendMessage.called); + strategy.sendMessage('test-send'); + ok(primary.sendMessage.calledOnce); + ok(primary.sendMessage.calledWith('test-send')); + + ok(!primary.dispose.called); + ok(!secondary.dispose.called); + primary.setStateForTesting(remoting.SignalStrategy.State.CLOSED, true); + strategy.dispose(); + ok(primary.dispose.calledOnce); + ok(secondary.dispose.calledOnce); + } +); + +test('primary fails; secondary succeeds; send & receive routed to it', + function() { + ok(!onStateChange.called); + ok(!primary.connect.called); + strategy.connect('server', 'username', 'authToken'); + ok(primary.connect.calledWith('server', 'username', 'authToken')); + + primary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + true); + primary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, true); + + ok(!onProgressCallback.called); + ok(!secondary.connect.called); + primary.setStateForTesting(remoting.SignalStrategy.State.FAILED, false); + ok(secondary.connect.calledWith('server', 'username', 'authToken')); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.PRIMARY_FAILED); + + secondary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + false); + secondary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, + false); + secondary.setStateForTesting(remoting.SignalStrategy.State.HANDSHAKE, true); + + ok(!onProgressCallback.called); + secondary.setStateForTesting(remoting.SignalStrategy.State.CONNECTED, true); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.SECONDARY_SUCCEEDED); + equal(strategy.getJid(), 'secondary-jid'); + + ok(!onIncomingStanzaCallback.called); + primary.onIncomingStanzaCallback('test-receive-primary'); + secondary.onIncomingStanzaCallback('test-receive-secondary'); + ok(onIncomingStanzaCallback.calledOnce); + ok(onIncomingStanzaCallback.calledWith('test-receive-secondary')); + + ok(!secondary.sendMessage.called); + strategy.sendMessage('test-send'); + ok(!primary.sendMessage.called); + ok(secondary.sendMessage.calledOnce); + ok(secondary.sendMessage.calledWith('test-send')); + } +); + +test('primary fails; secondary fails', + function() { + ok(!onStateChange.called); + ok(!primary.connect.called); + strategy.connect('server', 'username', 'authToken'); + ok(primary.connect.calledWith('server', 'username', 'authToken')); + + primary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + true); + ok(!onProgressCallback.called); + ok(!secondary.connect.called); + primary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, true); + primary.setStateForTesting(remoting.SignalStrategy.State.FAILED, false); + ok(secondary.connect.calledWith('server', 'username', 'authToken')); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.PRIMARY_FAILED); + secondary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + false); + primary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, false); + secondary.setStateForTesting(remoting.SignalStrategy.State.FAILED, true); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.SECONDARY_FAILED); + } +); + +test('primary times out; secondary succeeds', + function() { + ok(!onStateChange.called); + ok(!primary.connect.called); + strategy.connect('server', 'username', 'authToken'); + ok(primary.connect.calledWith('server', 'username', 'authToken')); + + primary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + true); + primary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, true); + this.clock.tick(strategy.PRIMARY_CONNECT_TIMEOUT_MS_ - 1); + ok(!secondary.connect.called); + ok(!onProgressCallback.called); + this.clock.tick(1); + ok(secondary.connect.calledWith('server', 'username', 'authToken')); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.PRIMARY_TIMED_OUT); + secondary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + false); + secondary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, + false); + secondary.setStateForTesting(remoting.SignalStrategy.State.HANDSHAKE, true); + secondary.setStateForTesting(remoting.SignalStrategy.State.CONNECTED, true); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.SECONDARY_SUCCEEDED); + secondary.setStateForTesting(remoting.SignalStrategy.State.CLOSED, true); + primary.setStateForTesting(remoting.SignalStrategy.State.FAILED, false); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.PRIMARY_FAILED_LATE); + } +); + +test('primary times out; secondary fails', + function() { + ok(!onStateChange.called); + ok(!primary.connect.called); + strategy.connect('server', 'username', 'authToken'); + ok(primary.connect.calledWith('server', 'username', 'authToken')); + + primary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + true); + primary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, true); + this.clock.tick(strategy.PRIMARY_CONNECT_TIMEOUT_MS_ - 1); + ok(!secondary.connect.called); + ok(!onProgressCallback.called); + this.clock.tick(1); + ok(secondary.connect.calledWith('server', 'username', 'authToken')); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.PRIMARY_TIMED_OUT); + secondary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + false); + secondary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, + false); + secondary.setStateForTesting(remoting.SignalStrategy.State.FAILED, true); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.SECONDARY_FAILED); + } +); + +test('primary times out; secondary succeeds; primary succeeds late', + function() { + ok(!onStateChange.called); + ok(!primary.connect.called); + strategy.connect('server', 'username', 'authToken'); + ok(primary.connect.calledWith('server', 'username', 'authToken')); + + primary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + true); + primary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, true); + this.clock.tick(strategy.PRIMARY_CONNECT_TIMEOUT_MS_); + ok(secondary.connect.calledWith('server', 'username', 'authToken')); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.PRIMARY_TIMED_OUT); + secondary.setStateForTesting(remoting.SignalStrategy.State.NOT_CONNECTED, + false); + secondary.setStateForTesting(remoting.SignalStrategy.State.CONNECTING, + false); + secondary.setStateForTesting(remoting.SignalStrategy.State.HANDSHAKE, true); + secondary.setStateForTesting(remoting.SignalStrategy.State.CONNECTED, true); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.SECONDARY_SUCCEEDED); + primary.setStateForTesting(remoting.SignalStrategy.State.HANDSHAKE, false); + primary.setStateForTesting(remoting.SignalStrategy.State.CONNECTED, false); + this.assertProgress( + remoting.FallbackSignalStrategy.Progress.PRIMARY_SUCCEEDED_LATE); + } +); + +})(); -- 2.11.4.GIT