summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--extensions/renderer/api/serial/serial_api_unittest.cc173
-rw-r--r--extensions/renderer/resources/serial_custom_bindings.js14
-rw-r--r--extensions/renderer/resources/serial_service.js180
-rw-r--r--extensions/test/data/serial_unittest.js376
-rw-r--r--extensions/test/data/unit_test_environment_specific_bindings.js99
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;