diff options
-rw-r--r-- | device/hid/hid_service.cc | 9 | ||||
-rw-r--r-- | device/hid/hid_service.h | 2 | ||||
-rw-r--r-- | extensions/browser/api/hid/hid_apitest.cc | 187 | ||||
-rw-r--r-- | extensions/shell/app_shell.gyp | 1 | ||||
-rw-r--r-- | extensions/test/data/api_test/hid/api/background.js | 363 | ||||
-rw-r--r-- | extensions/test/data/api_test/hid/api/manifest.json | 15 |
6 files changed, 576 insertions, 1 deletions
diff --git a/device/hid/hid_service.cc b/device/hid/hid_service.cc index d2ccaa3..071b396 100644 --- a/device/hid/hid_service.cc +++ b/device/hid/hid_service.cc @@ -55,7 +55,7 @@ HidService* HidService::GetInstance( #elif defined(OS_WIN) g_service = new HidServiceWin(); #endif - if (g_service != NULL) { + if (g_service != nullptr) { Destroyer* destroyer = new Destroyer(g_service); base::MessageLoop::current()->AddDestructionObserver(destroyer); } @@ -63,6 +63,13 @@ HidService* HidService::GetInstance( return g_service; } +void HidService::SetInstanceForTest(HidService* instance) { + DCHECK(!g_service); + g_service = instance; + Destroyer* destroyer = new Destroyer(g_service); + base::MessageLoop::current()->AddDestructionObserver(destroyer); +} + HidService::~HidService() { DCHECK(thread_checker_.CalledOnValidThread()); } diff --git a/device/hid/hid_service.h b/device/hid/hid_service.h index a3f7132..c01fcbc 100644 --- a/device/hid/hid_service.h +++ b/device/hid/hid_service.h @@ -27,6 +27,8 @@ class HidService { scoped_refptr<base::SingleThreadTaskRunner> file_task_runner, scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner); + static void SetInstanceForTest(HidService* instance); + // Enumerates and returns a list of device identifiers. virtual void GetDevices(std::vector<HidDeviceInfo>* devices); diff --git a/extensions/browser/api/hid/hid_apitest.cc b/extensions/browser/api/hid/hid_apitest.cc new file mode 100644 index 0000000..ad91648 --- /dev/null +++ b/extensions/browser/api/hid/hid_apitest.cc @@ -0,0 +1,187 @@ +// 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. + +#include "base/run_loop.h" +#include "base/thread_task_runner_handle.h" +#include "content/public/browser/browser_thread.h" +#include "device/hid/hid_collection_info.h" +#include "device/hid/hid_connection.h" +#include "device/hid/hid_device_info.h" +#include "device/hid/hid_service.h" +#include "device/hid/hid_usage_and_page.h" +#include "extensions/shell/test/shell_apitest.h" +#include "net/base/io_buffer.h" + +namespace extensions { + +namespace { + +using base::ThreadTaskRunnerHandle; +using content::BrowserThread; +using device::HidCollectionInfo; +using device::HidConnection; +using device::HidDeviceId; +using device::HidDeviceInfo; +using device::HidService; +using device::HidUsageAndPage; +using net::IOBuffer; + +class MockHidConnection : public HidConnection { + public: + MockHidConnection(const HidDeviceInfo& device_info) + : HidConnection(device_info) {} + + void PlatformClose() override {} + + void PlatformRead(const ReadCallback& callback) override { + const char kResult[] = "This is a HID input report."; + uint8_t report_id = device_info().has_report_id ? 1 : 0; + scoped_refptr<IOBuffer> buffer(new IOBuffer(sizeof(kResult))); + buffer->data()[0] = report_id; + memcpy(buffer->data() + 1, kResult, sizeof(kResult) - 1); + ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, true, buffer, sizeof(kResult))); + } + + void PlatformWrite(scoped_refptr<net::IOBuffer> buffer, + size_t size, + const WriteCallback& callback) override { + const char kExpected[] = "This is a HID output report."; + bool result = false; + if (size == sizeof(kExpected)) { + uint8_t report_id = buffer->data()[0]; + uint8_t expected_report_id = device_info().has_report_id ? 1 : 0; + if (report_id == expected_report_id) { + if (memcmp(buffer->data() + 1, kExpected, sizeof(kExpected) - 1) == 0) { + result = true; + } + } + } + ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, result)); + } + + void PlatformGetFeatureReport(uint8_t report_id, + const ReadCallback& callback) override { + const char kResult[] = "This is a HID feature report."; + scoped_refptr<IOBuffer> buffer(new IOBuffer(sizeof(kResult))); + size_t offset = 0; + if (device_info().has_report_id) { + buffer->data()[offset++] = report_id; + } + memcpy(buffer->data() + offset, kResult, sizeof(kResult) - 1); + ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, true, buffer, sizeof(kResult) - 1 + offset)); + } + + void PlatformSendFeatureReport(scoped_refptr<net::IOBuffer> buffer, + size_t size, + const WriteCallback& callback) override { + const char kExpected[] = "The app is setting this HID feature report."; + bool result = false; + if (size == sizeof(kExpected)) { + uint8_t report_id = buffer->data()[0]; + uint8_t expected_report_id = device_info().has_report_id ? 1 : 0; + if (report_id == expected_report_id && + memcmp(buffer->data() + 1, kExpected, sizeof(kExpected) - 1) == 0) { + result = true; + } + } + ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, result)); + } + + private: + ~MockHidConnection() {} +}; + +class MockHidService : public HidService { + public: + MockHidService() : HidService() { + { + HidDeviceInfo device_info; + device_info.device_id = "Device A"; + device_info.vendor_id = 0x18D1; + device_info.product_id = 0x58F0; + device_info.max_input_report_size = 128; + device_info.max_output_report_size = 128; + device_info.max_feature_report_size = 128; + { + HidCollectionInfo collection_info; + device_info.collections.push_back(collection_info); + } + AddDevice(device_info); + } + + { + HidDeviceInfo device_info; + device_info.device_id = "Device B"; + device_info.vendor_id = 0x18D1; + device_info.product_id = 0x58F0; + device_info.max_input_report_size = 128; + device_info.max_output_report_size = 128; + device_info.max_feature_report_size = 128; + { + HidCollectionInfo collection_info; + collection_info.usage = + HidUsageAndPage(0, HidUsageAndPage::kPageVendor); + collection_info.report_ids.insert(1); + device_info.has_report_id = true; + device_info.collections.push_back(collection_info); + } + AddDevice(device_info); + } + + { + HidDeviceInfo device_info; + device_info.device_id = "Device C"; + device_info.vendor_id = 0x18D1; + device_info.product_id = 0x58F1; + device_info.max_input_report_size = 128; + device_info.max_output_report_size = 128; + device_info.max_feature_report_size = 128; + { + HidCollectionInfo collection_info; + device_info.collections.push_back(collection_info); + } + AddDevice(device_info); + } + } + + void Connect(const HidDeviceId& device_id, + const ConnectCallback& callback) override { + const auto& device_entry = devices().find(device_id); + scoped_refptr<HidConnection> connection; + if (device_entry != devices().end()) { + connection = new MockHidConnection(device_entry->second); + } + + ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, connection)); + } +}; + +} // namespace + +class HidApiTest : public ShellApiTest { + public: + void SetUpOnMainThread() override { + ShellApiTest::SetUpOnMainThread(); + base::RunLoop run_loop; + BrowserThread::PostTaskAndReply(BrowserThread::FILE, + FROM_HERE, + base::Bind(&HidApiTest::SetUpService, this), + run_loop.QuitClosure()); + run_loop.Run(); + } + + void SetUpService() { HidService::SetInstanceForTest(new MockHidService()); } +}; + +IN_PROC_BROWSER_TEST_F(HidApiTest, HidApp) { + ASSERT_TRUE(RunAppTest("api_test/hid/api")) << message_; +} + +} // namespace extensions diff --git a/extensions/shell/app_shell.gyp b/extensions/shell/app_shell.gyp index 6a48006..d641a2e 100644 --- a/extensions/shell/app_shell.gyp +++ b/extensions/shell/app_shell.gyp @@ -195,6 +195,7 @@ # TODO(yoz): Something is off here; should this .gyp file be # in the parent directory? Test target extensions_browsertests? '../browser/api/dns/dns_apitest.cc', + '../browser/api/hid/hid_apitest.cc', '../browser/api/socket/socket_apitest.cc', '../browser/api/sockets_tcp/sockets_tcp_apitest.cc', '../browser/api/sockets_udp/sockets_udp_apitest.cc', diff --git a/extensions/test/data/api_test/hid/api/background.js b/extensions/test/data/api_test/hid/api/background.js new file mode 100644 index 0000000..7bdd1eb --- /dev/null +++ b/extensions/test/data/api_test/hid/api/background.js @@ -0,0 +1,363 @@ +// 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. + +var kInvalidDeviceId = -1; +var kInvalidConnectionId = -1; + +function getDevice(wantReportIds, callback) { + chrome.hid.getDevices({}, function (devices) { + chrome.test.assertNoLastError(); + for (var device of devices) { + chrome.test.assertTrue(device.collections.length > 0); + var foundReportId = false; + for (var collection of device.collections) { + if (collection.reportIds.length > 0) { + foundReportId = true; + } + } + if (wantReportIds == foundReportId) { + callback(device); + return; + } + } + chrome.test.fail("No appropriate device found."); + }); +} + +function openDevice(wantReportIds, callback) { + getDevice(wantReportIds, function (device) { + chrome.hid.connect(device.deviceId, function (connection) { + chrome.test.assertNoLastError(); + callback(connection.connectionId); + }); + }); +} + +function openDeviceWithReportId(callback) { + return openDevice(true, callback); +} + +function openDeviceWithoutReportId(callback) { + return openDevice(false, callback); +} + +function arrayBufferToString(buffer) { + return String.fromCharCode.apply(null, new Uint8Array(buffer)); +} + +function stringToArrayBuffer(string) { + var buffer = new ArrayBuffer(string.length); + var view = new Uint8Array(buffer); + for (var i = 0; i < string.length; i++) { + view[i] = string.charCodeAt(i); + } + return buffer; +} + +function testGetDevicesWithNoOptions() { + chrome.hid.getDevices({}, function (devices) { + chrome.test.assertNoLastError(); + chrome.test.assertEq(2, devices.length, "Expected two enumerated devices."); + chrome.test.succeed("Device enumeration successful."); + }); +}; + +function testGetDevicesWithLegacyVidAndPid() { + chrome.hid.getDevices({ + 'vendorId': 0x18D1, + 'productId': 0x58F0 + }, function (devices) { + chrome.test.assertNoLastError(); + chrome.test.assertEq(2, devices.length, "Expected two enumerated devices."); + chrome.test.succeed("Device enumeration successful."); + }); +}; + +function testGetDevicesWithNoFilters() { + chrome.hid.getDevices({ 'filters': [] }, function (devices) { + chrome.test.assertNoLastError(); + chrome.test.assertEq(2, devices.length, "Expected two enumerated devices."); + chrome.test.succeed("Device enumeration successful."); + }); +}; + +function testGetDevicesWithVidPidFilter() { + chrome.hid.getDevices({ 'filters': [ + { 'vendorId': 0x18D1, 'productId': 0x58F0} + ] }, function (devices) { + chrome.test.assertNoLastError(); + chrome.test.assertEq(2, devices.length, "Expected two enumerated devices."); + chrome.test.succeed("Device enumeration successful."); + }); +}; + +function testGetDevicesWithUsageFilter() { + chrome.hid.getDevices({ 'filters': [ + { 'usagePage': 0xFF00 } /* vendor-specified usage page */ + ] }, function (devices) { + chrome.test.assertNoLastError(); + chrome.test.assertEq(1, devices.length, "Expected one enumerated device."); + var device = devices[0]; + chrome.test.assertEq(1, device.collections.length, + "Expected one collection."); + var collection = device.collections[0]; + chrome.test.assertEq(0xFF00, collection.usagePage); + chrome.test.succeed("Device enumeration successful."); + }); +} + +function testGetDevicesWithUnauthorizedDevice() { + chrome.hid.getDevices({ 'filters': [ + { 'vendorId': 0x18D1, 'productId': 0x58F1} + ] }, function (devices) { + chrome.test.assertNoLastError(); + chrome.test.assertEq(0, devices.length, "Expected no enumerated devices."); + chrome.test.succeed("Device enumeration successful."); + }); +}; + +function testConnectWithInvalidDeviceId() { + chrome.hid.connect(kInvalidDeviceId, function (connection) { + chrome.test.assertLastError("Invalid HID device ID."); + chrome.test.succeed("Rejected invalid device ID."); + }); +}; + +function testConnectAndDisconnect() { + chrome.hid.getDevices({ 'filters': [ + { 'vendorId': 0x18D1, 'productId': 0x58F0 } + ] }, function (devices) { + chrome.test.assertNoLastError(); + chrome.test.assertTrue(devices.length >= 1, "Expected connectable device."); + chrome.hid.connect(devices[0].deviceId, function (connection) { + chrome.test.assertNoLastError(); + chrome.hid.disconnect(connection.connectionId, function () { + chrome.test.assertNoLastError(); + chrome.test.succeed("Opened and closed device."); + }); + }); + }); +}; + +function testDisconnectWithInvalidConnectionId() { + chrome.hid.disconnect(kInvalidConnectionId, function () { + chrome.test.assertLastError("Connection not established."); + chrome.test.succeed("Rejected invalid connection ID."); + }); +} + +function testReceiveWithInvalidConnectionId() { + chrome.hid.receive(kInvalidConnectionId, function (reportId, data) { + chrome.test.assertLastError("Connection not established."); + chrome.test.succeed("Rejected invalid connection ID."); + }); +} + +function testReceiveWithReportId() { + openDeviceWithReportId(function (connection) { + chrome.hid.receive(connection, function (reportId, data) { + chrome.test.assertEq(1, reportId, "Expected report_id == 1."); + var expected = "This is a HID input report."; + chrome.test.assertEq(expected, arrayBufferToString(data)); + chrome.test.succeed("Receive successful."); + }); + }); +} + +function testReceiveWithoutReportId() { + openDeviceWithoutReportId(function (connection) { + chrome.hid.receive(connection, function (reportId, data) { + chrome.test.assertNoLastError(); + chrome.test.assertEq(0, reportId, "Expected report_id == 0."); + var expected = "This is a HID input report."; + chrome.test.assertEq(expected, arrayBufferToString(data)); + chrome.test.succeed("Receive successful."); + }); + }); +} + +function testSendWithInvalidConnectionId() { + var buffer = new ArrayBuffer(); + chrome.hid.send(kInvalidConnectionId, 0, buffer, function () { + chrome.test.assertLastError("Connection not established."); + chrome.test.succeed("Rejected invalid connection ID."); + }); +} + +function testSendWithReportId() { + openDeviceWithReportId(function (connection) { + var buffer = stringToArrayBuffer("This is a HID output report."); + chrome.hid.send(connection, 1, buffer, function () { + chrome.test.assertNoLastError(); + chrome.hid.disconnect(connection); + chrome.test.succeed("Send successful."); + }); + }); +} + +function testSendWithoutReportId() { + openDeviceWithoutReportId(function (connection) { + var buffer = stringToArrayBuffer("This is a HID output report."); + chrome.hid.send(connection, 0, buffer, function () { + chrome.test.assertNoLastError(); + chrome.hid.disconnect(connection); + chrome.test.succeed("Send successful."); + }); + }); +} + +function testSendWithInvalidReportId() { + openDeviceWithReportId(function (connection) { + var buffer = stringToArrayBuffer("This is a HID output report."); + chrome.hid.send(connection, 0, buffer, function () { + chrome.test.assertLastError("Transfer failed."); + chrome.hid.disconnect(connection); + chrome.test.succeed("Caught invalid report ID."); + }); + }); +} + +function testSendWithUnexpectedReportId() { + openDeviceWithoutReportId(function (connection) { + var buffer = stringToArrayBuffer("This is a HID output report."); + chrome.hid.send(connection, 1, buffer, function () { + chrome.test.assertLastError("Transfer failed."); + chrome.hid.disconnect(connection); + chrome.test.succeed("Caught unexpected report ID."); + }); + }); +} + +function testReceiveFeatureReportWithInvalidConnectionId() { + chrome.hid.receiveFeatureReport(kInvalidConnectionId, 0, function (data) { + chrome.test.assertLastError("Connection not established."); + chrome.test.succeed("Rejected invalid connection ID."); + }); +} + +function testReceiveFeatureReportWithReportId() { + openDeviceWithReportId(function (connection) { + chrome.hid.receiveFeatureReport(connection, 1, function (data) { + chrome.test.assertNoLastError(); + var expected = "\1This is a HID feature report."; + chrome.test.assertEq(expected, arrayBufferToString(data)); + chrome.test.succeed("Received feature report."); + }); + }); +} + +function testReceiveFeatureReportWithoutReportId() { + openDeviceWithoutReportId(function (connection) { + chrome.hid.receiveFeatureReport(connection, 0, function (data) { + chrome.test.assertNoLastError(); + var expected = "This is a HID feature report."; + chrome.test.assertEq(expected, arrayBufferToString(data)); + chrome.test.succeed("Received feature report."); + }); + }); +} + +function testReceiveFeatureReportWithInvalidReportId() { + openDeviceWithReportId(function (connection) { + chrome.hid.receiveFeatureReport(connection, 0, function (data) { + chrome.test.assertLastError("Transfer failed."); + chrome.test.succeed("Caught invalid report ID."); + }); + }); +} + +function testReceiveFeatureReportWithUnexpectedReportId() { + openDeviceWithoutReportId(function (connection) { + chrome.hid.receiveFeatureReport(connection, 1, function (data) { + chrome.test.assertLastError("Transfer failed."); + chrome.test.succeed("Caught unexpected report ID."); + }); + }); +} + +function testSendFeatureReportWithInvalidConnectionId() { + var buffer = new ArrayBuffer(); + chrome.hid.sendFeatureReport(kInvalidConnectionId, 0, buffer, function () { + chrome.test.assertLastError("Connection not established."); + chrome.test.succeed("Rejected invalid connection ID."); + }); +} + +function testSendFeatureReportWithReportId() { + openDeviceWithReportId(function (connection) { + var buffer = + stringToArrayBuffer("The app is setting this HID feature report."); + chrome.hid.sendFeatureReport(connection, 1, buffer, function () { + chrome.test.assertNoLastError(); + chrome.hid.disconnect(connection); + chrome.test.succeed("Send successful."); + }); + }); +} + +function testSendFeatureReportWithoutReportId() { + openDeviceWithoutReportId(function (connection) { + var buffer = + stringToArrayBuffer("The app is setting this HID feature report."); + chrome.hid.sendFeatureReport(connection, 0, buffer, function () { + chrome.test.assertNoLastError(); + chrome.hid.disconnect(connection); + chrome.test.succeed("Send successful."); + }); + }); +} + +function testSendFeatureReportWithInvalidReportId() { + openDeviceWithReportId(function (connection) { + var buffer = + stringToArrayBuffer("The app is setting this HID feature report."); + chrome.hid.sendFeatureReport(connection, 0, buffer, function () { + chrome.test.assertLastError("Transfer failed."); + chrome.hid.disconnect(connection); + chrome.test.succeed("Caught invalid report ID."); + }); + }); +} + +function testSendFeatureReportWithUnexpectedReportId() { + openDeviceWithoutReportId(function (connection) { + var buffer = + stringToArrayBuffer("The app is setting this HID feature report."); + chrome.hid.sendFeatureReport(connection, 1, buffer, function () { + chrome.test.assertLastError("Transfer failed."); + chrome.hid.disconnect(connection); + chrome.test.succeed("Caught unexpected report ID."); + }); + }); +} + +chrome.test.runTests([ + testGetDevicesWithNoOptions, + testGetDevicesWithLegacyVidAndPid, + testGetDevicesWithNoFilters, + testGetDevicesWithVidPidFilter, + testGetDevicesWithUsageFilter, + testGetDevicesWithUnauthorizedDevice, + testConnectWithInvalidDeviceId, + testConnectAndDisconnect, + testDisconnectWithInvalidConnectionId, + testReceiveWithInvalidConnectionId, + testReceiveWithReportId, + testReceiveWithoutReportId, + testSendWithInvalidConnectionId, + testSendWithReportId, + testSendWithoutReportId, + testSendWithInvalidReportId, + testSendWithUnexpectedReportId, + testReceiveFeatureReportWithInvalidConnectionId, + testReceiveFeatureReportWithReportId, + testReceiveFeatureReportWithoutReportId, + testReceiveFeatureReportWithInvalidReportId, + testReceiveFeatureReportWithUnexpectedReportId, + testSendFeatureReportWithInvalidConnectionId, + testSendFeatureReportWithReportId, + testSendFeatureReportWithoutReportId, + testSendFeatureReportWithInvalidReportId, + testSendFeatureReportWithUnexpectedReportId, +]); diff --git a/extensions/test/data/api_test/hid/api/manifest.json b/extensions/test/data/api_test/hid/api/manifest.json new file mode 100644 index 0000000..e17391b --- /dev/null +++ b/extensions/test/data/api_test/hid/api/manifest.json @@ -0,0 +1,15 @@ +{ + "name": "chrome.hid", + "version": "0.1", + "description": "end-to-end browser tests for chrome.hid API", + "app": { + "background": { + "scripts": ["background.js"] + } + }, + "permissions": [ + "hid", + // This is a test device emulated by the mocks enabled for the test. + { "usbDevices": [{ "vendorId": 6353, "productId": 22768 }]} + ] +} |