blob: 592af904d6a648a36331d7e5635ff303cffeaef4 [file] [log] [blame]
// Copyright 2019 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 "services/device/hid/hid_connection_impl.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/memory/ref_counted_memory.h"
#include "build/build_config.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "services/device/device_service_test_base.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace device {
namespace {
#if defined(OS_MACOSX)
const uint64_t kTestDeviceId = 123;
#else
const char* kTestDeviceId = "123";
#endif
// The report ID to use for reports sent to or received from the test device.
const uint8_t kTestReportId = 0x42;
// The max size of input and output reports for the test device. Feature reports
// are not used in this test.
const uint64_t kMaxReportSizeBytes = 10;
// A fake HidConnection implementation that allows the test to simulate an
// input report.
class FakeHidConnection : public HidConnection {
public:
FakeHidConnection(scoped_refptr<HidDeviceInfo> device)
: HidConnection(device) {}
// HidConnection implementation.
void PlatformClose() override {}
void PlatformWrite(scoped_refptr<base::RefCountedBytes> buffer,
WriteCallback callback) override {
std::move(callback).Run(true);
}
void PlatformGetFeatureReport(uint8_t report_id,
ReadCallback callback) override {
NOTIMPLEMENTED();
}
void PlatformSendFeatureReport(scoped_refptr<base::RefCountedBytes> buffer,
WriteCallback callback) override {
NOTIMPLEMENTED();
}
void SimulateInputReport(scoped_refptr<base::RefCountedBytes> buffer) {
ProcessInputReport(buffer, buffer->size());
}
private:
~FakeHidConnection() override = default;
DISALLOW_COPY_AND_ASSIGN(FakeHidConnection);
};
// A test implementation of HidConnectionClient that signals once an input
// report has been received. The contents of the input report are saved.
class TestHidConnectionClient : public mojom::HidConnectionClient {
public:
TestHidConnectionClient() : binding_(this) {}
void Bind(mojom::HidConnectionClientRequest request) {
binding_.Bind(std::move(request));
}
// mojom::HidConnectionClient implementation.
void OnInputReport(uint8_t report_id,
const std::vector<uint8_t>& buffer) override {
report_id_ = report_id;
buffer_ = buffer;
run_loop_.Quit();
}
void WaitForInputReport() { run_loop_.Run(); }
uint8_t report_id() { return report_id_; }
const std::vector<uint8_t>& buffer() { return buffer_; }
private:
base::RunLoop run_loop_;
mojo::Binding<mojom::HidConnectionClient> binding_;
uint8_t report_id_ = 0;
std::vector<uint8_t> buffer_;
DISALLOW_COPY_AND_ASSIGN(TestHidConnectionClient);
};
// A utility for capturing the state returned by mojom::HidConnection I/O
// callbacks.
class TestIoCallback {
public:
TestIoCallback() = default;
~TestIoCallback() = default;
void SetReadResult(bool result,
uint8_t report_id,
const base::Optional<std::vector<uint8_t>>& buffer) {
result_ = result;
report_id_ = report_id;
has_buffer_ = buffer.has_value();
if (has_buffer_)
buffer_ = *buffer;
run_loop_.Quit();
}
void SetWriteResult(bool result) {
result_ = result;
run_loop_.Quit();
}
bool WaitForResult() {
run_loop_.Run();
return result_;
}
mojom::HidConnection::ReadCallback GetReadCallback() {
return base::BindOnce(&TestIoCallback::SetReadResult,
base::Unretained(this));
}
mojom::HidConnection::WriteCallback GetWriteCallback() {
return base::BindOnce(&TestIoCallback::SetWriteResult,
base::Unretained(this));
}
uint8_t report_id() { return report_id_; }
bool has_buffer() { return has_buffer_; }
const std::vector<uint8_t>& buffer() { return buffer_; }
private:
base::RunLoop run_loop_;
bool result_ = false;
uint8_t report_id_ = 0;
bool has_buffer_ = false;
std::vector<uint8_t> buffer_;
};
} // namespace
class HidConnectionImplTest : public DeviceServiceTestBase {
public:
HidConnectionImplTest() = default;
protected:
void SetUp() override {
DeviceServiceTestBase::SetUp();
base::RunLoop().RunUntilIdle();
}
void CreateHidConnection(bool with_connection_client) {
mojom::HidConnectionClientPtr hid_connection_client;
if (with_connection_client) {
connection_client_ = std::make_unique<TestHidConnectionClient>();
connection_client_->Bind(mojo::MakeRequest(&hid_connection_client));
}
fake_connection_ = new FakeHidConnection(CreateTestDevice());
hid_connection_impl_ = std::make_unique<HidConnectionImpl>(
fake_connection_, std::move(hid_connection_client));
}
scoped_refptr<HidDeviceInfo> CreateTestDevice() {
auto hid_collection_info = mojom::HidCollectionInfo::New();
hid_collection_info->usage = mojom::HidUsageAndPage::New(0, 0);
hid_collection_info->report_ids.push_back(kTestReportId);
return base::MakeRefCounted<HidDeviceInfo>(
kTestDeviceId, 0x1234, 0xabcd, "product name", "serial number",
mojom::HidBusType::kHIDBusTypeUSB, std::move(hid_collection_info),
kMaxReportSizeBytes, kMaxReportSizeBytes, 0);
}
std::vector<uint8_t> CreateTestReportBuffer(uint8_t report_id, size_t size) {
std::vector<uint8_t> buffer(size);
buffer[0] = report_id;
for (size_t i = 1; i < size; ++i)
buffer[i] = i;
return buffer;
}
std::unique_ptr<HidConnectionImpl> hid_connection_impl_;
scoped_refptr<FakeHidConnection> fake_connection_;
std::unique_ptr<TestHidConnectionClient> connection_client_;
};
TEST_F(HidConnectionImplTest, ReadWrite) {
CreateHidConnection(/*with_connection_client=*/false);
const size_t kTestBufferSize = kMaxReportSizeBytes;
std::vector<uint8_t> buffer_vec =
CreateTestReportBuffer(kTestReportId, kTestBufferSize);
// Simulate an output report (host to device).
TestIoCallback write_callback;
hid_connection_impl_->Write(kTestReportId, buffer_vec,
write_callback.GetWriteCallback());
ASSERT_TRUE(write_callback.WaitForResult());
// Simulate an input report (device to host).
auto buffer = base::MakeRefCounted<base::RefCountedBytes>(buffer_vec);
ASSERT_EQ(buffer->size(), kTestBufferSize);
fake_connection_->SimulateInputReport(buffer);
// Simulate reading the input report.
TestIoCallback read_callback;
hid_connection_impl_->Read(read_callback.GetReadCallback());
ASSERT_TRUE(read_callback.WaitForResult());
EXPECT_EQ(read_callback.report_id(), kTestReportId);
ASSERT_TRUE(read_callback.has_buffer());
const auto& read_buffer = read_callback.buffer();
ASSERT_EQ(read_buffer.size(), kTestBufferSize - 1);
for (size_t i = 1; i < kTestBufferSize; ++i) {
EXPECT_EQ(read_buffer[i - 1], buffer_vec[i])
<< "Mismatch at index " << i << ".";
}
}
TEST_F(HidConnectionImplTest, ReadWriteWithConnectionClient) {
CreateHidConnection(/*with_connection_client=*/true);
const size_t kTestBufferSize = kMaxReportSizeBytes;
std::vector<uint8_t> buffer_vec =
CreateTestReportBuffer(kTestReportId, kTestBufferSize);
// Simulate an output report (host to device).
TestIoCallback write_callback;
hid_connection_impl_->Write(kTestReportId, buffer_vec,
write_callback.GetWriteCallback());
ASSERT_TRUE(write_callback.WaitForResult());
// Simulate an input report (device to host).
auto buffer = base::MakeRefCounted<base::RefCountedBytes>(buffer_vec);
ASSERT_EQ(buffer->size(), kTestBufferSize);
fake_connection_->SimulateInputReport(buffer);
connection_client_->WaitForInputReport();
// The connection client should have been notified.
EXPECT_EQ(connection_client_->report_id(), kTestReportId);
const std::vector<uint8_t>& in_buffer = connection_client_->buffer();
ASSERT_EQ(in_buffer.size(), kTestBufferSize - 1);
for (size_t i = 1; i < kTestBufferSize; ++i) {
EXPECT_EQ(in_buffer[i - 1], buffer_vec[i])
<< "Mismatch at index " << i << ".";
}
}
TEST_F(HidConnectionImplTest, DestroyWithPendingInputReport) {
CreateHidConnection(/*with_connection_client=*/false);
const size_t kTestBufferSize = kMaxReportSizeBytes;
std::vector<uint8_t> buffer_vec =
CreateTestReportBuffer(kTestReportId, kTestBufferSize);
// Simulate an input report (device to host).
auto buffer = base::MakeRefCounted<base::RefCountedBytes>(buffer_vec);
ASSERT_EQ(buffer->size(), kTestBufferSize);
fake_connection_->SimulateInputReport(buffer);
// Destroy the connection without reading the report.
hid_connection_impl_.reset();
}
TEST_F(HidConnectionImplTest, DestroyWithPendingRead) {
CreateHidConnection(/*with_connection_client=*/false);
// Simulate reading an input report.
TestIoCallback read_callback;
hid_connection_impl_->Read(read_callback.GetReadCallback());
// Destroy the connection without receiving an input report.
hid_connection_impl_.reset();
}
} // namespace device