diff options
-rw-r--r-- | extensions/renderer/api/serial/serial_api_unittest.cc | 173 | ||||
-rw-r--r-- | extensions/renderer/resources/serial_custom_bindings.js | 14 | ||||
-rw-r--r-- | extensions/renderer/resources/serial_service.js | 180 | ||||
-rw-r--r-- | extensions/test/data/serial_unittest.js | 376 | ||||
-rw-r--r-- | extensions/test/data/unit_test_environment_specific_bindings.js | 99 |
5 files changed, 813 insertions, 29 deletions
diff --git a/extensions/renderer/api/serial/serial_api_unittest.cc b/extensions/renderer/api/serial/serial_api_unittest.cc index 14e3156..a5690eb 100644 --- a/extensions/renderer/api/serial/serial_api_unittest.cc +++ b/extensions/renderer/api/serial/serial_api_unittest.cc @@ -8,6 +8,11 @@ #include "extensions/renderer/api_test_base.h" #include "grit/extensions_renderer_resources.h" +// A test launcher for tests for the serial API defined in +// extensions/test/data/serial_unittest.js. Each C++ test function sets up a +// fake DeviceEnumerator or SerialIoHandler expecting or returning particular +// values for that test. + namespace extensions { namespace { @@ -298,6 +303,91 @@ class FailToGetInfoTestIoHandler : public TestIoHandlerBase { DISALLOW_COPY_AND_ASSIGN(FailToGetInfoTestIoHandler); }; +class SendErrorTestIoHandler : public TestIoHandlerBase { + public: + explicit SendErrorTestIoHandler(device::serial::SendError error) + : error_(error) {} + + virtual void WriteImpl() OVERRIDE { QueueWriteCompleted(0, error_); } + + private: + virtual ~SendErrorTestIoHandler() {} + + device::serial::SendError error_; + + DISALLOW_COPY_AND_ASSIGN(SendErrorTestIoHandler); +}; + +class FixedDataReceiveTestIoHandler : public TestIoHandlerBase { + public: + explicit FixedDataReceiveTestIoHandler(const std::string& data) + : data_(data) {} + + virtual void ReadImpl() OVERRIDE { + if (pending_read_buffer_len() < data_.size()) + return; + memcpy(pending_read_buffer(), data_.c_str(), data_.size()); + QueueReadCompleted(static_cast<uint32_t>(data_.size()), + device::serial::RECEIVE_ERROR_NONE); + } + + private: + virtual ~FixedDataReceiveTestIoHandler() {} + + const std::string data_; + + DISALLOW_COPY_AND_ASSIGN(FixedDataReceiveTestIoHandler); +}; + +class ReceiveErrorTestIoHandler : public TestIoHandlerBase { + public: + explicit ReceiveErrorTestIoHandler(device::serial::ReceiveError error) + : error_(error) {} + + virtual void ReadImpl() OVERRIDE { QueueReadCompleted(0, error_); } + + private: + virtual ~ReceiveErrorTestIoHandler() {} + + device::serial::ReceiveError error_; + + DISALLOW_COPY_AND_ASSIGN(ReceiveErrorTestIoHandler); +}; + +class SendDataWithErrorIoHandler : public TestIoHandlerBase { + public: + SendDataWithErrorIoHandler() : sent_error_(false) {} + virtual void WriteImpl() OVERRIDE { + if (sent_error_) { + WriteCompleted(pending_write_buffer_len(), + device::serial::SEND_ERROR_NONE); + return; + } + sent_error_ = true; + // We expect the JS test code to send a 4 byte buffer. + ASSERT_LT(2u, pending_write_buffer_len()); + WriteCompleted(2, device::serial::SEND_ERROR_SYSTEM_ERROR); + } + + private: + virtual ~SendDataWithErrorIoHandler() {} + + bool sent_error_; + + DISALLOW_COPY_AND_ASSIGN(SendDataWithErrorIoHandler); +}; + +class BlockSendsForeverSendIoHandler : public TestIoHandlerBase { + public: + BlockSendsForeverSendIoHandler() {} + virtual void WriteImpl() OVERRIDE {} + + private: + virtual ~BlockSendsForeverSendIoHandler() {} + + DISALLOW_COPY_AND_ASSIGN(BlockSendsForeverSendIoHandler); +}; + } // namespace class SerialApiTest : public ApiTestBase { @@ -306,6 +396,9 @@ class SerialApiTest : public ApiTestBase { virtual void SetUp() OVERRIDE { ApiTestBase::SetUp(); + env()->RegisterModule("async_waiter", IDR_ASYNC_WAITER_JS); + env()->RegisterModule("data_receiver", IDR_DATA_RECEIVER_JS); + env()->RegisterModule("data_sender", IDR_DATA_SENDER_JS); env()->RegisterModule("serial", IDR_SERIAL_CUSTOM_BINDINGS_JS); env()->RegisterModule("serial_service", IDR_SERIAL_SERVICE_JS); env()->RegisterModule("device/serial/data_stream.mojom", @@ -418,6 +511,82 @@ TEST_F(SerialApiTest, SetPaused) { RunTest("serial_unittest.js", "testSetPaused"); } +TEST_F(SerialApiTest, Echo) { + RunTest("serial_unittest.js", "testEcho"); +} + +TEST_F(SerialApiTest, SendDuringExistingSend) { + RunTest("serial_unittest.js", "testSendDuringExistingSend"); +} + +TEST_F(SerialApiTest, SendAfterSuccessfulSend) { + RunTest("serial_unittest.js", "testSendAfterSuccessfulSend"); +} + +TEST_F(SerialApiTest, SendPartialSuccessWithError) { + io_handler_ = new SendDataWithErrorIoHandler(); + RunTest("serial_unittest.js", "testSendPartialSuccessWithError"); +} + +TEST_F(SerialApiTest, SendTimeout) { + io_handler_ = new BlockSendsForeverSendIoHandler(); + RunTest("serial_unittest.js", "testSendTimeout"); +} + +TEST_F(SerialApiTest, DisableSendTimeout) { + io_handler_ = new BlockSendsForeverSendIoHandler(); + RunTest("serial_unittest.js", "testDisableSendTimeout"); +} + +TEST_F(SerialApiTest, PausedReceive) { + io_handler_ = new FixedDataReceiveTestIoHandler("data"); + RunTest("serial_unittest.js", "testPausedReceive"); +} + +TEST_F(SerialApiTest, PausedReceiveError) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::RECEIVE_ERROR_DEVICE_LOST); + RunTest("serial_unittest.js", "testPausedReceiveError"); +} + +TEST_F(SerialApiTest, ReceiveTimeout) { + RunTest("serial_unittest.js", "testReceiveTimeout"); +} + +TEST_F(SerialApiTest, DisableReceiveTimeout) { + RunTest("serial_unittest.js", "testDisableReceiveTimeout"); +} + +TEST_F(SerialApiTest, ReceiveErrorDisconnected) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::RECEIVE_ERROR_DISCONNECTED); + RunTest("serial_unittest.js", "testReceiveErrorDisconnected"); +} + +TEST_F(SerialApiTest, ReceiveErrorDeviceLost) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::RECEIVE_ERROR_DEVICE_LOST); + RunTest("serial_unittest.js", "testReceiveErrorDeviceLost"); +} + +TEST_F(SerialApiTest, ReceiveErrorSystemError) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::RECEIVE_ERROR_SYSTEM_ERROR); + RunTest("serial_unittest.js", "testReceiveErrorSystemError"); +} + +TEST_F(SerialApiTest, SendErrorDisconnected) { + io_handler_ = + new SendErrorTestIoHandler(device::serial::SEND_ERROR_DISCONNECTED); + RunTest("serial_unittest.js", "testSendErrorDisconnected"); +} + +TEST_F(SerialApiTest, SendErrorSystemError) { + io_handler_ = + new SendErrorTestIoHandler(device::serial::SEND_ERROR_SYSTEM_ERROR); + RunTest("serial_unittest.js", "testSendErrorSystemError"); +} + TEST_F(SerialApiTest, DisconnectUnknownConnectionId) { RunTest("serial_unittest.js", "testDisconnectUnknownConnectionId"); } @@ -446,4 +615,8 @@ TEST_F(SerialApiTest, SetPausedUnknownConnectionId) { RunTest("serial_unittest.js", "testSetPausedUnknownConnectionId"); } +TEST_F(SerialApiTest, SendUnknownConnectionId) { + RunTest("serial_unittest.js", "testSendUnknownConnectionId"); +} + } // namespace extensions diff --git a/extensions/renderer/resources/serial_custom_bindings.js b/extensions/renderer/resources/serial_custom_bindings.js index 1d107c0..f0af8da 100644 --- a/extensions/renderer/resources/serial_custom_bindings.js +++ b/extensions/renderer/resources/serial_custom_bindings.js @@ -13,6 +13,7 @@ var binding = require('binding').Binding.create('serial'); var context = requireNative('v8_context'); +var eventBindings = require('event_bindings'); var utils = require('utils'); var serialServicePromise = function() { @@ -48,13 +49,22 @@ binding.registerCustomHook(function(bindingsAPI) { apiFunctions.setHandleRequestWithPromise('getDevices', function() { return serialServicePromise.then(function(serialService) { return serialService.getDevices(); - }) + }); }); apiFunctions.setHandleRequestWithPromise('connect', function(path, options) { return serialServicePromise.then(function(serialService) { return serialService.createConnection(path, options); }).then(function(result) { + var id = result.info.connectionId; + result.connection.onData = function(data) { + eventBindings.dispatchEvent( + 'serial.onReceive', [{connectionId: id, data: data}]); + }; + result.connection.onError = function(error) { + eventBindings.dispatchEvent( + 'serial.onReceiveError', [{connectionId: id, error: error}]); + }; return result.info; }).catch (function(e) { throw new Error('Failed to connect to the port.'); @@ -75,6 +85,8 @@ binding.registerCustomHook(function(bindingsAPI) { 'flush', forwardToConnection('flush')); apiFunctions.setHandleRequestWithPromise( 'setPaused', forwardToConnection('setPaused')); + apiFunctions.setHandleRequestWithPromise( + 'send', forwardToConnection('send')); apiFunctions.setHandleRequestWithPromise('getConnections', function() { return serialServicePromise.then(function(serialService) { diff --git a/extensions/renderer/resources/serial_service.js b/extensions/renderer/resources/serial_service.js index b8b0970..60b0afd 100644 --- a/extensions/renderer/resources/serial_service.js +++ b/extensions/renderer/resources/serial_service.js @@ -4,10 +4,17 @@ define('serial_service', [ 'content/public/renderer/service_provider', + 'data_receiver', + 'data_sender', 'device/serial/serial.mojom', 'mojo/public/js/bindings/core', 'mojo/public/js/bindings/router', -], function(serviceProvider, serialMojom, core, routerModule) { +], function(serviceProvider, + dataReceiver, + dataSender, + serialMojom, + core, + routerModule) { /** * A Javascript client for the serial service and connection Mojo services. * @@ -59,6 +66,20 @@ define('serial_service', [ 'odd': serialMojom.ParityBit.ODD, 'even': serialMojom.ParityBit.EVEN, }; + var SEND_ERROR_TO_MOJO = { + undefined: serialMojom.SendError.NONE, + 'disconnected': serialMojom.SendError.DISCONNECTED, + 'pending': serialMojom.SendError.PENDING, + 'timeout': serialMojom.SendError.TIMEOUT, + 'system_error': serialMojom.SendError.SYSTEM_ERROR, + }; + var RECEIVE_ERROR_TO_MOJO = { + undefined: serialMojom.ReceiveError.NONE, + 'disconnected': serialMojom.ReceiveError.DISCONNECTED, + 'device_lost': serialMojom.ReceiveError.DEVICE_LOST, + 'timeout': serialMojom.ReceiveError.TIMEOUT, + 'system_error': serialMojom.ReceiveError.SYSTEM_ERROR, + }; function invertMap(input) { var output = {}; @@ -73,6 +94,8 @@ define('serial_service', [ var DATA_BITS_FROM_MOJO = invertMap(DATA_BITS_TO_MOJO); var STOP_BITS_FROM_MOJO = invertMap(STOP_BITS_TO_MOJO); var PARITY_BIT_FROM_MOJO = invertMap(PARITY_BIT_TO_MOJO); + var SEND_ERROR_FROM_MOJO = invertMap(SEND_ERROR_TO_MOJO); + var RECEIVE_ERROR_FROM_MOJO = invertMap(RECEIVE_ERROR_TO_MOJO); function getServiceOptions(options) { var out = {}; @@ -103,29 +126,44 @@ define('serial_service', [ }; } - function Connection(remoteConnection, router, id, options) { + function Connection( + remoteConnection, router, receivePipe, sendPipe, id, options) { this.remoteConnection_ = remoteConnection; this.router_ = router; - this.id_ = id; - getConnections().then(function(connections) { - connections[this.id_] = this; - }.bind(this)); - this.paused_ = false; 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; + + // 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_(); } Connection.create = function(path, options) { options = options || {}; var serviceOptions = getServiceOptions(options); var pipe = core.createMessagePipe(); - // Note: These two are created and closed because the service implementation - // requires that we provide valid message pipes for the data source and - // sink. Currently the client handles are immediately closed; the real - // implementation will come later. var sendPipe = core.createMessagePipe(); var receivePipe = core.createMessagePipe(); service.connect(path, @@ -133,21 +171,24 @@ define('serial_service', [ pipe.handle0, sendPipe.handle0, receivePipe.handle0); - core.close(sendPipe.handle1); - core.close(receivePipe.handle1); var router = new routerModule.Router(pipe.handle1); var connection = new serialMojom.ConnectionProxy(router); - return connection.getInfo().then(convertServiceInfo).then( - function(info) { + return connection.getInfo().then(convertServiceInfo).then(function(info) { return Promise.all([info, allocateConnectionId()]); }).catch(function(e) { router.close(); + core.close(sendPipe.handle1); + core.close(receivePipe.handle1); throw e; }).then(function(results) { var info = results[0]; var id = results[1]; - var serialConnectionClient = new Connection( - connection, router, id, options); + var serialConnectionClient = new Connection(connection, + router, + receivePipe.handle1, + sendPipe.handle1, + id, + options); var clientInfo = serialConnectionClient.getClientInfo_(); for (var key in clientInfo) { info[key] = clientInfo[key]; @@ -161,8 +202,12 @@ define('serial_service', [ Connection.prototype.close = function() { this.router_.close(); + this.receivePipe_.close(); + this.sendPipe_.close(); + clearTimeout(this.receiveTimeoutId_); + clearTimeout(this.sendTimeoutId_); return getConnections().then(function(connections) { - delete connections[this.id_] + delete connections[this.id_]; return true; }.bind(this)); }; @@ -171,7 +216,7 @@ define('serial_service', [ var info = { connectionId: this.id_, paused: this.paused_, - } + }; for (var key in this.options_) { info[key] = this.options_[key]; } @@ -253,6 +298,99 @@ define('serial_service', [ Connection.prototype.setPaused = function(paused) { this.paused_ = paused; + if (paused) { + clearTimeout(this.receiveTimeoutId_); + this.receiveTimeoutId_ = null; + } else if (!this.receiveInProgress_) { + this.startReceive_(); + } + }; + + Connection.prototype.send = function(data) { + if (this.sendInProgress_) + return Promise.resolve({bytesSent: 0, error: 'pending'}); + + if (this.options_.sendTimeout) { + this.sendTimeoutId_ = setTimeout(function() { + this.sendPipe_.cancel(serialMojom.SendError.TIMEOUT); + }.bind(this), this.options_.sendTimeout); + } + this.sendInProgress_ = true; + return this.sendPipe_.send(data).then(function(bytesSent) { + return {bytesSent: bytesSent}; + }).catch(function(e) { + return { + bytesSent: e.bytesSent, + error: SEND_ERROR_FROM_MOJO[e.error], + }; + }).then(function(result) { + if (this.sendTimeoutId_) + clearTimeout(this.sendTimeoutId_); + this.sendTimeoutId_ = null; + this.sendInProgress_ = false; + return result; + }.bind(this)); + }; + + Connection.prototype.startReceive_ = function() { + this.receiveInProgress_ = true; + var receivePromise = null; + // If we have a queued receive result, dispatch it immediately instead of + // starting a new receive. + if (this.queuedReceiveData_) { + receivePromise = Promise.resolve(this.queuedReceiveData_); + this.queuedReceiveData_ = null; + } else if (this.queuedReceiveError) { + receivePromise = Promise.reject(this.queuedReceiveError); + this.queuedReceiveError = null; + } else { + receivePromise = this.receivePipe_.receive(); + } + receivePromise.then(this.onDataReceived_.bind(this)).catch( + this.onReceiveError_.bind(this)); + this.startReceiveTimeoutTimer_(); + }; + + Connection.prototype.onDataReceived_ = function(data) { + this.startReceiveTimeoutTimer_(); + this.receiveInProgress_ = false; + if (this.paused_) { + this.queuedReceiveData_ = data; + return; + } + if (this.onData) { + this.onData(data); + } + if (!this.paused_) { + this.startReceive_(); + } + }; + + Connection.prototype.onReceiveError_ = function(e) { + clearTimeout(this.receiveTimeoutId_); + this.receiveInProgress_ = false; + if (this.paused_) { + this.queuedReceiveError = e; + return; + } + var error = e.error; + this.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_) { + this.receiveTimeoutId_ = setTimeout(this.onReceiveTimeout_.bind(this), + this.options_.receiveTimeout); + } + }; + + Connection.prototype.onReceiveTimeout_ = function() { + if (this.onError) + this.onError('timeout'); + this.startReceiveTimeoutTimer_(); }; var connections_ = {}; @@ -268,7 +406,7 @@ define('serial_service', [ function getConnection(id) { return getConnections().then(function(connections) { if (!connections[id]) - throw new Error ('Serial connection not found.'); + throw new Error('Serial connection not found.'); return connections[id]; }); } @@ -282,5 +420,7 @@ define('serial_service', [ createConnection: Connection.create, getConnection: getConnection, getConnections: getConnections, + // For testing. + Connection: Connection, }; }); diff --git a/extensions/test/data/serial_unittest.js b/extensions/test/data/serial_unittest.js index dc7bbea..c3cc504 100644 --- a/extensions/test/data/serial_unittest.js +++ b/extensions/test/data/serial_unittest.js @@ -6,29 +6,69 @@ * Unit tests for the JS serial service client. * * These test that configuration and data are correctly transmitted between the - * client and the service. + * client and the service. They are launched by + * extensions/renderer/api/serial/serial_api_unittest.cc. */ var test = require('test').binding; var serial = require('serial').binding; var unittestBindings = require('test_environment_specific_bindings'); +var utils = require('utils'); + +var timeoutManager = new unittestBindings.TimeoutManager(); +timeoutManager.installGlobals(); + +var BUFFER_SIZE = 10; var connectionId = null; function connect(callback, options) { options = options || { name: 'test connection', - bufferSize: 8192, + bufferSize: BUFFER_SIZE, receiveTimeout: 12345, sendTimeout: 6789, persistent: true, - } + }; serial.connect('device', options, test.callbackPass(function(connectionInfo) { connectionId = connectionInfo.connectionId; - callback(connectionInfo); + if (callback) + callback(connectionInfo); })); } +// Sets a function to be called once when data is received. Returns a promise +// that will resolve once the hook is installed. +function addReceiveHook(callback) { + return requireAsync('serial_service').then(function(serialService) { + var called = false; + var dataReceived = serialService.Connection.prototype.onDataReceived_; + serialService.Connection.prototype.onDataReceived_ = function() { + var result = $Function.apply(dataReceived, this, arguments); + if (!called) + callback(); + called = true; + return result; + }; + }); +} + +// Sets a function to be called once when a receive error is received. Returns a +// promise that will resolve once the hook is installed. +function addReceiveErrorHook(callback) { + return requireAsync('serial_service').then(function(serialService) { + var called = false; + var receiveError = serialService.Connection.prototype.onReceiveError_; + serialService.Connection.prototype.onReceiveError_ = function() { + var result = $Function.apply(receiveError, this, arguments); + if (!called) + callback(); + called = true; + return result; + }; + }); +} + function disconnect() { serial.disconnect(connectionId, test.callbackPass(function(success) { test.assertTrue(success); @@ -41,7 +81,7 @@ function checkClientConnectionInfo(connectionInfo) { test.assertEq('test connection', connectionInfo.name); test.assertEq(12345, connectionInfo.receiveTimeout); test.assertEq(6789, connectionInfo.sendTimeout); - test.assertEq(8192, connectionInfo.bufferSize); + test.assertEq(BUFFER_SIZE, connectionInfo.bufferSize); test.assertFalse(connectionInfo.paused); } @@ -58,7 +98,52 @@ function checkConnectionInfo(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); + })); + }); +} + +function sendData() { + var data = 'data'; + var buffer = new ArrayBuffer(data.length); + var byteBuffer = new Int8Array(buffer); + for (var i = 0; i < data.length; i++) { + byteBuffer[i] = data.charCodeAt(i); + } + return utils.promise(serial.send, connectionId, buffer); +} + +function checkReceivedData(result) { + var data = 'data'; + test.assertEq(connectionId, result.connectionId); + test.assertEq(data.length, result.data.byteLength); + var resultByteBuffer = new Int8Array(result.data); + for (var i = 0; i < data.length; i++) { + test.assertEq(data.charCodeAt(i), resultByteBuffer[i]); + } +} + unittestBindings.exportTests([ + // Test that getDevices correctly transforms the data returned by the + // SerialDeviceEnumerator. function testGetDevices() { serial.getDevices(test.callbackPass(function(devices) { test.assertEq(3, devices.length); @@ -74,21 +159,29 @@ unittestBindings.exportTests([ })); }, + // Test that the correct error message is returned when an error occurs in + // connecting to the port. This test uses an IoHandler that fails to connect. function testConnectFail() { serial.connect('device', test.callbackFail('Failed to connect to the port.')); }, + // Test that the correct error message is returned when an error occurs in + // calling getPortInfo after connecting to the port. This test uses an + // IoHandler that fails on calls to GetPortInfo. function testGetInfoFailOnConnect() { serial.connect('device', test.callbackFail('Failed to connect to the port.')); }, + // Test that the correct error message is returned when an invalid bit-rate + // value is passed to connect. function testConnectInvalidBitrate() { serial.connect('device', {bitrate: -1}, test.callbackFail( 'Failed to connect to the port.')); }, + // Test that a successful connect returns the expected connection info. function testConnect() { connect(function(connectionInfo) { disconnect(); @@ -96,6 +189,8 @@ unittestBindings.exportTests([ }); }, + // Test that a connection created with no options has the correct default + // options. function testConnectDefaultOptions() { connect(function(connectionInfo) { disconnect(); @@ -112,6 +207,8 @@ unittestBindings.exportTests([ }, {}); }, + // 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, @@ -122,6 +219,9 @@ unittestBindings.exportTests([ }); }, + // 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, @@ -137,6 +237,7 @@ unittestBindings.exportTests([ }); }, + // Test that getConnections returns an array containing the open connection. function testGetConnections() { connect(function() { serial.getConnections(test.callbackPass(function(connections) { @@ -147,6 +248,9 @@ unittestBindings.exportTests([ }); }, + // 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; @@ -169,6 +273,9 @@ unittestBindings.exportTests([ }); }, + // 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 = [ @@ -197,6 +304,10 @@ unittestBindings.exportTests([ }); }, + // Test that update correctly passes values to the service only for + // service-side options and that all update calls are reflected by the result + // of getInfo calls. This test uses an IoHandler that expects corresponding + // ConfigurePort calls. function testUpdate() { connect(function() { var optionsValues = [ @@ -240,6 +351,7 @@ unittestBindings.exportTests([ }); }, + // Test that passing an invalid bit-rate reslts in an error. function testUpdateInvalidBitrate() { connect(function() { serial.update(connectionId, @@ -251,16 +363,19 @@ unittestBindings.exportTests([ }); }, + // 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) { + serial.flush(connectionId, test.callbackPass(function(success) { disconnect(); test.assertTrue(success); })); }); }, + // Test that setPaused values are reflected by the results returned by getInfo + // calls. function testSetPaused() { connect(function() { serial.setPaused(connectionId, true, test.callbackPass(function() { @@ -277,36 +392,283 @@ unittestBindings.exportTests([ }); }, + // 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(); + }); + }); + }, + + // 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); + })); + }); + }, + + // 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(); + }); + }); + }); + }, + + // 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(); + })); + }); + }, + + // 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}); + }, + + // 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}); + }, + + // 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(); + }); + }, + + // 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); + }); + serial.onReceive.addListener(function() { + test.fail('unexpected onReceive event'); + }); + }, + + // 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}); + }, + + // 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}); + }, + + // Test that a receive error from the service is correctly dispatched. This + // test uses an IoHandler that only reports 'disconnected' receive errors. + function testReceiveErrorDisconnected() { + runReceiveErrorTest('disconnected'); + }, + + // Test that a receive error from the service is correctly dispatched. This + // test uses an IoHandler that only reports 'device_lost' receive errors. + function testReceiveErrorDeviceLost() { + runReceiveErrorTest('device_lost'); + }, + + // Test that a receive from error the service is correctly dispatched. This + // test uses an IoHandler that only reports 'system_error' receive errors. + function testReceiveErrorSystemError() { + runReceiveErrorTest('system_error'); + }, + + // Test that a send error from the service is correctly returned as the send + // result. This test uses an IoHandler that only reports 'disconnected' send + // errors. + function testSendErrorDisconnected() { + runSendErrorTest('disconnected'); + }, + + // Test that a send error from the service is correctly returned as the send + // result. This test uses an IoHandler that only reports 'system_error' send + // errors. + function testSendErrorSystemError() { + runSendErrorTest('system_error'); + }, + + // Test that disconnect returns the correct error for a connection ID that + // does not exist. function testDisconnectUnknownConnectionId() { serial.disconnect(-1, test.callbackFail('Serial connection not found.')); }, + // Test that getInfo returns the correct error for a connection ID that does + // not exist. function testGetInfoUnknownConnectionId() { serial.getInfo(-1, test.callbackFail('Serial connection not found.')); }, + // Test that update returns the correct error for a connection ID that does + // not exist. function testUpdateUnknownConnectionId() { serial.update(-1, {}, test.callbackFail('Serial connection not found.')); }, + // Test that setControlSignals returns the correct error for a connection ID + // that does not exist. function testSetControlSignalsUnknownConnectionId() { serial.setControlSignals(-1, {}, test.callbackFail( 'Serial connection not found.')); }, + // Test that getControlSignals returns the correct error for a connection ID + // that does not exist. function testGetControlSignalsUnknownConnectionId() { serial.getControlSignals(-1, test.callbackFail( 'Serial connection not found.')); }, + // Test that flush returns the correct error for a connection ID that does not + // exist. function testFlushUnknownConnectionId() { serial.flush(-1, test.callbackFail('Serial connection not found.')); }, + // Test that setPaused returns the correct error for a connection ID that does + // not exist. function testSetPausedUnknownConnectionId() { serial.setPaused( -1, true, test.callbackFail('Serial connection not found.')); serial.setPaused( -1, false, test.callbackFail('Serial connection not found.')); }, + + // Test that send returns the correct error for a connection ID that does not + // exist. + function testSendUnknownConnectionId() { + var buffer = new ArrayBuffer(1); + serial.send(-1, buffer, test.callbackFail('Serial connection not found.')); + }, ], test.runTests, exports); diff --git a/extensions/test/data/unit_test_environment_specific_bindings.js b/extensions/test/data/unit_test_environment_specific_bindings.js index 245d050..3636ec7 100644 --- a/extensions/test/data/unit_test_environment_specific_bindings.js +++ b/extensions/test/data/unit_test_environment_specific_bindings.js @@ -3,6 +3,7 @@ // found in the LICENSE file. var nativesPromise = requireAsync('testNatives'); +var sendRequestNatives = requireNative('sendRequest'); function registerHooks(api) { var chromeTest = api.compiledApi; @@ -41,10 +42,106 @@ function exportTests(tests, runTests, exports) { exports[test.name] = function() { runTests([test]); return true; - } + }; }); } +/** + * A fake implementation of setTimeout and clearTimeout. + * @constructor + */ +function TimeoutManager() { + this.timeouts_ = {}; + this.nextTimeoutId_ = 0; + this.currentTime = 0; + this.autorunEnabled_ = false; +} + +/** + * Installs setTimeout and clearTimeout into the global object. + */ +TimeoutManager.prototype.installGlobals = function() { + var global = sendRequestNatives.GetGlobal({}); + global.setTimeout = this.setTimeout_.bind(this); + global.clearTimeout = this.clearTimeout_.bind(this); +}; + +/** + * Starts auto-running of timeout callbacks. Until |numCallbacksToRun| callbacks + * have run, any timeout callbacks set by calls to setTimeout (including before + * the call to run) will cause the currentTime to be advanced to the time of + * the timeout. + */ +TimeoutManager.prototype.run = function(numCallbacksToRun) { + this.numCallbacksToRun_ = numCallbacksToRun; + Promise.resolve().then(this.autoRun_.bind(this)); +}; + +/** + * Runs timeout callbacks with earliest timeout. + * @private + */ +TimeoutManager.prototype.autoRun_ = function() { + if (this.numCallbacksToRun_ <= 0 || $Object.keys(this.timeouts_).length == 0) + return; + + // Bucket the timeouts by their timeout time. + var timeoutsByTimeout = {}; + var timeoutIds = $Object.keys(this.timeouts_); + for (var i = 0; i < timeoutIds.length; i++) { + var timeout = this.timeouts_[timeoutIds[i]]; + var timeMs = timeout.timeMs; + if (!timeoutsByTimeout[timeMs]) + timeoutsByTimeout[timeMs] = []; + timeoutsByTimeout[timeMs].push(timeout); + } + this.currentTime = + $Function.apply(Math.min, null, $Object.keys((timeoutsByTimeout))); + // Run all timeouts in the earliest timeout bucket. + var timeouts = timeoutsByTimeout[this.currentTime]; + for (var i = 0; i < timeouts.length; i++) { + var currentTimeout = timeouts[i]; + if (!this.timeouts_[currentTimeout.id]) + continue; + this.numCallbacksToRun_--; + delete this.timeouts_[currentTimeout.id]; + try { + currentTimeout.target(); + } catch (e) { + console.log('error calling timeout target ' + e.stack); + } + } + // Continue running any later callbacks. + Promise.resolve().then(this.autoRun_.bind(this)); +}; + +/** + * A fake implementation of setTimeout. This does not support passing callback + * arguments. + * @private + */ +TimeoutManager.prototype.setTimeout_ = function(target, timeoutMs) { + var timeoutId = this.nextTimeoutId_++; + this.timeouts_[timeoutId] = { + id: timeoutId, + target: target, + timeMs: timeoutMs + this.currentTime, + }; + if (this.autorunEnabled_) + Promise.resolve().then(this.autoRun_.bind(this)); + return timeoutId; +}; + +/** + * A fake implementation of clearTimeout. + * @private + */ +TimeoutManager.prototype.clearTimeout_ = function(timeoutId) { + if (this.timeouts_[timeoutId]) + delete this.timeouts_[timeoutId]; +}; + exports.registerHooks = registerHooks; exports.testDone = testDone; exports.exportTests = exportTests; +exports.TimeoutManager = TimeoutManager; |