| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/device/serial/serial_port_manager_impl.h" |
| |
| #include <algorithm> |
| #include <set> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "base/threading/thread.h" |
| #include "device/bluetooth/bluetooth_adapter_factory.h" |
| #include "device/bluetooth/public/cpp/bluetooth_uuid.h" |
| #include "device/bluetooth/test/mock_bluetooth_adapter.h" |
| #include "device/bluetooth/test/mock_bluetooth_device.h" |
| #include "device/bluetooth/test/mock_bluetooth_socket.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/bindings/self_owned_receiver.h" |
| #include "services/device/device_service_test_base.h" |
| #include "services/device/public/cpp/bluetooth/bluetooth_utils.h" |
| #include "services/device/public/cpp/device_features.h" |
| #include "services/device/public/cpp/test/fake_serial_port_client.h" |
| #include "services/device/public/mojom/serial.mojom.h" |
| #include "services/device/serial/bluetooth_serial_device_enumerator.h" |
| #include "services/device/serial/fake_serial_device_enumerator.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace device { |
| |
| namespace { |
| |
| using ::base::test::InvokeFuture; |
| using ::base::test::RunOnceCallback; |
| using ::base::test::TestFuture; |
| using ::testing::_; |
| using ::testing::Invoke; |
| using ::testing::Return; |
| |
| const base::FilePath kFakeDevicePath1(FILE_PATH_LITERAL("/dev/fakeserialmojo")); |
| const base::FilePath kFakeDevicePath2(FILE_PATH_LITERAL("\\\\COM800\\")); |
| constexpr char kDeviceAddress[] = "00:00:00:00:00:00"; |
| |
| class MockSerialPortManagerClient : public mojom::SerialPortManagerClient { |
| public: |
| MockSerialPortManagerClient() = default; |
| MockSerialPortManagerClient(const MockSerialPortManagerClient&) = delete; |
| MockSerialPortManagerClient& operator=(const MockSerialPortManagerClient&) = |
| delete; |
| ~MockSerialPortManagerClient() override = default; |
| |
| mojo::PendingRemote<mojom::SerialPortManagerClient> |
| BindNewPipeAndPassRemote() { |
| return receiver_.BindNewPipeAndPassRemote(); |
| } |
| |
| // mojom::SerialPortManagerClient |
| MOCK_METHOD1(OnPortAdded, void(mojom::SerialPortInfoPtr)); |
| MOCK_METHOD1(OnPortRemoved, void(mojom::SerialPortInfoPtr)); |
| MOCK_METHOD1(OnPortConnectedStateChanged, void(mojom::SerialPortInfoPtr)); |
| |
| private: |
| mojo::Receiver<mojom::SerialPortManagerClient> receiver_{this}; |
| }; |
| |
| class TestingBluetoothAdapter : public device::MockBluetoothAdapter { |
| public: |
| bool IsInitialized() const override { return false; } |
| MOCK_METHOD1(Initialize, void(base::OnceClosure callback)); |
| |
| private: |
| ~TestingBluetoothAdapter() override = default; |
| }; |
| |
| } // namespace |
| |
| class SerialPortManagerImplTest : public DeviceServiceTestBase { |
| public: |
| SerialPortManagerImplTest() { |
| auto enumerator = std::make_unique<FakeSerialEnumerator>(); |
| enumerator_ = enumerator.get(); |
| enumerator_->AddDevicePath(kFakeDevicePath1); |
| enumerator_->AddDevicePath(kFakeDevicePath2); |
| |
| manager_ = std::make_unique<SerialPortManagerImpl>( |
| io_task_runner_, base::SingleThreadTaskRunner::GetCurrentDefault()); |
| manager_->SetSerialEnumeratorForTesting(std::move(enumerator)); |
| } |
| |
| SerialPortManagerImplTest(const SerialPortManagerImplTest&) = delete; |
| SerialPortManagerImplTest& operator=(const SerialPortManagerImplTest&) = |
| delete; |
| |
| ~SerialPortManagerImplTest() override = default; |
| |
| void TearDown() override { |
| // Resetting `manager_` will delete the BluetoothSerialDeviceEnumerator |
| // which will enqueue the deletion of a `SequenceBound` helper. |
| manager_.reset(); |
| // Reset the DeviceService to ensure cleanup of any AdapterHelper related |
| // tasks. |
| DestroyDeviceService(); |
| // Wait for any `SequenceBound` objects have been destroyed |
| // to avoid tripping leak detection. |
| task_environment_.RunUntilIdle(); |
| } |
| |
| // Creates a MockBluetoothDevice with the Serial Port Profile service and no |
| // other service UUIDs. |
| std::unique_ptr<MockBluetoothDevice> CreateSerialPortProfileDevice() { |
| auto mock_device = std::make_unique<MockBluetoothDevice>( |
| adapter_.get(), /*bluetooth_class=*/0, "Test Device", kDeviceAddress, |
| /*initially_paired=*/true, /*connected=*/true); |
| mock_device->AddUUID(GetSerialPortProfileUUID()); |
| return mock_device; |
| } |
| |
| // Since not all functions need to use a MockBluetoothAdapter, this function |
| // is called at the beginning of test cases that do require a |
| // MockBluetoothAdapter. |
| void SetupBluetoothEnumerator() { |
| ON_CALL(*adapter_, GetDevices()) |
| .WillByDefault( |
| Invoke(adapter_.get(), &MockBluetoothAdapter::GetConstMockDevices)); |
| device::BluetoothAdapterFactory::SetAdapterForTesting(adapter_); |
| |
| adapter_->AddMockDevice(CreateSerialPortProfileDevice()); |
| |
| auto bluetooth_enumerator = |
| std::make_unique<BluetoothSerialDeviceEnumerator>( |
| adapter_task_runner()); |
| bluetooth_enumerator_ = bluetooth_enumerator.get(); |
| |
| manager_->SetBluetoothSerialEnumeratorForTesting( |
| std::move(bluetooth_enumerator)); |
| } |
| |
| void SetupBluetoothEnumeratorWithExpectations() { |
| ON_CALL(*adapter_, GetDevices()) |
| .WillByDefault( |
| Invoke(adapter_.get(), &MockBluetoothAdapter::GetConstMockDevices)); |
| device::BluetoothAdapterFactory::SetAdapterForTesting(adapter_); |
| |
| auto mock_device = std::make_unique<MockBluetoothDevice>( |
| adapter_.get(), 0, "Test Device", kDeviceAddress, false, false); |
| mock_device->AddUUID(GetSerialPortProfileUUID()); |
| MockBluetoothDevice* mock_device_ptr = mock_device.get(); |
| adapter_->AddMockDevice(std::move(mock_device)); |
| |
| EXPECT_CALL(*adapter_, GetDevice(kDeviceAddress)) |
| .WillOnce(Return(mock_device_ptr)); |
| EXPECT_CALL(*mock_device_ptr, |
| ConnectToService(GetSerialPortProfileUUID(), _, _)) |
| .WillOnce(RunOnceCallback<1>(mock_socket_)); |
| |
| auto bluetooth_enumerator = |
| std::make_unique<BluetoothSerialDeviceEnumerator>( |
| adapter_task_runner()); |
| bluetooth_enumerator_ = bluetooth_enumerator.get(); |
| |
| manager_->SetBluetoothSerialEnumeratorForTesting( |
| std::move(bluetooth_enumerator)); |
| } |
| |
| protected: |
| scoped_refptr<base::SingleThreadTaskRunner> adapter_task_runner() { |
| return base::SingleThreadTaskRunner::GetCurrentDefault(); |
| } |
| |
| raw_ptr<FakeSerialEnumerator, DanglingUntriaged> enumerator_; |
| raw_ptr<BluetoothSerialDeviceEnumerator, DanglingUntriaged> |
| bluetooth_enumerator_; |
| scoped_refptr<MockBluetoothAdapter> adapter_ = |
| base::MakeRefCounted<MockBluetoothAdapter>(); |
| scoped_refptr<MockBluetoothSocket> mock_socket_ = |
| base::MakeRefCounted<MockBluetoothSocket>(); |
| |
| void Bind(mojo::PendingReceiver<mojom::SerialPortManager> receiver) { |
| manager_->Bind(std::move(receiver)); |
| } |
| |
| private: |
| std::unique_ptr<SerialPortManagerImpl> manager_; |
| }; |
| |
| // This is to simply test that we can enumerate devices on the platform without |
| // hanging or crashing. |
| TEST_F(SerialPortManagerImplTest, SimpleEnumerationTest) { |
| SetupBluetoothEnumerator(); |
| // DeviceService has its own instance of SerialPortManagerImpl that is used to |
| // bind the receiver over the one created for this test. |
| mojo::Remote<mojom::SerialPortManager> port_manager; |
| device_service()->BindSerialPortManager( |
| port_manager.BindNewPipeAndPassReceiver()); |
| |
| base::RunLoop loop; |
| port_manager->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<mojom::SerialPortInfoPtr> results) { loop.Quit(); })); |
| loop.Run(); |
| } |
| |
| TEST_F(SerialPortManagerImplTest, GetDevices) { |
| SetupBluetoothEnumerator(); |
| mojo::Remote<mojom::SerialPortManager> port_manager; |
| Bind(port_manager.BindNewPipeAndPassReceiver()); |
| const std::set<base::FilePath> expected_paths = { |
| kFakeDevicePath1, kFakeDevicePath2, |
| base::FilePath::FromASCII(kDeviceAddress)}; |
| |
| base::RunLoop loop; |
| port_manager->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<mojom::SerialPortInfoPtr> results) { |
| EXPECT_EQ(expected_paths.size(), results.size()); |
| std::set<base::FilePath> actual_paths; |
| for (size_t i = 0; i < results.size(); ++i) |
| actual_paths.insert(results[i]->path); |
| EXPECT_EQ(expected_paths, actual_paths); |
| loop.Quit(); |
| })); |
| loop.Run(); |
| } |
| |
| TEST_F(SerialPortManagerImplTest, OpenUnknownPort) { |
| SetupBluetoothEnumerator(); |
| mojo::Remote<mojom::SerialPortManager> port_manager; |
| Bind(port_manager.BindNewPipeAndPassReceiver()); |
| |
| base::RunLoop loop; |
| port_manager->OpenPort( |
| base::UnguessableToken::Create(), |
| /*use_alternate_path=*/false, mojom::SerialConnectionOptions::New(), |
| FakeSerialPortClient::Create(), |
| /*watcher=*/mojo::NullRemote(), |
| base::BindLambdaForTesting( |
| [&](mojo::PendingRemote<mojom::SerialPort> pending_remote) { |
| EXPECT_FALSE(pending_remote.is_valid()); |
| loop.Quit(); |
| })); |
| loop.Run(); |
| } |
| |
| TEST_F(SerialPortManagerImplTest, PortRemovedAndAdded) { |
| SetupBluetoothEnumerator(); |
| mojo::Remote<mojom::SerialPortManager> port_manager; |
| Bind(port_manager.BindNewPipeAndPassReceiver()); |
| |
| MockSerialPortManagerClient client; |
| port_manager->SetClient(client.BindNewPipeAndPassRemote()); |
| |
| base::UnguessableToken port1_token; |
| { |
| base::RunLoop run_loop; |
| port_manager->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<mojom::SerialPortInfoPtr> results) { |
| for (const auto& port : results) { |
| if (port->path == kFakeDevicePath1) { |
| port1_token = port->token; |
| break; |
| } |
| } |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| ASSERT_FALSE(port1_token.is_empty()); |
| |
| enumerator_->RemoveDevicePath(kFakeDevicePath1); |
| { |
| base::RunLoop run_loop; |
| EXPECT_CALL(client, OnPortRemoved(_)) |
| .WillOnce([&](mojom::SerialPortInfoPtr port) { |
| EXPECT_EQ(port1_token, port->token); |
| EXPECT_EQ(kFakeDevicePath1, port->path); |
| run_loop.Quit(); |
| }); |
| run_loop.Run(); |
| } |
| |
| enumerator_->AddDevicePath(kFakeDevicePath1); |
| { |
| base::RunLoop run_loop; |
| EXPECT_CALL(client, OnPortAdded(_)) |
| .WillOnce([&](mojom::SerialPortInfoPtr port) { |
| EXPECT_NE(port1_token, port->token); |
| EXPECT_EQ(kFakeDevicePath1, port->path); |
| run_loop.Quit(); |
| }); |
| run_loop.Run(); |
| } |
| } |
| |
| TEST_F(SerialPortManagerImplTest, OpenBluetoothDevicePort) { |
| SetupBluetoothEnumeratorWithExpectations(); |
| mojo::Remote<mojom::SerialPortManager> port_manager; |
| Bind(port_manager.BindNewPipeAndPassReceiver()); |
| mojo::PendingRemote<mojom::SerialPortConnectionWatcher> watcher_remote; |
| mojo::SelfOwnedReceiverRef<mojom::SerialPortConnectionWatcher> watcher = |
| mojo::MakeSelfOwnedReceiver( |
| std::make_unique<mojom::SerialPortConnectionWatcher>(), |
| watcher_remote.InitWithNewPipeAndPassReceiver()); |
| |
| // Since we only want to use devices enumerated by the Bluetooth |
| // enumerator, we can remove the devices that are not. |
| enumerator_->RemoveDevicePath(kFakeDevicePath1); |
| enumerator_->RemoveDevicePath(kFakeDevicePath2); |
| |
| const std::set<base::FilePath> expected_paths = { |
| base::FilePath::FromASCII(kDeviceAddress)}; |
| |
| mojo::Remote<mojom::SerialPort> serial_port; |
| base::RunLoop loop; |
| port_manager->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<mojom::SerialPortInfoPtr> results) { |
| EXPECT_EQ(expected_paths.size(), results.size()); |
| std::set<base::FilePath> actual_paths; |
| for (size_t i = 0; i < results.size(); ++i) |
| actual_paths.insert(results[i]->path); |
| EXPECT_EQ(expected_paths, actual_paths); |
| |
| port_manager->OpenPort( |
| results[0]->token, |
| /*use_alternate_path=*/false, mojom::SerialConnectionOptions::New(), |
| FakeSerialPortClient::Create(), std::move(watcher_remote), |
| base::BindLambdaForTesting( |
| [&](mojo::PendingRemote<mojom::SerialPort> pending_remote) { |
| serial_port.Bind(std::move(pending_remote)); |
| EXPECT_TRUE(serial_port.is_connected()); |
| loop.Quit(); |
| })); |
| })); |
| |
| loop.Run(); |
| |
| base::RunLoop disconnect_loop; |
| watcher->set_connection_error_handler(disconnect_loop.QuitClosure()); |
| |
| serial_port.reset(); |
| disconnect_loop.Run(); |
| } |
| |
| TEST_F(SerialPortManagerImplTest, BluetoothPortRemovedAndAdded) { |
| SetupBluetoothEnumerator(); |
| mojo::Remote<mojom::SerialPortManager> port_manager; |
| Bind(port_manager.BindNewPipeAndPassReceiver()); |
| |
| MockSerialPortManagerClient client; |
| port_manager->SetClient(client.BindNewPipeAndPassRemote()); |
| |
| base::UnguessableToken port1_token; |
| { |
| base::RunLoop run_loop; |
| port_manager->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<mojom::SerialPortInfoPtr> results) { |
| for (const auto& port : results) { |
| if (port->path == base::FilePath::FromASCII(kDeviceAddress)) { |
| port1_token = port->token; |
| break; |
| } |
| } |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| ASSERT_FALSE(port1_token.is_empty()); |
| |
| bluetooth_enumerator_->DeviceRemoved(kDeviceAddress); |
| { |
| base::RunLoop run_loop; |
| EXPECT_CALL(client, OnPortRemoved(_)) |
| .WillOnce([&](mojom::SerialPortInfoPtr port) { |
| EXPECT_EQ(port1_token, port->token); |
| EXPECT_EQ(port->path, base::FilePath::FromASCII(kDeviceAddress)); |
| EXPECT_EQ(mojom::SerialPortType::BLUETOOTH_CLASSIC_RFCOMM, |
| port->type); |
| run_loop.Quit(); |
| }); |
| run_loop.Run(); |
| } |
| |
| auto mock_device = std::make_unique<MockBluetoothDevice>( |
| adapter_.get(), 0, "Test Device", kDeviceAddress, false, false); |
| static const BluetoothUUID kSerialPortProfileUUID("1101"); |
| mock_device->AddUUID(kSerialPortProfileUUID); |
| MockBluetoothDevice* mock_device_ptr = mock_device.get(); |
| adapter_->AddMockDevice(std::move(mock_device)); |
| |
| bluetooth_enumerator_->DeviceAddedForTesting(adapter_.get(), mock_device_ptr); |
| { |
| base::RunLoop run_loop; |
| EXPECT_CALL(client, OnPortAdded(_)) |
| .WillOnce([&](mojom::SerialPortInfoPtr port) { |
| EXPECT_NE(port1_token, port->token); |
| EXPECT_EQ(port->path, base::FilePath::FromASCII(kDeviceAddress)); |
| EXPECT_EQ(mojom::SerialPortType::BLUETOOTH_CLASSIC_RFCOMM, |
| port->type); |
| run_loop.Quit(); |
| }); |
| run_loop.Run(); |
| } |
| } |
| |
| TEST_F(SerialPortManagerImplTest, BluetoothDeviceChanged) { |
| SetupBluetoothEnumerator(); |
| mojo::Remote<mojom::SerialPortManager> port_manager; |
| Bind(port_manager.BindNewPipeAndPassReceiver()); |
| |
| MockSerialPortManagerClient client; |
| port_manager->SetClient(client.BindNewPipeAndPassRemote()); |
| |
| TestFuture<std::vector<mojom::SerialPortInfoPtr>> get_devices_future; |
| port_manager->GetDevices(get_devices_future.GetCallback()); |
| auto port_it = |
| std::ranges::find_if(get_devices_future.Get(), [&](const auto& port) { |
| return port->path == base::FilePath::FromASCII(kDeviceAddress); |
| }); |
| ASSERT_NE(port_it, get_devices_future.Get().end()); |
| EXPECT_EQ((*port_it)->path, base::FilePath::FromASCII(kDeviceAddress)); |
| EXPECT_EQ((*port_it)->type, mojom::SerialPortType::BLUETOOTH_CLASSIC_RFCOMM); |
| const base::UnguessableToken token = (*port_it)->token; |
| |
| // Create an updated device with another service UUID. A second RFCOMM port is |
| // created with the same device address but different token. |
| auto updated_device = CreateSerialPortProfileDevice(); |
| updated_device->AddUUID( |
| device::BluetoothUUID("25e97ff7-24ce-4c4c-8951-f764a708f7b5")); |
| TestFuture<mojom::SerialPortInfoPtr> port_added_future; |
| EXPECT_CALL(client, OnPortAdded).WillOnce(InvokeFuture(port_added_future)); |
| bluetooth_enumerator_->DeviceChangedForTesting(adapter_.get(), |
| updated_device.get()); |
| ASSERT_TRUE(port_added_future.Get()); |
| EXPECT_NE(port_added_future.Get()->token, token); |
| EXPECT_EQ(port_added_future.Get()->path, |
| base::FilePath::FromASCII(kDeviceAddress)); |
| EXPECT_EQ(port_added_future.Get()->type, |
| mojom::SerialPortType::BLUETOOTH_CLASSIC_RFCOMM); |
| } |
| |
| TEST_F(SerialPortManagerImplTest, BluetoothDeviceConnectedStateChanged) { |
| base::test::ScopedFeatureList scoped_feature_list( |
| features::kSerialPortConnected); |
| SetupBluetoothEnumerator(); |
| mojo::Remote<mojom::SerialPortManager> port_manager; |
| Bind(port_manager.BindNewPipeAndPassReceiver()); |
| |
| MockSerialPortManagerClient client; |
| port_manager->SetClient(client.BindNewPipeAndPassRemote()); |
| |
| // Call GetDevices to ensure the port manager is initialized and the client is |
| // set. |
| TestFuture<std::vector<mojom::SerialPortInfoPtr>> get_devices_future; |
| port_manager->GetDevices(get_devices_future.GetCallback()); |
| auto port_it = |
| std::ranges::find_if(get_devices_future.Get(), [&](const auto& port) { |
| return port->path == base::FilePath::FromASCII(kDeviceAddress); |
| }); |
| ASSERT_NE(port_it, get_devices_future.Get().end()); |
| EXPECT_EQ((*port_it)->path, base::FilePath::FromASCII(kDeviceAddress)); |
| EXPECT_EQ((*port_it)->type, mojom::SerialPortType::BLUETOOTH_CLASSIC_RFCOMM); |
| const base::UnguessableToken token = (*port_it)->token; |
| |
| // Simulate the device becoming disconnected. |
| TestFuture<mojom::SerialPortInfoPtr> disconnect_future; |
| EXPECT_CALL(client, OnPortConnectedStateChanged) |
| .WillOnce(InvokeFuture(disconnect_future)); |
| auto updated_device = CreateSerialPortProfileDevice(); |
| updated_device->SetConnected(false); |
| bluetooth_enumerator_->DeviceChangedForTesting(adapter_.get(), |
| updated_device.get()); |
| EXPECT_EQ(disconnect_future.Get()->token, token); |
| EXPECT_FALSE(disconnect_future.Get()->connected); |
| } |
| |
| TEST_F(SerialPortManagerImplTest, |
| BluetoothSerialDeviceEnumerator_DeleteBeforeAdapterInit) { |
| auto adapter = base::MakeRefCounted<TestingBluetoothAdapter>(); |
| BluetoothAdapterFactory::SetAdapterForTesting(adapter); |
| |
| // BluetoothAdapterFactory does not initialize the test adapter. Instead it |
| // holds on to the GetAdapter callback until adapter initialization is |
| // complete. |
| EXPECT_CALL(*adapter, Initialize).Times(0); |
| |
| // Create the enumerator, which calls GetAdapter(), which is blocked waiting |
| // on adapter initialization. |
| auto enumerator = |
| std::make_unique<BluetoothSerialDeviceEnumerator>(adapter_task_runner()); |
| |
| // Delete the enumerator before adapter initialization completes. |
| // Explicitly delete its helper. This workaround is needed because this test |
| // does not mock out the BluetoothAdapterFactory singleton, which (on Linux) |
| // calls bluez::BluezDBusManager::Get() - failing a CHECK. |
| enumerator->SynchronouslyResetHelperForTesting(); |
| enumerator.reset(); |
| |
| // Directly call the adapter initialization callback, which calls any saved |
| // GetAdapter() callbacks - i.e. the one made by the |
| // BluetoothSerialDeviceEnumerator constructor. |
| BluetoothAdapterFactory::Get()->AdapterInitialized(); |
| |
| // We didn't crash? yay \o/ test passed. |
| } |
| |
| } // namespace device |