diff options
-rw-r--r-- | device/serial/BUILD.gn | 1 | ||||
-rw-r--r-- | device/serial/serial.gyp | 3 | ||||
-rw-r--r-- | device/serial/serial_serialization.mojom | 31 | ||||
-rw-r--r-- | extensions/renderer/api/serial/serial_api_unittest.cc | 25 | ||||
-rw-r--r-- | extensions/renderer/resources/extensions_renderer_resources.grd | 1 | ||||
-rw-r--r-- | extensions/renderer/resources/serial_custom_bindings.js | 4 | ||||
-rw-r--r-- | extensions/renderer/resources/serial_service.js | 248 | ||||
-rw-r--r-- | extensions/test/data/serial_unittest.js | 799 |
8 files changed, 713 insertions, 399 deletions
diff --git a/device/serial/BUILD.gn b/device/serial/BUILD.gn index ccab385..5e0dff6 100644 --- a/device/serial/BUILD.gn +++ b/device/serial/BUILD.gn @@ -77,5 +77,6 @@ mojom("serial_mojo") { "data_stream.mojom", "data_stream_serialization.mojom", "serial.mojom", + "serial_serialization.mojom", ] } diff --git a/device/serial/serial.gyp b/device/serial/serial.gyp index 1e0d05e..e9e47f2 100644 --- a/device/serial/serial.gyp +++ b/device/serial/serial.gyp @@ -21,6 +21,7 @@ 'data_stream.mojom', 'data_stream_serialization.mojom', 'serial.mojom', + 'serial_serialization.mojom', ], }, { @@ -55,6 +56,8 @@ '<(SHARED_INTERMEDIATE_DIR)/device/serial/data_stream_serialization.mojom.h', '<(SHARED_INTERMEDIATE_DIR)/device/serial/serial.mojom.cc', '<(SHARED_INTERMEDIATE_DIR)/device/serial/serial.mojom.h', + '<(SHARED_INTERMEDIATE_DIR)/device/serial/serial_serialization.mojom.cc', + '<(SHARED_INTERMEDIATE_DIR)/device/serial/serial_serialization.mojom.h', 'async_waiter.cc', 'async_waiter.h', 'buffer.cc', diff --git a/device/serial/serial_serialization.mojom b/device/serial/serial_serialization.mojom new file mode 100644 index 0000000..77a7ef7 --- /dev/null +++ b/device/serial/serial_serialization.mojom @@ -0,0 +1,31 @@ +// Copyright 2014 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. + +import "serial.mojom" +import "data_stream_serialization.mojom" + +module device.serial { + +// The client state of a serial connection. +struct ConnectionState { + uint32 connectionId; + bool paused = false; + bool persistent = false; + string name = ""; + uint32 receiveTimeout = 0; + uint32 sendTimeout = 0; + uint32 bufferSize = 4096; +}; + +// A serialized serial connection. +struct SerializedConnection { + ConnectionState state; + ReceiveError queuedReceiveError = NONE; + array<int8>? queuedReceiveData; + Connection connection; + SerializedDataSender sender; + SerializedDataReceiver receiver; +}; + +} diff --git a/extensions/renderer/api/serial/serial_api_unittest.cc b/extensions/renderer/api/serial/serial_api_unittest.cc index bf14bf3..bc60b7d 100644 --- a/extensions/renderer/api/serial/serial_api_unittest.cc +++ b/extensions/renderer/api/serial/serial_api_unittest.cc @@ -407,6 +407,8 @@ class SerialApiTest : public ApiTestBase { env()->RegisterModule("device/serial/data_stream_serialization.mojom", IDR_DATA_STREAM_SERIALIZATION_MOJOM_JS); env()->RegisterModule("device/serial/serial.mojom", IDR_SERIAL_MOJOM_JS); + env()->RegisterModule("device/serial/serial_serialization.mojom", + IDR_SERIAL_SERIALIZATION_MOJOM_JS); service_provider()->AddService<device::serial::SerialService>(base::Bind( &SerialApiTest::CreateSerialService, base::Unretained(this))); } @@ -465,6 +467,10 @@ TEST_F(SerialApiTest, GetInfo) { RunTest("serial_unittest.js", "testGetInfo"); } +TEST_F(SerialApiTest, GetInfoAfterSerialization) { + RunTest("serial_unittest.js", "testGetInfoAfterSerialization"); +} + TEST_F(SerialApiTest, GetInfoFailToGetPortInfo) { io_handler_ = new FailToGetInfoTestIoHandler(1); RunTest("serial_unittest.js", "testGetInfoFailToGetPortInfo"); @@ -492,6 +498,12 @@ TEST_F(SerialApiTest, Update) { EXPECT_EQ(11u, io_handler_->num_calls()); } +TEST_F(SerialApiTest, UpdateAcrossSerialization) { + io_handler_ = new ConfigurePortTestIoHandler; + RunTest("serial_unittest.js", "testUpdateAcrossSerialization"); + EXPECT_EQ(11u, io_handler_->num_calls()); +} + TEST_F(SerialApiTest, UpdateInvalidBitrate) { io_handler_ = new ConfigurePortTestIoHandler; RunTest("serial_unittest.js", "testUpdateInvalidBitrate"); @@ -512,6 +524,10 @@ TEST_F(SerialApiTest, Echo) { RunTest("serial_unittest.js", "testEcho"); } +TEST_F(SerialApiTest, EchoAfterSerialization) { + RunTest("serial_unittest.js", "testEchoAfterSerialization"); +} + TEST_F(SerialApiTest, SendDuringExistingSend) { RunTest("serial_unittest.js", "testSendDuringExistingSend"); } @@ -530,6 +546,11 @@ TEST_F(SerialApiTest, SendTimeout) { RunTest("serial_unittest.js", "testSendTimeout"); } +TEST_F(SerialApiTest, SendTimeoutAfterSerialization) { + io_handler_ = new BlockSendsForeverSendIoHandler(); + RunTest("serial_unittest.js", "testSendTimeoutAfterSerialization"); +} + TEST_F(SerialApiTest, DisableSendTimeout) { io_handler_ = new BlockSendsForeverSendIoHandler(); RunTest("serial_unittest.js", "testDisableSendTimeout"); @@ -550,6 +571,10 @@ TEST_F(SerialApiTest, ReceiveTimeout) { RunTest("serial_unittest.js", "testReceiveTimeout"); } +TEST_F(SerialApiTest, ReceiveTimeoutAfterSerialization) { + RunTest("serial_unittest.js", "testReceiveTimeoutAfterSerialization"); +} + TEST_F(SerialApiTest, DisableReceiveTimeout) { RunTest("serial_unittest.js", "testDisableReceiveTimeout"); } diff --git a/extensions/renderer/resources/extensions_renderer_resources.grd b/extensions/renderer/resources/extensions_renderer_resources.grd index 82aee00..8d984c7 100644 --- a/extensions/renderer/resources/extensions_renderer_resources.grd +++ b/extensions/renderer/resources/extensions_renderer_resources.grd @@ -25,6 +25,7 @@ <include name="IDR_SEND_REQUEST_JS" file="send_request.js" type="BINDATA" /> <include name="IDR_SERIAL_CUSTOM_BINDINGS_JS" file="serial_custom_bindings.js" type="BINDATA" /> <include name="IDR_SERIAL_MOJOM_JS" file="${mojom_root}\device\serial\serial.mojom.js" use_base_dir="false" type="BINDATA" /> + <include name="IDR_SERIAL_SERIALIZATION_MOJOM_JS" file="${mojom_root}\device\serial\serial_serialization.mojom.js" use_base_dir="false" type="BINDATA" /> <include name="IDR_SERIAL_SERVICE_JS" file="serial_service.js" type="BINDATA" /> <include name="IDR_SET_ICON_JS" file="set_icon.js" type="BINDATA" /> <include name="IDR_BROWSER_TEST_ENVIRONMENT_SPECIFIC_BINDINGS_JS" file="browser_test_environment_specific_bindings.js" type="BINDATA" /> diff --git a/extensions/renderer/resources/serial_custom_bindings.js b/extensions/renderer/resources/serial_custom_bindings.js index f0af8da..8c943ed 100644 --- a/extensions/renderer/resources/serial_custom_bindings.js +++ b/extensions/renderer/resources/serial_custom_bindings.js @@ -93,8 +93,8 @@ binding.registerCustomHook(function(bindingsAPI) { return serialService.getConnections(); }).then(function(connections) { var promises = []; - for (var id in connections) { - promises.push(connections[id].getInfo()); + for (var connection of connections.values()) { + promises.push(connection.getInfo()); } return Promise.all(promises); }); diff --git a/extensions/renderer/resources/serial_service.js b/extensions/renderer/resources/serial_service.js index 0e0327d..aa53eb4 100644 --- a/extensions/renderer/resources/serial_service.js +++ b/extensions/renderer/resources/serial_service.js @@ -7,12 +7,14 @@ define('serial_service', [ 'data_receiver', 'data_sender', 'device/serial/serial.mojom', + 'device/serial/serial_serialization.mojom', 'mojo/public/js/bindings/core', 'mojo/public/js/bindings/router', ], function(serviceProvider, dataReceiver, dataSender, serialMojom, + serialization, core, routerModule) { /** @@ -42,14 +44,6 @@ define('serial_service', [ }); } - var DEFAULT_CLIENT_OPTIONS = { - persistent: false, - name: '', - receiveTimeout: 0, - sendTimeout: 0, - bufferSize: 4096, - }; - var DATA_BITS_TO_MOJO = { undefined: serialMojom.DataBits.NONE, 'seven': serialMojom.DataBits.SEVEN, @@ -126,39 +120,66 @@ define('serial_service', [ }; } - function Connection( - remoteConnection, router, receivePipe, sendPipe, id, options) { - this.remoteConnection_ = remoteConnection; - this.router_ = router; - this.options_ = {}; - for (var key in DEFAULT_CLIENT_OPTIONS) { - this.options_[key] = DEFAULT_CLIENT_OPTIONS[key]; - } - this.setClientOptions_(options); - this.receivePipe_ = - new dataReceiver.DataReceiver(receivePipe, - this.options_.bufferSize, - serialMojom.ReceiveError.DISCONNECTED); - this.sendPipe_ = new dataSender.DataSender( - sendPipe, this.options_.bufferSize, serialMojom.SendError.DISCONNECTED); - this.id_ = id; - getConnections().then(function(connections) { - connections[this.id_] = this; - }.bind(this)); - this.paused_ = false; - this.sendInProgress_ = false; + // Update client-side options |clientOptions| from the user-provided + // |options|. + function updateClientOptions(clientOptions, options) { + if ('name' in options) + clientOptions.name = options.name; + if ('receiveTimeout' in options) + clientOptions.receiveTimeout = options.receiveTimeout; + if ('sendTimeout' in options) + clientOptions.sendTimeout = options.sendTimeout; + if ('bufferSize' in options) + clientOptions.bufferSize = options.bufferSize; + }; - // queuedReceiveData_ or queuedReceiveError will store the receive result or - // error, respectively, if a receive completes or fails while this + function Connection(connection, router, receivePipe, sendPipe, id, options) { + var state = new serialization.ConnectionState(); + state.connectionId = id; + updateClientOptions(state, options); + var receiver = new dataReceiver.DataReceiver( + receivePipe, state.bufferSize, serialMojom.ReceiveError.DISCONNECTED); + var sender = new dataSender.DataSender( + sendPipe, state.bufferSize, serialMojom.SendError.DISCONNECTED); + this.init_(state, + connection, + router, + receiver, + sender, + null, + serialMojom.ReceiveError.NONE); + connections_.set(id, this); + this.startReceive_(); + } + + // Initializes this Connection from the provided args. + Connection.prototype.init_ = function(state, + connection, + router, + receiver, + sender, + queuedReceiveData, + queuedReceiveError) { + this.state_ = state; + + // queuedReceiveData_ or queuedReceiveError_ will store the receive result + // or error, respectively, if a receive completes or fails while this // connection is paused. At most one of the the two may be non-null: a // receive completed while paused will only set one of them, no further // receives will be performed while paused and a queued result is dispatched // before any further receives are initiated when unpausing. - this.queuedReceiveData_ = null; - this.queuedReceiveError = null; - - this.startReceive_(); - } + if (queuedReceiveError != serialMojom.ReceiveError.NONE) + this.queuedReceiveError_ = {error: queuedReceiveError}; + if (queuedReceiveData) { + this.queuedReceiveData_ = new ArrayBuffer(queuedReceiveData.length); + new Int8Array(this.queuedReceiveData_).set(queuedReceiveData); + } + this.router_ = router; + this.remoteConnection_ = connection; + this.receivePipe_ = receiver; + this.sendPipe_ = sender; + this.sendInProgress_ = false; + }; Connection.create = function(path, options) { options = options || {}; @@ -206,21 +227,20 @@ define('serial_service', [ this.sendPipe_.close(); clearTimeout(this.receiveTimeoutId_); clearTimeout(this.sendTimeoutId_); - return getConnections().then(function(connections) { - delete connections[this.id_]; - return true; - }.bind(this)); + connections_.delete(this.state_.connectionId); + return true; }; Connection.prototype.getClientInfo_ = function() { - var info = { - connectionId: this.id_, - paused: this.paused_, + return { + connectionId: this.state_.connectionId, + paused: this.state_.paused, + persistent: this.state_.persistent, + name: this.state_.name, + receiveTimeout: this.state_.receiveTimeout, + sendTimeout: this.state_.sendTimeout, + bufferSize: this.state_.bufferSize, }; - for (var key in this.options_) { - info[key] = this.options_[key]; - } - return info; }; Connection.prototype.getInfo = function() { @@ -236,19 +256,8 @@ define('serial_service', [ }); }; - Connection.prototype.setClientOptions_ = function(options) { - if ('name' in options) - this.options_.name = options.name; - if ('receiveTimeout' in options) - this.options_.receiveTimeout = options.receiveTimeout; - if ('sendTimeout' in options) - this.options_.sendTimeout = options.sendTimeout; - if ('bufferSize' in options) - this.options_.bufferSize = options.bufferSize; - }; - Connection.prototype.setOptions = function(options) { - this.setClientOptions_(options); + updateClientOptions(this.state_, options); var serviceOptions = getServiceOptions(options); if ($Object.keys(serviceOptions).length == 0) return true; @@ -297,7 +306,7 @@ define('serial_service', [ }; Connection.prototype.setPaused = function(paused) { - this.paused_ = paused; + this.state_.paused = paused; if (paused) { clearTimeout(this.receiveTimeoutId_); this.receiveTimeoutId_ = null; @@ -310,10 +319,10 @@ define('serial_service', [ if (this.sendInProgress_) return Promise.resolve({bytesSent: 0, error: 'pending'}); - if (this.options_.sendTimeout) { + if (this.state_.sendTimeout) { this.sendTimeoutId_ = setTimeout(function() { this.sendPipe_.cancel(serialMojom.SendError.TIMEOUT); - }.bind(this), this.options_.sendTimeout); + }.bind(this), this.state_.sendTimeout); } this.sendInProgress_ = true; return this.sendPipe_.send(data).then(function(bytesSent) { @@ -340,9 +349,9 @@ define('serial_service', [ if (this.queuedReceiveData_) { receivePromise = Promise.resolve(this.queuedReceiveData_); this.queuedReceiveData_ = null; - } else if (this.queuedReceiveError) { - receivePromise = Promise.reject(this.queuedReceiveError); - this.queuedReceiveError = null; + } else if (this.queuedReceiveError_) { + receivePromise = Promise.reject(this.queuedReceiveError_); + this.queuedReceiveError_ = null; } else { receivePromise = this.receivePipe_.receive(); } @@ -354,14 +363,14 @@ define('serial_service', [ Connection.prototype.onDataReceived_ = function(data) { this.startReceiveTimeoutTimer_(); this.receiveInProgress_ = false; - if (this.paused_) { + if (this.state_.paused) { this.queuedReceiveData_ = data; return; } if (this.onData) { this.onData(data); } - if (!this.paused_) { + if (!this.state_.paused) { this.startReceive_(); } }; @@ -369,21 +378,21 @@ define('serial_service', [ Connection.prototype.onReceiveError_ = function(e) { clearTimeout(this.receiveTimeoutId_); this.receiveInProgress_ = false; - if (this.paused_) { - this.queuedReceiveError = e; + if (this.state_.paused) { + this.queuedReceiveError_ = e; return; } var error = e.error; - this.paused_ = true; + this.state_.paused = true; if (this.onError) this.onError(RECEIVE_ERROR_FROM_MOJO[error]); }; Connection.prototype.startReceiveTimeoutTimer_ = function() { clearTimeout(this.receiveTimeoutId_); - if (this.options_.receiveTimeout && !this.paused_) { + if (this.state_.receiveTimeout && !this.state_.paused) { this.receiveTimeoutId_ = setTimeout(this.onReceiveTimeout_.bind(this), - this.options_.receiveTimeout); + this.state_.receiveTimeout); } }; @@ -393,26 +402,105 @@ define('serial_service', [ this.startReceiveTimeoutTimer_(); }; - var connections_ = {}; + Connection.prototype.serialize = function() { + connections_.delete(this.state_.connectionId); + this.onData = null; + this.onError = null; + var handle = this.router_.connector_.handle_; + this.router_.connector_.handle_ = null; + this.router_.close(); + clearTimeout(this.receiveTimeoutId_); + clearTimeout(this.sendTimeoutId_); + + // Serializing receivePipe_ will cancel an in-progress receive, which would + // pause the connection, so save it ahead of time. + var paused = this.state_.paused; + return Promise.all([ + this.receivePipe_.serialize(), + this.sendPipe_.serialize(), + ]).then(function(serializedComponents) { + var queuedReceiveError = serialMojom.ReceiveError.NONE; + if (this.queuedReceiveError_) + queuedReceiveError = this.queuedReceiveError_.error; + this.state_.paused = paused; + var serialized = new serialization.SerializedConnection(); + serialized.state = this.state_; + serialized.queuedReceiveError = queuedReceiveError; + serialized.queuedReceiveData = + this.queuedReceiveData_ ? new Int8Array(this.queuedReceiveData_) : + null; + serialized.connection = handle; + serialized.receiver = serializedComponents[0]; + serialized.sender = serializedComponents[1]; + return serialized; + }.bind(this)); + }; + + Connection.deserialize = function(serialized) { + var serialConnection = $Object.create(Connection.prototype); + var router = new routerModule.Router(serialized.connection); + var connection = new serialMojom.ConnectionProxy(router); + var receiver = dataReceiver.DataReceiver.deserialize(serialized.receiver); + var sender = dataSender.DataSender.deserialize(serialized.sender); + + // Ensure that paused and persistent are booleans. + serialized.state.paused = !!serialized.state.paused; + serialized.state.persistent = !!serialized.state.persistent; + serialConnection.init_(serialized.state, + connection, + router, + receiver, + sender, + serialized.queuedReceiveData, + serialized.queuedReceiveError); + serialConnection.awaitingResume_ = true; + var connectionId = serialized.state.connectionId; + connections_.set(connectionId, serialConnection); + if (connectionId >= nextConnectionId_) + nextConnectionId_ = connectionId + 1; + return serialConnection; + }; + + // Resume receives on a deserialized connection. + Connection.prototype.resumeReceives = function() { + if (!this.awaitingResume_) + return; + this.awaitingResume_ = false; + if (!this.state_.paused) + this.startReceive_(); + }; + + // All accesses to connections_ and nextConnectionId_ other than those + // involved in deserialization should ensure that + // connectionDeserializationComplete_ has resolved first. + // Note: this will not immediately resolve once serial connection stashing and + // restoring is implemented. + var connectionDeserializationComplete_ = Promise.resolve(); + + // The map of connection ID to connection object. + var connections_ = new Map(); + + // The next connection ID to be allocated. var nextConnectionId_ = 0; - // Wrap all access to |connections_| through getConnections to avoid adding - // any synchronous dependencies on it. This will likely be important when - // supporting persistent connections by stashing them. function getConnections() { - return Promise.resolve(connections_); + return connectionDeserializationComplete_.then(function() { + return new Map(connections_); + }); } function getConnection(id) { return getConnections().then(function(connections) { - if (!connections[id]) + if (!connections.has(id)) throw new Error('Serial connection not found.'); - return connections[id]; + return connections.get(id); }); } function allocateConnectionId() { - return Promise.resolve(nextConnectionId_++); + return connectionDeserializationComplete_.then(function() { + return nextConnectionId_++; + }); } return { diff --git a/extensions/test/data/serial_unittest.js b/extensions/test/data/serial_unittest.js index c3cc504..56ebee3 100644 --- a/extensions/test/data/serial_unittest.js +++ b/extensions/test/data/serial_unittest.js @@ -22,7 +22,28 @@ var BUFFER_SIZE = 10; var connectionId = null; -function connect(callback, options) { +var OPTIONS_VALUES = [ + {}, // SetPortOptions is called once during connection. + {bitrate: 57600}, + {dataBits: 'seven'}, + {dataBits: 'eight'}, + {parityBit: 'no'}, + {parityBit: 'odd'}, + {parityBit: 'even'}, + {stopBits: 'one'}, + {stopBits: 'two'}, + {ctsFlowControl: false}, + {ctsFlowControl: true}, + {bufferSize: 1}, + {sendTimeout: 0}, + {receiveTimeout: 0}, + {persistent: false}, + {name: 'name'}, +]; + +// Create a serial connection. That serial connection will be used by the other +// helper functions below. +function connect(options) { options = options || { name: 'test connection', bufferSize: BUFFER_SIZE, @@ -30,11 +51,117 @@ function connect(callback, options) { sendTimeout: 6789, persistent: true, }; - serial.connect('device', options, test.callbackPass(function(connectionInfo) { - connectionId = connectionInfo.connectionId; - if (callback) - callback(connectionInfo); - })); + return utils.promise(serial.connect, 'device', options).then(function(info) { + connectionId = info.connectionId; + return info; + }); +} + +// Serialize and deserialize all serial connections, preserving onData and +// onError event listeners. +function serializeRoundTrip() { + return requireAsync('serial_service').then(function(serialService) { + function serializeConnections(connections) { + var serializedConnections = []; + for (var connection in connections.values()) { + serializedConnections.push(serializeConnection(connection)); + } + return Promise.all(serializedConnections); + } + + function serializeConnection(connection) { + var onData = connection.onData; + var onError = connection.onError; + return connection.serialize().then(function(serialization) { + return { + serialization: serialization, + onData: onData, + onError: onError, + }; + }); + } + + function deserializeConnections(serializedConnections) { + $Array.forEach(serializedConnections, function(serializedConnection) { + var connection = serialService.Connection.deserialize( + serializedConnection.serialization); + connection.onData = serializedConnection.onData; + connection.onError = serializedConnection.onError; + connection.resumeReceives(); + }); + } + + return serialService.getConnections() + .then(serializeConnections) + .then(deserializeConnections); + }); +} + +// Returns a promise that will resolve to the connection info for the +// connection. +function getInfo() { + return utils.promise(serial.getInfo, connectionId); +} + +// Returns a function that checks that the values of keys contained within +// |expectedInfo| match the values of the same keys contained within |info|. +function checkInfo(expectedInfo) { + return function(info) { + for (var key in expectedInfo) { + test.assertEq(expectedInfo[key], info[key]); + } + }; +} + +// Returns a function that will update the options of the serial connection with +// those contained within |values|. +function update(values) { + return function() { + return utils.promise(serial.update, connectionId, values); + }; +} + +// Checks that the previous operation succeeded. +function expectSuccess(success) { + test.assertTrue(success); +} + +// Returns a function that checks that the send result matches |bytesSent| and +// |error|. If no error is expected, |error| may be omitted. +function expectSendResult(bytesSent, error) { + return function(sendInfo) { + test.assertEq(bytesSent, sendInfo.bytesSent); + test.assertEq(error, sendInfo.error); + }; +} + +// Returns a function that checks that the current time is |expectedTime|. +function expectCurrentTime(expectedTime) { + return function() { + test.assertEq(expectedTime, timeoutManager.currentTime); + } +} + +// Returns a promise that will resolve to the device control signals for the +// serial connection. +function getControlSignals() { + return utils.promise(serial.getControlSignals, connectionId); +} + +// Returns a function that will set the control signals for the serial +// connection to |signals|. +function setControlSignals(signals) { + return function() { + return utils.promise(serial.setControlSignals, connectionId, signals); + }; +} + +// Returns a function that will set the paused state of the serial connection to +// |paused|. +function setPaused(paused) { + return function() { + return utils.promise(serial.setPaused, connectionId, paused); + } } // Sets a function to be called once when data is received. Returns a promise @@ -69,11 +196,19 @@ function addReceiveErrorHook(callback) { }); } +function listenOnce(targetEvent) { + return new Promise(function(resolve, reject) { + targetEvent.addListener(function(result) { + resolve(result); + }); + }); +} + function disconnect() { - serial.disconnect(connectionId, test.callbackPass(function(success) { + return utils.promise(serial.disconnect, connectionId).then(function(success) { test.assertTrue(success); connectionId = null; - })); + }); } function checkClientConnectionInfo(connectionInfo) { @@ -96,29 +231,7 @@ function checkServiceConnectionInfo(connectionInfo) { function checkConnectionInfo(connectionInfo) { checkClientConnectionInfo(connectionInfo); checkServiceConnectionInfo(connectionInfo); -} - -function runReceiveErrorTest(expectedError) { - connect(); - test.listenOnce(serial.onReceiveError, function(result) { - serial.getInfo(connectionId, test.callbackPass(function(connectionInfo) { - disconnect(); - test.assertTrue(connectionInfo.paused); - })); - test.assertEq(connectionId, result.connectionId); - test.assertEq(expectedError, result.error); - }); -} - -function runSendErrorTest(expectedError) { - connect(function() { - var buffer = new ArrayBuffer(1); - serial.send(connectionId, buffer, test.callbackPass(function(sendInfo) { - disconnect(); - test.assertEq(0, sendInfo.bytesSent); - test.assertEq(expectedError, sendInfo.error); - })); - }); + test.assertEq(12, $Object.keys(connectionInfo).length); } function sendData() { @@ -141,11 +254,40 @@ function checkReceivedData(result) { } } +function checkReceiveError(expectedError) { + return function(result) { + test.assertEq(connectionId, result.connectionId); + test.assertEq(expectedError, result.error); + } +} + +function runReceiveErrorTest(expectedError) { + var errorReceived = listenOnce(serial.onReceiveError); + Promise.all([ + connect(), + errorReceived + .then(checkReceiveError(expectedError)), + errorReceived + .then(getInfo) + .then(checkInfo({paused: true})), + ]) + .then(disconnect) + .then(test.succeed, test.fail); +} + +function runSendErrorTest(expectedError) { + connect() + .then(sendData) + .then(expectSendResult(0, expectedError)) + .then(disconnect) + .then(test.succeed, test.fail); +} + unittestBindings.exportTests([ // Test that getDevices correctly transforms the data returned by the // SerialDeviceEnumerator. function testGetDevices() { - serial.getDevices(test.callbackPass(function(devices) { + utils.promise(serial.getDevices).then(function(devices) { test.assertEq(3, devices.length); test.assertEq(4, $Object.keys(devices[0]).length); test.assertEq('device', devices[0].path); @@ -156,7 +298,7 @@ unittestBindings.exportTests([ test.assertEq('another_device', devices[1].path); test.assertEq(1, $Object.keys(devices[2]).length); test.assertEq('', devices[2].path); - })); + }).then(test.succeed, test.fail); }, // Test that the correct error message is returned when an error occurs in @@ -183,17 +325,16 @@ unittestBindings.exportTests([ // Test that a successful connect returns the expected connection info. function testConnect() { - connect(function(connectionInfo) { - disconnect(); - checkConnectionInfo(connectionInfo); - }); + connect() + .then(checkConnectionInfo) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that a connection created with no options has the correct default // options. function testConnectDefaultOptions() { - connect(function(connectionInfo) { - disconnect(); + connect({}).then(function(connectionInfo) { test.assertEq(9600, connectionInfo.bitrate); test.assertEq('eight', connectionInfo.dataBits); test.assertEq('no', connectionInfo.parityBit); @@ -204,104 +345,107 @@ unittestBindings.exportTests([ test.assertEq(0, connectionInfo.receiveTimeout); test.assertEq(0, connectionInfo.sendTimeout); test.assertEq(4096, connectionInfo.bufferSize); - }, {}); + }) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that a getInfo call correctly converts the service-side info from the // Mojo format and returns both it and the client-side configuration. function testGetInfo() { - connect(function() { - serial.getInfo(connectionId, - test.callbackPass(function(connectionInfo) { - disconnect(); - checkConnectionInfo(connectionInfo); - })); - }); + connect() + .then(getInfo) + .then(checkConnectionInfo) + .then(disconnect) + .then(test.succeed, test.fail); + }, + + // Test that a getInfo call returns the correct info after serialization. + function testGetInfoAfterSerialization() { + connect() + .then(serializeRoundTrip) + .then(getInfo) + .then(checkConnectionInfo) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that only client-side options are returned when the service fails a // getInfo call. This test uses an IoHandler that fails GetPortInfo calls // after the initial call during connect. function testGetInfoFailToGetPortInfo() { - connect(function() { - serial.getInfo(connectionId, - test.callbackPass(function(connectionInfo) { - disconnect(); - checkClientConnectionInfo(connectionInfo); - test.assertFalse('bitrate' in connectionInfo); - test.assertFalse('dataBits' in connectionInfo); - test.assertFalse('parityBit' in connectionInfo); - test.assertFalse('stopBit' in connectionInfo); - test.assertFalse('ctsFlowControl' in connectionInfo); - })); - }); + var info = connect().then(getInfo); + Promise.all([ + info.then(function(connectionInfo) { + test.assertFalse('bitrate' in connectionInfo); + test.assertFalse('dataBits' in connectionInfo); + test.assertFalse('parityBit' in connectionInfo); + test.assertFalse('stopBit' in connectionInfo); + test.assertFalse('ctsFlowControl' in connectionInfo); + }), + info.then(checkClientConnectionInfo), + ]) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that getConnections returns an array containing the open connection. function testGetConnections() { - connect(function() { - serial.getConnections(test.callbackPass(function(connections) { - disconnect(); - test.assertEq(1, connections.length); - checkConnectionInfo(connections[0]); - })); - }); + connect().then(function() { + return utils.promise(serial.getConnections); + }).then(function(connections) { + test.assertEq(1, connections.length); + checkConnectionInfo(connections[0]); + }) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that getControlSignals correctly converts the Mojo format result. This // test uses an IoHandler that returns values matching the pattern being // tested. function testGetControlSignals() { - connect(function() { - var calls = 0; - function checkControlSignals(signals) { - if (calls == 15) { - disconnect(); - } else { - serial.getControlSignals( - connectionId, - test.callbackPass(checkControlSignals)); - } - test.assertEq(!!(calls & 1), signals.dcd); - test.assertEq(!!(calls & 2), signals.cts); - test.assertEq(!!(calls & 4), signals.ri); - test.assertEq(!!(calls & 8), signals.dsr); - calls++; - } - serial.getControlSignals(connectionId, - test.callbackPass(checkControlSignals)); - }); + function checkControlSignals(expectedBitfield) { + return function(signals) { + test.assertEq(!!(expectedBitfield & 1), signals.dcd); + test.assertEq(!!(expectedBitfield & 2), signals.cts); + test.assertEq(!!(expectedBitfield & 4), signals.ri); + test.assertEq(!!(expectedBitfield & 8), signals.dsr); + }; + } + var promiseChain = connect(); + for (var i = 0; i < 16; i++) { + promiseChain = promiseChain + .then(getControlSignals) + .then(checkControlSignals(i)); + } + promiseChain + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that setControlSignals correctly converts to the Mojo format result. // This test uses an IoHandler that returns values following the same table of // values as |signalsValues|. function testSetControlSignals() { - connect(function() { - var signalsValues = [ - {}, - {dtr: false}, - {dtr: true}, - {rts: false}, - {dtr: false, rts: false}, - {dtr: true, rts: false}, - {rts: true}, - {dtr: false, rts: true}, - {dtr: true, rts: true}, - ]; - var calls = 0; - function setControlSignals(success) { - if (calls == signalsValues.length) { - disconnect(); - } else { - serial.setControlSignals(connectionId, - signalsValues[calls++], - test.callbackPass(setControlSignals)); - } - test.assertTrue(success); - } - setControlSignals(true); - }); + var signalsValues = [ + {}, + {dtr: false}, + {dtr: true}, + {rts: false}, + {dtr: false, rts: false}, + {dtr: true, rts: false}, + {rts: true}, + {dtr: false, rts: true}, + {dtr: true, rts: true}, + ]; + var promiseChain = connect(); + for (var i = 0; i < signalsValues.length; i++) { + promiseChain = promiseChain.then(setControlSignals(signalsValues[i])); + } + promiseChain + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that update correctly passes values to the service only for @@ -309,236 +453,239 @@ unittestBindings.exportTests([ // of getInfo calls. This test uses an IoHandler that expects corresponding // ConfigurePort calls. function testUpdate() { - connect(function() { - var optionsValues = [ - {}, // SetPortOptions is called once during connection. - {bitrate: 57600}, - {dataBits: 'seven'}, - {dataBits: 'eight'}, - {parityBit: 'no'}, - {parityBit: 'odd'}, - {parityBit: 'even'}, - {stopBits: 'one'}, - {stopBits: 'two'}, - {ctsFlowControl: false}, - {ctsFlowControl: true}, - {bufferSize: 1}, - {sendTimeout: 0}, - {receiveTimeout: 0}, - {persistent: false}, - {name: 'name'}, - ]; - var calls = 0; - function checkInfo(info) { - for (var key in optionsValues[calls]) { - test.assertEq(optionsValues[calls][key], info[key]); - } - setOptions(); - } - function setOptions() { - if (++calls == optionsValues.length) { - disconnect(); - } else { - serial.update(connectionId, - optionsValues[calls], - test.callbackPass(function(success) { - serial.getInfo(connectionId, test.callbackPass(checkInfo)); - test.assertTrue(success); - })); - } - } - setOptions(); - }); + var promiseChain = connect() + .then(getInfo) + .then(checkInfo(OPTIONS_VALUES[i])); + for (var i = 1; i < OPTIONS_VALUES.length; i++) { + promiseChain = promiseChain + .then(update(OPTIONS_VALUES[i])) + .then(expectSuccess) + .then(getInfo) + .then(checkInfo(OPTIONS_VALUES[i])); + } + promiseChain + .then(disconnect) + .then(test.succeed, test.fail); + }, + + // Test that options set by update persist after serialization. + function testUpdateAcrossSerialization() { + var promiseChain = connect() + .then(serializeRoundTrip) + .then(getInfo) + .then(checkInfo(OPTIONS_VALUES[i])); + for (var i = 1; i < OPTIONS_VALUES.length; i++) { + promiseChain = promiseChain + .then(update(OPTIONS_VALUES[i])) + .then(expectSuccess) + .then(serializeRoundTrip) + .then(getInfo) + .then(checkInfo(OPTIONS_VALUES[i])); + } + promiseChain + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that passing an invalid bit-rate reslts in an error. function testUpdateInvalidBitrate() { - connect(function() { - serial.update(connectionId, - {bitrate: -1}, - test.callbackPass(function(success) { - disconnect(); - test.assertFalse(success); - })); - }); + connect() + .then(update({bitrate: -1})) + .then(function(success) { + test.assertFalse(success); + }) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test flush. This test uses an IoHandler that counts the number of flush // calls. function testFlush() { - connect(function() { - serial.flush(connectionId, test.callbackPass(function(success) { - disconnect(); - test.assertTrue(success); - })); - }); + connect().then(function() { + return utils.promise(serial.flush, connectionId); + }) + .then(expectSuccess) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that setPaused values are reflected by the results returned by getInfo // calls. function testSetPaused() { - connect(function() { - serial.setPaused(connectionId, true, test.callbackPass(function() { - serial.getInfo(connectionId, test.callbackPass(function(info) { - serial.setPaused(connectionId, false, test.callbackPass(function() { - serial.getInfo(connectionId, test.callbackPass(function(info) { - test.assertFalse(info.paused); - disconnect(); - })); - })); - test.assertTrue(info.paused); - })); - })); - }); + connect() + .then(setPaused(true)) + .then(getInfo) + .then(checkInfo({paused: true})) + .then(setPaused(false)) + .then(getInfo) + .then(checkInfo({paused: false})) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that a send and a receive correctly echoes data. This uses an // IoHandler that echoes data sent to it. function testEcho() { - connect(function() { - sendData().then(test.callbackPass(function(sendInfo) { - test.assertEq(4, sendInfo.bytesSent); - test.assertEq(undefined, sendInfo.error); - })); - test.listenOnce(serial.onReceive, function(result) { - checkReceivedData(result); - disconnect(); - }); - }); + Promise.all([ + connect() + .then(sendData) + .then(expectSendResult(4)), + listenOnce(serial.onReceive) + .then(checkReceivedData), + ]) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that a send while another send is in progress returns a pending error. function testSendDuringExistingSend() { - connect(function() { - sendData().then(test.callbackPass(function(sendInfo) { - test.assertEq(4, sendInfo.bytesSent); - test.assertEq(undefined, sendInfo.error); - disconnect(); - })); - sendData().then(test.callbackPass(function(sendInfo) { - test.assertEq(0, sendInfo.bytesSent); - test.assertEq('pending', sendInfo.error); - })); - }); + var connected = connect(); + Promise.all([ + connected + .then(sendData) + .then(expectSendResult(4)), + connected + .then(sendData) + .then(expectSendResult(0, 'pending')), + ]) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that a second send after the first finishes is successful. This uses // an IoHandler that echoes data sent to it. function testSendAfterSuccessfulSend() { - connect(function() { - sendData().then(test.callbackPass(function(sendInfo) { - test.assertEq(4, sendInfo.bytesSent); - test.assertEq(undefined, sendInfo.error); - return sendData(); - })).then(test.callbackPass(function(sendInfo) { - test.assertEq(4, sendInfo.bytesSent); - test.assertEq(undefined, sendInfo.error); - })); - // Check that the correct data is echoed twice. - test.listenOnce(serial.onReceive, function(result) { - checkReceivedData(result); - test.listenOnce(serial.onReceive, function(result) { - checkReceivedData(result); - disconnect(); - }); - }); - }); + connect() + .then(sendData) + .then(expectSendResult(4)) + .then(sendData) + .then(expectSendResult(4)) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that a second send after the first fails is successful. This uses an // IoHandler that returns system_error for only the first send. function testSendPartialSuccessWithError() { - connect(function() { - sendData().then(test.callbackPass(function(sendInfo) { - test.assertEq(2, sendInfo.bytesSent); - test.assertEq('system_error', sendInfo.error); - return sendData(); - })).then(test.callbackPass(function(sendInfo) { - test.assertEq(4, sendInfo.bytesSent); - test.assertEq(undefined, sendInfo.error); - disconnect(); - })); - }); + connect() + .then(sendData) + .then(expectSendResult(2, 'system_error')) + .then(sendData) + .then(expectSendResult(4)) + .then(disconnect) + .then(test.succeed, test.fail); + }, + + // Test that a send and a receive correctly echoes data after serialization. + function testEchoAfterSerialization() { + Promise.all([ + connect() + .then(serializeRoundTrip) + .then(sendData) + .then(expectSendResult(4)), + listenOnce(serial.onReceive).then(checkReceivedData) + ]) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that a timed-out send returns a timeout error and that changing the // send timeout during a send does not affect its timeout. This test uses an // IoHandle that never completes sends. function testSendTimeout() { - connect(function() { - sendData().then(test.callbackPass(function(sendInfo) { - test.assertEq(0, sendInfo.bytesSent); - test.assertEq('timeout', sendInfo.error); - test.assertEq(5, timeoutManager.currentTime); - disconnect(); - })); - serial.update(connectionId, {sendTimeout: 10}, test.callbackPass( - timeoutManager.run.bind(timeoutManager, 1))); - }, {sendTimeout: 5}); + var connected = connect({sendTimeout: 5}); + var sent = connected.then(sendData); + Promise.all([ + sent.then(expectSendResult(0, 'timeout')), + sent.then(expectCurrentTime(5)), + connected.then(update({sendTimeout: 10})) + .then(expectSuccess) + .then(timeoutManager.run.bind(timeoutManager, 1)), + ]) + .then(disconnect) + .then(test.succeed, test.fail); + }, + + // Test that send timeouts still function correctly after a serialization + // round trip. + function testSendTimeoutAfterSerialization() { + var connected = connect({sendTimeout: 5}).then(serializeRoundTrip); + var sent = connected.then(sendData); + Promise.all([ + sent.then(expectSendResult(0, 'timeout')), + sent.then(expectCurrentTime(5)), + connected.then(update({sendTimeout: 10})) + .then(expectSuccess) + .then(timeoutManager.run.bind(timeoutManager, 1)), + ]) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that a timed-out send returns a timeout error and that disabling the // send timeout during a send does not affect its timeout. This test uses an // IoHandle that never completes sends. function testDisableSendTimeout() { - connect(function() { - sendData().then(test.callbackPass(function(sendInfo) { - test.assertEq(0, sendInfo.bytesSent); - test.assertEq('timeout', sendInfo.error); - test.assertEq(6, timeoutManager.currentTime); - disconnect(); - })); - serial.update(connectionId, {sendTimeout: 0}, test.callbackPass( - timeoutManager.run.bind(timeoutManager, 1))); - }, {sendTimeout: 6}); + var connected = connect({sendTimeout: 5}); + var sent = connected.then(sendData); + Promise.all([ + sent.then(expectSendResult(0, 'timeout')), + sent.then(expectCurrentTime(5)), + connected.then(update({sendTimeout: 0})) + .then(expectSuccess) + .then(timeoutManager.run.bind(timeoutManager, 1)), + ]) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that data received while the connection is paused is queued and // dispatched once the connection is unpaused. function testPausedReceive() { - // Wait until the receive hook is installed, then start the test. - addReceiveHook(function() { - // Unpause the connection after the connection has queued the received - // data to ensure the queued data is dispatched when the connection is - // unpaused. - serial.setPaused(connectionId, false, test.callbackPass()); - // Check that setPaused(false) is idempotent. - serial.setPaused(connectionId, false, test.callbackPass()); - }).then(function() { - connect(function() { - // Check that setPaused(true) is idempotent. - serial.setPaused(connectionId, true, test.callbackPass()); - serial.setPaused(connectionId, true, test.callbackPass()); - }); - }); - test.listenOnce(serial.onReceive, function(result) { - checkReceivedData(result); - disconnect(); - }); + Promise.all([ + // Wait until the receive hook is installed, then start the test. + addReceiveHook(function() { + // Unpause the connection after the connection has queued the received + // data to ensure the queued data is dispatched when the connection is + // unpaused. + Promise.all([ + utils.promise(serial.setPaused, connectionId, false), + // Check that setPaused(false) is idempotent. + utils.promise(serial.setPaused, connectionId, false), + ]).catch(test.fail); + }) + .then(connect) + .then(function() { + // Check that setPaused(true) is idempotent. + return Promise.all([ + utils.promise(serial.setPaused, connectionId, true), + utils.promise(serial.setPaused, connectionId, true), + ]); + }), + listenOnce(serial.onReceive).then(checkReceivedData), + ]) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that a receive error received while the connection is paused is queued // and dispatched once the connection is unpaused. function testPausedReceiveError() { - addReceiveErrorHook(function() { - // Unpause the connection after the connection has queued the receive - // error to ensure the queued error is dispatched when the connection is - // unpaused. - serial.setPaused(connectionId, false, test.callbackPass()); - }).then(test.callbackPass(function() { - connect(function() { - serial.setPaused(connectionId, true, test.callbackPass()); - }); - })); - - test.listenOnce(serial.onReceiveError, function(result) { - serial.getInfo(connectionId, test.callbackPass(function(connectionInfo) { - disconnect(); - test.assertTrue(connectionInfo.paused); - })); - test.assertEq(connectionId, result.connectionId); - test.assertEq('device_lost', result.error); - }); + Promise.all([ + // Wait until the receive hook is installed, then start the test. + addReceiveErrorHook(function() { + // Unpause the connection after the connection has queued the received + // data to ensure the queued data is dispatched when the connection is + // unpaused. + utils.promise(serial.setPaused, connectionId, false).catch(test.fail); + }) + .then(connect) + .then(setPaused(true)), + listenOnce(serial.onReceiveError) + .then(checkReceiveError('device_lost')), + ]) + .then(disconnect) + .then(test.succeed, test.fail); serial.onReceive.addListener(function() { test.fail('unexpected onReceive event'); }); @@ -547,43 +694,61 @@ unittestBindings.exportTests([ // Test that receive timeouts trigger after the timeout time elapses and that // changing the receive timeout does not affect a wait in progress. function testReceiveTimeout() { - connect(function() { - test.listenOnce(serial.onReceiveError, function(result) { - test.assertEq(connectionId, result.connectionId); - test.assertEq('timeout', result.error); - test.assertEq(20, timeoutManager.currentTime); - serial.getInfo(connectionId, test.callbackPass( - function(connectionInfo) { - test.assertFalse(connectionInfo.paused); - disconnect(); - })); - }); - // Changing the timeout does not take effect until the current timeout - // expires or a receive completes. - serial.update(connectionId, {receiveTimeout: 10}, test.callbackPass( - timeoutManager.run.bind(timeoutManager, 1))); - }, {receiveTimeout: 20}); + var errorReceived = listenOnce(serial.onReceiveError); + Promise.all([ + errorReceived.then(checkReceiveError('timeout')), + errorReceived.then(expectCurrentTime(20)), + errorReceived + .then(getInfo) + .then(checkInfo({paused: false})), + connect({receiveTimeout: 20}) + // Changing the timeout does not take effect until the current + // timeout expires or a receive completes. + .then(update({receiveTimeout: 10})) + .then(expectSuccess) + .then(timeoutManager.run.bind(timeoutManager, 1)), + ]) + .then(disconnect) + .then(test.succeed, test.fail); + }, + + // Test that receive timeouts still function correctly after a serialization + // round trip. + function testReceiveTimeoutAfterSerialization() { + var errorReceived = listenOnce(serial.onReceiveError); + Promise.all([ + errorReceived.then(checkReceiveError('timeout')), + errorReceived.then(expectCurrentTime(20)), + errorReceived + .then(getInfo) + .then(checkInfo({paused: false})), + connect({receiveTimeout: 20}) + .then(serializeRoundTrip) + .then(timeoutManager.run.bind(timeoutManager, 1)), + ]) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that receive timeouts trigger after the timeout time elapses and that // disabling the receive timeout does not affect a wait in progress. function testDisableReceiveTimeout() { - connect(function() { - test.listenOnce(serial.onReceiveError, function(result) { - test.assertEq(connectionId, result.connectionId); - test.assertEq('timeout', result.error); - test.assertEq(30, timeoutManager.currentTime); - serial.getInfo(connectionId, test.callbackPass( - function(connectionInfo) { - disconnect(); - test.assertFalse(connectionInfo.paused); - })); - }); - // Disabling the timeout does not take effect until the current timeout - // expires or a receive completes. - serial.update(connectionId, {receiveTimeout: 0}, test.callbackPass( - timeoutManager.run.bind(timeoutManager, 1))); - }, {receiveTimeout: 30}); + var errorReceived = listenOnce(serial.onReceiveError); + Promise.all([ + errorReceived.then(checkReceiveError('timeout')), + errorReceived.then(expectCurrentTime(20)), + errorReceived + .then(getInfo) + .then(checkInfo({paused: false})), + connect({receiveTimeout: 20}) + // Disabling the timeout does not take effect until the current + // timeout expires or a receive completes. + .then(update({receiveTimeout: 0})) + .then(expectSuccess) + .then(timeoutManager.run.bind(timeoutManager, 1)), + ]) + .then(disconnect) + .then(test.succeed, test.fail); }, // Test that a receive error from the service is correctly dispatched. This |