| // Copyright 2022 The Fuchsia 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 "src/media/audio/services/device_registry/device_detector.h" |
| |
| #include <fidl/fuchsia.audio.device/cpp/natural_types.h> |
| #include <fidl/fuchsia.hardware.audio/cpp/fidl.h> |
| #include <fidl/fuchsia.hardware.audio/cpp/test_base.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/fdio/namespace.h> |
| #include <lib/fidl/cpp/binding.h> |
| #include <lib/fidl/cpp/channel.h> |
| #include <lib/fidl/cpp/wire/channel.h> |
| #include <lib/fidl/cpp/wire/internal/transport.h> |
| #include <lib/fidl/cpp/wire/internal/transport_channel.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/zx/clock.h> |
| |
| #include <memory> |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/lib/testing/loop_fixture/test_loop_fixture.h" |
| #include "src/storage/lib/vfs/cpp/pseudo_dir.h" |
| #include "src/storage/lib/vfs/cpp/service.h" |
| #include "src/storage/lib/vfs/cpp/synchronous_vfs.h" |
| |
| namespace media_audio { |
| |
| namespace { |
| |
| // Minimal `fuchsia_hardware_audio::Codec` used to emulate a fake devfs directory for tests. |
| using fuchsia_hardware_audio::Codec; |
| using fuchsia_hardware_audio::CodecConnector; |
| class FakeAudioCodec : public fidl::testing::TestBase<CodecConnector>, |
| public fidl::testing::TestBase<Codec> { |
| public: |
| explicit FakeAudioCodec(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {} |
| |
| void NotImplemented_(const std::string& name, ::fidl::CompleterBase& completer) override { |
| ADD_FAILURE() << "Method not implemented: '" << name << ""; |
| completer.Close(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| // Used synchronously, this 2-way call ensures that Connect is complete, before we proceed. |
| void GetProperties(GetPropertiesCompleter::Sync& completer) override { completer.Reply({}); } |
| |
| fbl::RefPtr<fs::Service> AsService() { |
| return fbl::MakeRefCounted<fs::Service>([this](fidl::ServerEnd<CodecConnector> c) { |
| connector_binding_ = fidl::BindServer(dispatcher(), std::move(c), this); |
| return ZX_OK; |
| }); |
| } |
| |
| bool is_bound() const { return binding_.has_value(); } |
| async_dispatcher_t* dispatcher() { return dispatcher_; } |
| |
| private: |
| // FIDL method for fuchsia.hardware.audio.CodecConnector. |
| void Connect(ConnectRequest& request, ConnectCompleter::Sync& completer) override { |
| binding_ = fidl::BindServer(dispatcher(), std::move(request.codec_protocol()), this); |
| } |
| |
| async_dispatcher_t* dispatcher_; |
| std::optional<fidl::ServerBindingRef<CodecConnector>> connector_binding_; |
| std::optional<fidl::ServerBindingRef<Codec>> binding_; |
| }; |
| |
| using fuchsia_hardware_audio::Composite; |
| // TODO(https://fxbug.dev/304551042): Convert VirtualAudioComposite to DFv2; remove Connector. |
| using fuchsia_hardware_audio::CompositeConnector; |
| class FakeAudioComposite : public fidl::testing::TestBase<Composite>, |
| public fidl::testing::TestBase<CompositeConnector> { |
| public: |
| explicit FakeAudioComposite(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {} |
| |
| void NotImplemented_(const std::string& name, ::fidl::CompleterBase& completer) override { |
| ADD_FAILURE() << "Method not implemented: '" << name << ""; |
| completer.Close(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| void GetProperties(GetPropertiesCompleter::Sync& completer) override { completer.Reply({}); } |
| |
| fbl::RefPtr<fs::Service> AsService() { |
| // TODO(https://fxbug.dev/304551042): Convert VirtualAudioComposite to DFv2; remove 'else'. |
| if constexpr (kDetectDFv2CompositeDevices) { |
| return fbl::MakeRefCounted<fs::Service>([this](fidl::ServerEnd<Composite> c) { |
| binding_ = fidl::BindServer(dispatcher(), std::move(c), this); |
| return ZX_OK; |
| }); |
| } else { |
| return fbl::MakeRefCounted<fs::Service>([this](fidl::ServerEnd<CompositeConnector> c) { |
| connector_binding_ = fidl::BindServer(dispatcher(), std::move(c), this); |
| return ZX_OK; |
| }); |
| } |
| } |
| |
| bool is_bound() const { return binding_.has_value(); } |
| async_dispatcher_t* dispatcher() { return dispatcher_; } |
| |
| private: |
| // FIDL method for fuchsia.hardware.audio.CompositeConnector. |
| // TODO(https://fxbug.dev/304551042): Convert VirtualAudioComposite to DFv2; remove this. |
| void Connect(ConnectRequest& request, ConnectCompleter::Sync& completer) override { |
| binding_ = fidl::BindServer(dispatcher(), std::move(request.composite_protocol()), this); |
| } |
| |
| async_dispatcher_t* dispatcher_; |
| std::optional<fidl::ServerBindingRef<CompositeConnector>> connector_binding_; |
| std::optional<fidl::ServerBindingRef<Composite>> binding_; |
| }; |
| |
| using fuchsia_hardware_audio::StreamConfig; |
| using fuchsia_hardware_audio::StreamConfigConnector; |
| class FakeAudioStreamConfig : public fidl::testing::TestBase<StreamConfigConnector>, |
| public fidl::testing::TestBase<StreamConfig> { |
| public: |
| explicit FakeAudioStreamConfig(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {} |
| |
| void NotImplemented_(const std::string& name, ::fidl::CompleterBase& completer) override { |
| ADD_FAILURE() << "Method not implemented: '" << name << ""; |
| completer.Close(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| void GetProperties(GetPropertiesCompleter::Sync& completer) override { completer.Reply({}); } |
| |
| fbl::RefPtr<fs::Service> AsService() { |
| return fbl::MakeRefCounted<fs::Service>([this](fidl::ServerEnd<StreamConfigConnector> c) { |
| connector_binding_ = fidl::BindServer(dispatcher(), std::move(c), this); |
| return ZX_OK; |
| }); |
| } |
| |
| bool is_bound() const { return binding_.has_value(); } |
| async_dispatcher_t* dispatcher() { return dispatcher_; } |
| |
| private: |
| void Connect(ConnectRequest& request, ConnectCompleter::Sync& completer) override { |
| binding_ = fidl::BindServer(dispatcher(), std::move(request.protocol()), this); |
| } |
| |
| async_dispatcher_t* dispatcher_; |
| std::optional<fidl::ServerBindingRef<StreamConfigConnector>> connector_binding_; |
| std::optional<fidl::ServerBindingRef<StreamConfig>> binding_; |
| }; |
| |
| using fuchsia_audio_device::DeviceType; |
| using fuchsia_audio_device::DriverClient; |
| |
| class DeviceTracker { |
| public: |
| struct DeviceConnection { |
| std::string_view name; |
| DeviceType device_type; |
| DriverClient client; |
| }; |
| |
| DeviceTracker(async_dispatcher_t* dispatcher, bool detection_is_expected) |
| : dispatcher_(dispatcher), detection_is_expected_(detection_is_expected) {} |
| |
| virtual ~DeviceTracker() = default; |
| |
| size_t size() const { return devices_.size(); } |
| const std::vector<DeviceConnection>& devices() const { return devices_; } |
| DeviceDetectionHandler handler() { return handler_; } |
| async_dispatcher_t* dispatcher() { return dispatcher_; } |
| |
| private: |
| DeviceDetectionHandler handler_ = [this](std::string_view name, DeviceType device_type, |
| DriverClient driver_client) { |
| ASSERT_TRUE(detection_is_expected_) << "Unexpected device detection"; |
| |
| devices_.emplace_back(DeviceConnection{name, device_type, std::move(driver_client)}); |
| }; |
| async_dispatcher_t* dispatcher_; |
| const bool detection_is_expected_; |
| |
| std::vector<DeviceConnection> devices_; |
| }; |
| |
| class DeviceDetectorTest : public gtest::TestLoopFixture { |
| protected: |
| static inline constexpr zx::duration kCommandTimeout = zx::sec(10); |
| |
| void SetUp() override { |
| ASSERT_TRUE(input_dir_ != nullptr); |
| ASSERT_TRUE(output_dir_ != nullptr); |
| |
| ASSERT_EQ(fdio_ns_get_installed(&ns_), ZX_OK); |
| zx::channel channel0, channel1; |
| |
| // Serve up the emulated audio-composite directory |
| ASSERT_EQ(zx::channel::create(0, &channel0, &channel1), ZX_OK); |
| ASSERT_EQ( |
| vfs_.Serve(composite_dir_, std::move(channel0), fs::VnodeConnectionOptions::ReadOnly()), |
| ZX_OK); |
| ASSERT_EQ(fdio_ns_bind(ns_, "/dev/class/audio-composite", channel1.release()), ZX_OK); |
| |
| // Serve up the emulated audio-input directory |
| ASSERT_EQ(zx::channel::create(0, &channel0, &channel1), ZX_OK); |
| ASSERT_EQ(vfs_.Serve(input_dir_, std::move(channel0), fs::VnodeConnectionOptions::ReadOnly()), |
| ZX_OK); |
| ASSERT_EQ(fdio_ns_bind(ns_, "/dev/class/audio-input", channel1.release()), ZX_OK); |
| |
| // Serve up the emulated audio-output directory |
| ASSERT_EQ(zx::channel::create(0, &channel0, &channel1), ZX_OK); |
| ASSERT_EQ(vfs_.Serve(output_dir_, std::move(channel0), fs::VnodeConnectionOptions::ReadOnly()), |
| ZX_OK); |
| ASSERT_EQ(fdio_ns_bind(ns_, "/dev/class/audio-output", channel1.release()), ZX_OK); |
| |
| // Serve up the emulated codec directory |
| ASSERT_EQ(zx::channel::create(0, &channel0, &channel1), ZX_OK); |
| ASSERT_EQ(vfs_.Serve(codec_dir_, std::move(channel0), fs::VnodeConnectionOptions::ReadOnly()), |
| ZX_OK); |
| ASSERT_EQ(fdio_ns_bind(ns_, "/dev/class/codec", channel1.release()), ZX_OK); |
| } |
| |
| void TearDown() override { |
| // Scoped directory entries have gone out of scope, but to avoid races we remove all entries. |
| bool task_has_run = false; |
| async::PostTask(dispatcher(), [this, &task_has_run]() { |
| codec_dir_->RemoveAllEntries(); |
| composite_dir_->RemoveAllEntries(); |
| input_dir_->RemoveAllEntries(); |
| output_dir_->RemoveAllEntries(); |
| task_has_run = true; |
| }); |
| while (!task_has_run) { |
| RunLoopUntilIdle(); |
| } |
| ASSERT_TRUE(codec_dir_->IsEmpty() && composite_dir_->IsEmpty() && input_dir_->IsEmpty() && |
| output_dir_->IsEmpty()) |
| << "codec_dir is " << (codec_dir_->IsEmpty() ? "" : "NOT ") << "empty; composite_dir is " |
| << (composite_dir_->IsEmpty() ? "" : "NOT ") << "empty; input_dir is " |
| << (input_dir_->IsEmpty() ? "" : "NOT ") << "empty; output_dir is " |
| << (output_dir_->IsEmpty() ? "" : "NOT ") << "empty"; |
| |
| ASSERT_NE(ns_, nullptr); |
| ASSERT_EQ(fdio_ns_unbind(ns_, "/dev/class/audio-composite"), ZX_OK); |
| ASSERT_EQ(fdio_ns_unbind(ns_, "/dev/class/audio-input"), ZX_OK); |
| ASSERT_EQ(fdio_ns_unbind(ns_, "/dev/class/audio-output"), ZX_OK); |
| ASSERT_EQ(fdio_ns_unbind(ns_, "/dev/class/codec"), ZX_OK); |
| } |
| |
| // Holds a ref to a pseudo dir entry that removes the entry when this object goes out of scope. |
| struct ScopedDirent { |
| std::string name; |
| fbl::RefPtr<fs::PseudoDir> dir; |
| async_dispatcher_t* dispatcher; |
| ~ScopedDirent() { |
| async::PostTask(dispatcher, [n = name, d = dir]() { d->RemoveEntry(n); }); |
| } |
| }; |
| |
| // Adds a `FakeAudioCodec` to the emulated 'codec' directory that has been installed in |
| // the local namespace at /dev/class/codec. |
| ScopedDirent AddCodecDevice(std::shared_ptr<FakeAudioCodec> device) { |
| auto name = std::to_string(next_device_number_++); |
| bool task_has_run = false; |
| async::PostTask(dispatcher(), [this, name, device = std::move(device), &task_has_run]() { |
| task_has_run = true; |
| ASSERT_EQ(ZX_OK, codec_dir_->AddEntry(name, device->AsService())); |
| }); |
| while (!task_has_run) { |
| RunLoopUntilIdle(); |
| } |
| return {name, codec_dir_, dispatcher()}; |
| } |
| |
| // Adds a `FakeAudioComposite` to the emulated 'composite' directory that has been installed in |
| // the local namespace at /dev/class/audio-composite. |
| ScopedDirent AddCompositeDevice(std::shared_ptr<FakeAudioComposite> device) { |
| auto name = std::to_string(next_device_number_++); |
| bool task_has_run = false; |
| async::PostTask(dispatcher(), [this, name, device = std::move(device), &task_has_run]() { |
| task_has_run = true; |
| ASSERT_EQ(ZX_OK, composite_dir_->AddEntry(name, device->AsService())); |
| }); |
| while (!task_has_run) { |
| RunLoopUntilIdle(); |
| } |
| return {name, composite_dir_, dispatcher()}; |
| } |
| |
| // Adds a `FakeAudioStreamConfig` to the emulated 'audio-input' directory that has been installed |
| // in the local namespace at /dev/class/audio-input. |
| ScopedDirent AddStreamConfigInputDevice(std::shared_ptr<FakeAudioStreamConfig> device) { |
| auto name = std::to_string(next_device_number_++); |
| bool task_has_run = false; |
| async::PostTask(dispatcher(), [this, name, device = std::move(device), &task_has_run]() { |
| task_has_run = true; |
| ASSERT_EQ(ZX_OK, input_dir_->AddEntry(name, device->AsService())); |
| }); |
| while (!task_has_run) { |
| RunLoopUntilIdle(); |
| } |
| return {name, input_dir_, dispatcher()}; |
| } |
| |
| // Adds a `FakeAudioStreamConfig` to the emulated 'audio-output' directory that has been installed |
| // in the local namespace at /dev/class/audio-output. |
| ScopedDirent AddStreamConfigOutputDevice(std::shared_ptr<FakeAudioStreamConfig> device) { |
| auto name = std::to_string(next_device_number_++); |
| bool task_has_run = false; |
| async::PostTask(dispatcher(), [this, name, device = std::move(device), &task_has_run]() { |
| task_has_run = true; |
| ASSERT_EQ(ZX_OK, output_dir_->AddEntry(name, device->AsService())); |
| }); |
| while (!task_has_run) { |
| RunLoopUntilIdle(); |
| } |
| return {name, output_dir_, dispatcher()}; |
| } |
| |
| private: |
| fdio_ns_t* ns_ = nullptr; |
| uint32_t next_device_number_ = 0; |
| |
| fs::SynchronousVfs vfs_{dispatcher()}; |
| |
| // Note these _must_ be RefPtrs since vfs_ will try to AdoptRef on the raw pointer passed to it. |
| fbl::RefPtr<fs::PseudoDir> codec_dir_{fbl::MakeRefCounted<fs::PseudoDir>()}; |
| fbl::RefPtr<fs::PseudoDir> composite_dir_{fbl::MakeRefCounted<fs::PseudoDir>()}; |
| fbl::RefPtr<fs::PseudoDir> input_dir_{fbl::MakeRefCounted<fs::PseudoDir>()}; |
| fbl::RefPtr<fs::PseudoDir> output_dir_{fbl::MakeRefCounted<fs::PseudoDir>()}; |
| }; |
| |
| // For devices that exist before the plug detector, verify pre-Start, post-Start, post-Stop. |
| TEST_F(DeviceDetectorTest, DetectExistingDevices) { |
| // Add some devices that will exist before the plug detector is created. |
| auto input0 = std::make_shared<FakeAudioStreamConfig>(dispatcher()); |
| auto composite0 = std::make_shared<FakeAudioComposite>(dispatcher()); |
| auto output0 = std::make_shared<FakeAudioStreamConfig>(dispatcher()); |
| auto codec0 = std::make_shared<FakeAudioCodec>(dispatcher()); |
| auto input1 = std::make_shared<FakeAudioStreamConfig>(dispatcher()); |
| auto composite1 = std::make_shared<FakeAudioComposite>(dispatcher()); |
| auto output1 = std::make_shared<FakeAudioStreamConfig>(dispatcher()); |
| auto codec1 = std::make_shared<FakeAudioCodec>(dispatcher()); |
| |
| [[maybe_unused]] auto dev0 = AddStreamConfigInputDevice(input0); |
| [[maybe_unused]] auto dev1 = AddCompositeDevice(composite0); |
| [[maybe_unused]] auto dev2 = AddStreamConfigOutputDevice(output0); |
| [[maybe_unused]] auto dev3 = AddCodecDevice(codec0); |
| [[maybe_unused]] auto dev4 = AddStreamConfigInputDevice(input1); |
| [[maybe_unused]] auto dev5 = AddCompositeDevice(composite1); |
| [[maybe_unused]] auto dev6 = AddStreamConfigOutputDevice(output1); |
| [[maybe_unused]] auto dev7 = AddCodecDevice(codec1); |
| |
| auto tracker = std::make_shared<DeviceTracker>(dispatcher(), true); |
| RunLoopUntilIdle(); |
| ASSERT_EQ(0u, tracker->size()); |
| { |
| // Create the detector; expect 8 events (1 for each device above); |
| auto device_detector = DeviceDetector::Create(tracker->handler(), dispatcher()); |
| zx::time deadline = zx::clock::get_monotonic() + kCommandTimeout; |
| while (zx::clock::get_monotonic() < deadline) { |
| // A fake audio device could still be setting up its server end, by the time the |
| // tracker adds it. We wait for the tracker AND the server-ends, to avoid a race. |
| if (input0->is_bound() && input1->is_bound() && composite0->is_bound() && |
| composite1->is_bound() && output0->is_bound() && output1->is_bound() && |
| codec0->is_bound() && codec1->is_bound() && tracker->size() >= 8) { |
| break; |
| } |
| RunLoopUntilIdle(); |
| } |
| RunLoopUntilIdle(); // Allow erroneous extra device additions to reveal themselves. |
| ASSERT_EQ(tracker->size(), 8u) << "Timed out waiting for preexisting devices to be detected"; |
| |
| int num_inputs = 0, num_composites = 0, num_outputs = 0, num_codecs = 0; |
| for (auto dev_num = 0u; dev_num < tracker->size(); ++dev_num) { |
| auto& device = tracker->devices()[dev_num]; |
| if (device.device_type == DeviceType::kInput) { |
| EXPECT_EQ(device.client.Which(), DriverClient::Tag::kStreamConfig); |
| EXPECT_TRUE(device.client.stream_config()->is_valid()); |
| ++num_inputs; |
| } else if (device.device_type == DeviceType::kComposite) { |
| EXPECT_EQ(device.client.Which(), DriverClient::Tag::kComposite); |
| EXPECT_TRUE(device.client.composite()->is_valid()); |
| ++num_composites; |
| } else if (device.device_type == DeviceType::kOutput) { |
| EXPECT_EQ(device.client.Which(), DriverClient::Tag::kStreamConfig); |
| EXPECT_TRUE(device.client.stream_config()->is_valid()); |
| ++num_outputs; |
| } else if (device.device_type == DeviceType::kCodec) { |
| EXPECT_EQ(device.client.Which(), DriverClient::Tag::kCodec); |
| EXPECT_TRUE(device.client.codec()->is_valid()); |
| ++num_codecs; |
| } else { |
| ADD_FAILURE() << "Unknown device_type during test"; |
| } |
| } |
| EXPECT_EQ(num_inputs, 2); |
| EXPECT_EQ(num_composites, 2); |
| EXPECT_EQ(num_outputs, 2); |
| EXPECT_EQ(num_codecs, 2); |
| } |
| |
| RunLoopUntilIdle(); // Allow any erroneous device unbinds to reveal themselves. |
| |
| // After the detector is gone, preexisting devices we detected should still be bound. |
| std::for_each(tracker->devices().begin(), tracker->devices().end(), [](const auto& device) { |
| switch (device.device_type) { |
| case DeviceType::kCodec: |
| EXPECT_TRUE(device.client.codec()->is_valid()); |
| break; |
| case DeviceType::kComposite: |
| EXPECT_TRUE(device.client.composite()->is_valid()); |
| break; |
| case DeviceType::kInput: |
| case DeviceType::kOutput: |
| EXPECT_TRUE(device.client.stream_config()->is_valid()); |
| break; |
| case DeviceType::kDai: |
| default: |
| ADD_FAILURE() << "Unknown device_type after test"; |
| break; |
| } |
| }); |
| |
| EXPECT_TRUE(input0->is_bound()); |
| EXPECT_TRUE(input1->is_bound()); |
| EXPECT_TRUE(composite0->is_bound()); |
| EXPECT_TRUE(composite1->is_bound()); |
| EXPECT_TRUE(output0->is_bound()); |
| EXPECT_TRUE(output1->is_bound()); |
| EXPECT_TRUE(codec0->is_bound()); |
| EXPECT_TRUE(codec1->is_bound()); |
| } |
| |
| // For devices added after the plug detector, verify detection (and post-detector persistence). |
| TEST_F(DeviceDetectorTest, DetectHotplugDevices) { |
| auto output = std::make_shared<FakeAudioStreamConfig>(dispatcher()); |
| auto input = std::make_shared<FakeAudioStreamConfig>(dispatcher()); |
| auto composite = std::make_shared<FakeAudioComposite>(dispatcher()); |
| auto codec = std::make_shared<FakeAudioCodec>(dispatcher()); |
| |
| auto tracker = std::make_shared<DeviceTracker>(dispatcher(), true); |
| { |
| auto device_detector = DeviceDetector::Create(tracker->handler(), dispatcher()); |
| |
| RunLoopUntilIdle(); |
| ASSERT_EQ(0u, tracker->size()); |
| |
| // Hotplug a device of each type. |
| [[maybe_unused]] auto dev0 = AddStreamConfigOutputDevice(output); |
| zx::time deadline = zx::clock::get_monotonic() + kCommandTimeout; |
| while (zx::clock::get_monotonic() < deadline) { |
| // Wait for both tracker and device, same as above. |
| if (tracker->size() >= 1u && output->is_bound()) { |
| break; |
| } |
| RunLoopUntilIdle(); |
| } |
| RunLoopUntilIdle(); // Allow erroneous extra device additions to reveal themselves. |
| ASSERT_EQ(tracker->size(), 1u) << "Timed out waiting for output device to be detected"; |
| |
| [[maybe_unused]] auto dev1 = AddStreamConfigInputDevice(input); |
| deadline = zx::clock::get_monotonic() + kCommandTimeout; |
| while (zx::clock::get_monotonic() < deadline) { |
| // Wait for both tracker and device, same as above. |
| if (tracker->size() >= 2u && input->is_bound()) { |
| break; |
| } |
| RunLoopUntilIdle(); |
| } |
| RunLoopUntilIdle(); // Allow erroneous extra device additions to reveal themselves. |
| ASSERT_EQ(tracker->size(), 2u) << "Timed out waiting for input device to be detected"; |
| |
| [[maybe_unused]] auto dev2 = AddCompositeDevice(composite); |
| deadline = zx::clock::get_monotonic() + kCommandTimeout; |
| while (zx::clock::get_monotonic() < deadline) { |
| // Wait for both tracker and device, same as above. |
| if (tracker->size() >= 3u && composite->is_bound()) { |
| break; |
| } |
| RunLoopUntilIdle(); |
| } |
| RunLoopUntilIdle(); // Allow erroneous extra device additions to reveal themselves. |
| ASSERT_EQ(tracker->size(), 3u) << "Timed out waiting for composite device to be detected"; |
| |
| [[maybe_unused]] auto dev3 = AddCodecDevice(codec); |
| deadline = zx::clock::get_monotonic() + kCommandTimeout; |
| while (zx::clock::get_monotonic() < deadline) { |
| // Wait for both tracker and device, same as above. |
| if (tracker->size() >= 4u && codec->is_bound()) { |
| break; |
| } |
| RunLoopUntilIdle(); |
| } |
| RunLoopUntilIdle(); // Allow erroneous extra device additions to reveal themselves. |
| ASSERT_EQ(tracker->size(), 4u) << "Timed out waiting for codec device to be detected"; |
| |
| EXPECT_EQ(tracker->devices()[0].device_type, DeviceType::kOutput); |
| EXPECT_EQ(tracker->devices()[1].device_type, DeviceType::kInput); |
| EXPECT_EQ(tracker->devices()[2].device_type, DeviceType::kComposite); |
| EXPECT_EQ(tracker->devices()[3].device_type, DeviceType::kCodec); |
| } |
| |
| // After the device detector is gone, dynamically-detected devices should still be bound. |
| RunLoopUntilIdle(); // Allow any erroneous device unbinds to reveal themselves. |
| |
| std::for_each(tracker->devices().begin(), tracker->devices().end(), [](const auto& device) { |
| switch (device.device_type) { |
| case DeviceType::kCodec: |
| EXPECT_TRUE(device.client.codec()->is_valid()); |
| break; |
| case DeviceType::kComposite: |
| EXPECT_TRUE(device.client.composite()->is_valid()); |
| break; |
| case DeviceType::kInput: |
| case DeviceType::kOutput: |
| EXPECT_TRUE(device.client.stream_config()->is_valid()); |
| break; |
| case DeviceType::kDai: |
| default: |
| ADD_FAILURE() << "Unknown device_type after test"; |
| break; |
| } |
| }); |
| |
| EXPECT_TRUE(output->is_bound()); |
| EXPECT_TRUE(input->is_bound()); |
| EXPECT_TRUE(composite->is_bound()); |
| EXPECT_TRUE(codec->is_bound()); |
| } |
| |
| // Ensure that once the plug detector is destroyed, detection handlers are no longer called. |
| TEST_F(DeviceDetectorTest, NoDanglingDetectors) { |
| auto codec = std::make_shared<FakeAudioCodec>(dispatcher()); |
| auto composite = std::make_shared<FakeAudioComposite>(dispatcher()); |
| auto input = std::make_shared<FakeAudioStreamConfig>(dispatcher()); |
| auto output = std::make_shared<FakeAudioStreamConfig>(dispatcher()); |
| auto tracker = std::make_shared<DeviceTracker>(dispatcher(), false); |
| |
| { |
| auto device_detector = DeviceDetector::Create(tracker->handler(), dispatcher()); |
| RunLoopUntilIdle(); // Allow erroneous device handler callbacks to reveal themselves. |
| ASSERT_EQ(0u, tracker->size()); |
| } |
| // After the device detector is gone, additional devices should not be detected. |
| |
| // Hotplug an input device, an output device and a codec device. |
| // If a device-detection handler is still in place, these will be inserted into tracker's list. |
| [[maybe_unused]] auto dev0 = AddCodecDevice(codec); |
| [[maybe_unused]] auto dev1 = AddCompositeDevice(composite); |
| [[maybe_unused]] auto dev2 = AddStreamConfigInputDevice(input); |
| [[maybe_unused]] auto dev3 = AddStreamConfigOutputDevice(output); |
| |
| RunLoopUntilIdle(); // Allow erroneous device handler callbacks to reveal themselves. |
| EXPECT_EQ(0u, tracker->size()); |
| EXPECT_FALSE(codec->is_bound()); |
| EXPECT_FALSE(composite->is_bound()); |
| EXPECT_FALSE(input->is_bound()); |
| EXPECT_FALSE(output->is_bound()); |
| } |
| |
| } // namespace |
| |
| } // namespace media_audio |