| // Copyright 2020 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/drivers/test/admin_test.h" |
| |
| #include <lib/fzl/vmo-mapper.h> |
| #include <lib/zx/vmo.h> |
| #include <zircon/compiler.h> |
| |
| #include <algorithm> |
| #include <cstring> |
| |
| #include "lib/zx/time.h" |
| #include "src/media/audio/lib/logging/logging.h" |
| |
| namespace media::audio::drivers::test { |
| |
| // For the channelization and sample_format that we've set, determine the size of each frame. |
| // This method assumes that SetFormat has already been sent to the driver. |
| void AdminTest::CalculateFrameSize() { |
| if (!format_is_set_) { |
| return; |
| } |
| EXPECT_LE(pcm_format_.valid_bits_per_sample, pcm_format_.bytes_per_sample * 8); |
| frame_size_ = pcm_format_.number_of_channels * pcm_format_.bytes_per_sample; |
| } |
| |
| void AdminTest::SelectFirstFormat() { |
| ASSERT_NE(pcm_formats().size(), 0u); |
| |
| auto& first_format = pcm_formats()[0]; |
| pcm_format_.number_of_channels = first_format.number_of_channels[0]; |
| pcm_format_.channels_to_use_bitmask = (1 << pcm_format_.number_of_channels) - 1; // Use all. |
| pcm_format_.sample_format = first_format.sample_formats[0]; |
| pcm_format_.bytes_per_sample = first_format.bytes_per_sample[0]; |
| pcm_format_.valid_bits_per_sample = first_format.valid_bits_per_sample[0]; |
| pcm_format_.frame_rate = first_format.frame_rates[0]; |
| } |
| |
| void AdminTest::SelectLastFormat() { |
| ASSERT_NE(pcm_formats().size(), 0u); |
| |
| auto& last_format = pcm_formats()[pcm_formats().size() - 1]; |
| pcm_format_.number_of_channels = |
| last_format.number_of_channels[last_format.number_of_channels.size() - 1]; |
| pcm_format_.channels_to_use_bitmask = (1 << pcm_format_.number_of_channels) - 1; // Use all. |
| pcm_format_.sample_format = last_format.sample_formats[last_format.sample_formats.size() - 1]; |
| pcm_format_.bytes_per_sample = |
| last_format.bytes_per_sample[last_format.bytes_per_sample.size() - 1]; |
| pcm_format_.valid_bits_per_sample = |
| last_format.valid_bits_per_sample[last_format.valid_bits_per_sample.size() - 1]; |
| pcm_format_.frame_rate = last_format.frame_rates[last_format.frame_rates.size() - 1]; |
| } |
| |
| void AdminTest::RequestRingBufferChannel() { |
| fuchsia::hardware::audio::Format format = {}; |
| format.set_pcm_format(pcm_format_); |
| |
| fidl::InterfaceHandle<fuchsia::hardware::audio::RingBuffer> ring_buffer_handle; |
| stream_config()->CreateRingBuffer(std::move(format), ring_buffer_handle.NewRequest()); |
| |
| zx::channel channel = ring_buffer_handle.TakeChannel(); |
| ring_buffer_ = |
| fidl::InterfaceHandle<fuchsia::hardware::audio::RingBuffer>(std::move(channel)).Bind(); |
| ring_buffer_.set_error_handler([this](zx_status_t status) { |
| set_failed(); |
| if (status == ZX_ERR_PEER_CLOSED) { |
| FAIL() << "RingBuffer channel error " << status |
| << ": is another client already connected to the RingBuffer interface?"; |
| } |
| FAIL() << "RingBuffer channel error " << status; |
| }); |
| |
| if (!stream_config().is_bound() || !ring_buffer_.is_bound()) { |
| FAIL() << "Failed to get ring buffer channel"; |
| } |
| |
| format_is_set_ = true; |
| ring_buffer_ready_ = true; |
| } |
| |
| // Request that driver set format to the lowest rate/channelization of the first range reported. |
| // This method assumes that the driver has already successfully responded to a GetFormats request. |
| void AdminTest::RequestMinFormat() { |
| ASSERT_TRUE(received_get_formats()); |
| ASSERT_GT(pcm_formats().size(), 0u); |
| |
| SelectFirstFormat(); |
| RequestRingBufferChannel(); |
| CalculateFrameSize(); |
| } |
| |
| // Request that driver set the highest rate/channelization of the final range reported. This method |
| // assumes that the driver has already successfully responded to a GetFormats request. |
| void AdminTest::RequestMaxFormat() { |
| ASSERT_TRUE(received_get_formats()); |
| ASSERT_GT(pcm_formats().size(), 0u); |
| |
| SelectLastFormat(); |
| RequestRingBufferChannel(); |
| CalculateFrameSize(); |
| } |
| |
| // Ring-buffer channel requests |
| // |
| // Request the FIFO depth in bytes, at the current format (relies on the ring buffer channel). |
| void AdminTest::RequestRingBufferProperties() { |
| ASSERT_TRUE(ring_buffer_ready_); |
| |
| bool received_get_ring_buffer_properties = false; |
| ring_buffer_->GetProperties([this, &received_get_ring_buffer_properties]( |
| fuchsia::hardware::audio::RingBufferProperties prop) { |
| ring_buffer_props_ = std::move(prop); |
| |
| received_get_ring_buffer_properties = true; |
| }); |
| |
| // ring_buffer_->GetProperties can return an error, so we check for failed as well |
| RunLoopUntil([this, &received_get_ring_buffer_properties]() { |
| return received_get_ring_buffer_properties || failed(); |
| }); |
| } |
| |
| // Request the ring buffer's VMO handle, at the current format (relies on the ring buffer channel). |
| void AdminTest::RequestBuffer(uint32_t min_ring_buffer_frames, uint32_t notifications_per_ring) { |
| min_ring_buffer_frames_ = min_ring_buffer_frames; |
| notifications_per_ring_ = notifications_per_ring; |
| zx::vmo ring_buffer_vmo; |
| ring_buffer_->GetVmo( |
| min_ring_buffer_frames, notifications_per_ring, |
| [this, &ring_buffer_vmo](fuchsia::hardware::audio::RingBuffer_GetVmo_Result result) { |
| EXPECT_GE(result.response().num_frames, min_ring_buffer_frames_); |
| ring_buffer_frames_ = result.response().num_frames; |
| ring_buffer_vmo = std::move(result.response().ring_buffer); |
| EXPECT_TRUE(ring_buffer_vmo.is_valid()); |
| received_get_buffer_ = true; |
| }); |
| |
| RunLoopUntil([this]() { return received_get_buffer_ || failed(); }); |
| if (failed()) { |
| return; |
| } |
| |
| ring_buffer_mapper_.Unmap(); |
| const zx_vm_option_t option_flags = ZX_VM_PERM_READ | ZX_VM_PERM_WRITE; |
| EXPECT_EQ(ring_buffer_mapper_.CreateAndMap(ring_buffer_frames_ * frame_size_, option_flags, |
| nullptr, &ring_buffer_vmo, |
| ZX_RIGHT_READ | ZX_RIGHT_MAP | ZX_RIGHT_TRANSFER), |
| ZX_OK); |
| } |
| |
| void AdminTest::SetPositionNotification() { |
| watch_for_next_position_notification_ = true; |
| |
| ring_buffer_->WatchClockRecoveryPositionInfo( |
| [this](fuchsia::hardware::audio::RingBufferPositionInfo position_info) { |
| // If, after being dispatched, we were cancelled, then leave now. |
| if (!watch_for_next_position_notification_) { |
| return; |
| } |
| |
| EXPECT_GT(notifications_per_ring_, 0u); |
| |
| auto now = zx::clock::get_monotonic().get(); |
| EXPECT_LT(start_time_, now); |
| EXPECT_LT(position_info.timestamp, now); |
| |
| if (position_notification_count_) { |
| EXPECT_GT(position_info.timestamp, start_time_); |
| EXPECT_GT(position_info.timestamp, position_info_.timestamp); |
| } else { |
| EXPECT_GE(position_info.timestamp, start_time_); |
| } |
| running_position_ += position_info.position; |
| running_position_ -= position_info_.position; |
| |
| if (position_info.position <= position_info_.position) { |
| running_position_ += (ring_buffer_frames_ * frame_size_); |
| } |
| position_info_.timestamp = position_info.timestamp; |
| position_info_.position = position_info.position; |
| EXPECT_LT(position_info_.position, ring_buffer_frames_ * frame_size_); |
| |
| ++position_notification_count_; |
| |
| SetPositionNotification(); |
| }); |
| } |
| |
| // Set a position notification that, if called, automatically failed. We use this when we expect to |
| // NOT receive any position notifications. Although calling WatchClockRecoveryPositionInfo should |
| // clear any previously registered callback, the callback might already be dispatched to us and |
| // waiting, so we first clear the watch_for_next_position_notification_ flag to 'nerf' it. |
| void AdminTest::SetFailingPositionNotification() { |
| // Disable any last pending notification from re-queueing itself. |
| watch_for_next_position_notification_ = false; |
| |
| ring_buffer_->WatchClockRecoveryPositionInfo( |
| [](fuchsia::hardware::audio::RingBufferPositionInfo) { |
| FAIL() << "Unexpected position notification received"; |
| }); |
| } |
| |
| // As mentioned above, a previous callback might be dispatched and pending, so we 'nerf' it by |
| // clearing watch_for_next_position_notification_. Either way, we register a callback that does not |
| // register for another notification nor contribute to position calculations or notification counts. |
| void AdminTest::ClearPositionNotification() { |
| watch_for_next_position_notification_ = false; |
| |
| ring_buffer_->WatchClockRecoveryPositionInfo( |
| [](fuchsia::hardware::audio::RingBufferPositionInfo) {}); |
| } |
| |
| // Request that the driver start the ring buffer engine, responding with the start_time. |
| // This method assumes that the ring buffer VMO was received in a successful GetBuffer response. |
| void AdminTest::RequestStart() { |
| ASSERT_TRUE(ring_buffer_ready_); |
| |
| auto send_time = zx::clock::get_monotonic().get(); |
| ring_buffer_->Start([this](int64_t start_time) { |
| start_time_ = start_time; |
| received_start_ = true; |
| }); |
| RunLoopUntil([this]() { return received_start_ || failed(); }); |
| if (failed()) { |
| return; |
| } |
| |
| EXPECT_GT(start_time_, send_time); |
| } |
| |
| // Request that driver stop the ring buffer, including quieting position notifications. |
| // This method assumes that the ring buffer engine has previously been successfully started. |
| void AdminTest::RequestStop() { |
| if (failed()) { |
| return; |
| } |
| ASSERT_TRUE(received_start_); |
| |
| ring_buffer_->Stop([this]() { received_stop_ = true; }); |
| RunLoopUntil([this]() { return received_stop_ || failed(); }); |
| } |
| |
| // After Stop is called, no position notification should be received. |
| // To validate this without any race windows: from within the next position notification itself, |
| // we call Stop and register a position notification that FAILs if called. |
| // Within the next position notification, Stop and fail on any subsequent position notification. |
| void AdminTest::RequestStopAndExpectNoPositionNotifications() { |
| if (failed()) { |
| return; |
| } |
| |
| ASSERT_TRUE(received_start_); |
| |
| // Disable any last pending notification from re-queueing itself. |
| watch_for_next_position_notification_ = false; |
| |
| ring_buffer_->WatchClockRecoveryPositionInfo( |
| [this](fuchsia::hardware::audio::RingBufferPositionInfo) { |
| ring_buffer_->Stop([this]() { received_stop_ = true; }); |
| |
| SetFailingPositionNotification(); |
| }); |
| |
| RunLoopUntil([this]() { return received_stop_ || failed(); }); |
| |
| // We should NOT receive further position notifications. If we do, it triggers an error. |
| } |
| |
| // Wait for the specified number of position notifications. |
| void AdminTest::ExpectPositionNotifyCount(uint32_t count) { |
| RunLoopUntil([this, count]() { return position_notification_count_ >= count || failed(); }); |
| if (failed()) { |
| return; |
| } |
| |
| ClearPositionNotification(); |
| |
| auto timestamp_duration = position_info_.timestamp - start_time_; |
| auto observed_duration = zx::clock::get_monotonic().get() - start_time_; |
| |
| ASSERT_GT(position_notification_count_, 0u) << "No position notifications received"; |
| ASSERT_GE(position_notification_count_, count) << "Too few position notifications received"; |
| |
| // What timestamp do we expect, for the final notification received? We know how many |
| // notifications we've received; we'll multiply this by the per-notification time duration. |
| // However, upon enabling notifications, our first notification might arrive immediately. Thus, |
| // the average number of notification periods elapsed is (position_notification_count_ - 0.5). |
| ASSERT_NE(pcm_format_.frame_rate * notifications_per_ring_, 0u); |
| auto ns_per_notification = |
| (zx::sec(1) * ring_buffer_frames_) / (pcm_format_.frame_rate * notifications_per_ring_); |
| double average_num_notif_periods_elapsed = position_notification_count_ - 0.5; |
| |
| // Furthermore, notification timing requirements for drivers are somewhat loose, so we include |
| // a tolerance range of +/- 2. notification periods. |
| auto expected_time = ns_per_notification.get() * average_num_notif_periods_elapsed; |
| auto timing_tolerance = ns_per_notification.get() * 2; |
| auto min_allowed_time = expected_time - timing_tolerance; |
| auto max_allowed_time = expected_time + timing_tolerance; |
| |
| EXPECT_GE(timestamp_duration, min_allowed_time) |
| << "Notification rate too high. Device clock rate too fast?"; |
| EXPECT_LE(timestamp_duration, max_allowed_time) |
| << "Notification rate too low. Device clock rate too slow?"; |
| |
| // Also validate when the notification was actually received (not just the timestamp). |
| EXPECT_GT(observed_duration, min_allowed_time); |
| } |
| |
| #define DEFINE_ADMIN_TEST_CLASS(CLASS_NAME, CODE) \ |
| class CLASS_NAME : public AdminTest { \ |
| public: \ |
| explicit CLASS_NAME(const DeviceEntry& dev_entry) : AdminTest(dev_entry) {} \ |
| void TestBody() override { CODE } \ |
| } |
| |
| // Test cases that target each of the various admin commands |
| |
| // Verify valid responses: ring buffer properties, get buffer, ring buffer VMO. |
| DEFINE_ADMIN_TEST_CLASS(GetRingBufferProperties, { |
| RequestFormats(); |
| RequestMaxFormat(); |
| RequestRingBufferProperties(); |
| }); |
| DEFINE_ADMIN_TEST_CLASS(GetBuffer, { |
| RequestFormats(); |
| RequestMinFormat(); |
| RequestBuffer(100, 1); |
| }); |
| |
| // Verify that valid start and stop responses are successfully received. |
| DEFINE_ADMIN_TEST_CLASS(Start, { |
| RequestFormats(); |
| RequestMinFormat(); |
| RequestBuffer(32000, 0); |
| RequestStart(); |
| }); |
| DEFINE_ADMIN_TEST_CLASS(Stop, { |
| RequestFormats(); |
| RequestMinFormat(); |
| RequestBuffer(100, 0); |
| RequestStart(); |
| RequestStop(); |
| WaitForError(); |
| }); |
| |
| // Verify position notifications at fast (~180/sec) and slow (2/sec) rate. |
| DEFINE_ADMIN_TEST_CLASS(PositionNotifyFast, { |
| RequestFormats(); |
| RequestMaxFormat(); |
| RequestBuffer(8000, 32); |
| SetPositionNotification(); |
| RequestStart(); |
| ExpectPositionNotifyCount(16); |
| WaitForError(); |
| }); |
| // Notifications arrive every 500 msec; we must wait longer than the default 100 msec. |
| DEFINE_ADMIN_TEST_CLASS(PositionNotifySlow, { |
| RequestFormats(); |
| RequestMinFormat(); |
| RequestBuffer(48000, 2); |
| SetPositionNotification(); |
| RequestStart(); |
| ExpectPositionNotifyCount(3); |
| WaitForError(zx::msec(600)); |
| }); |
| |
| // Verify no position notifications arrive after stop, or if notifications_per_ring is 0. |
| DEFINE_ADMIN_TEST_CLASS(NoPositionNotifyAfterStop, { |
| RequestFormats(); |
| RequestMaxFormat(); |
| RequestBuffer(8000, 32); |
| SetPositionNotification(); |
| RequestStart(); |
| ExpectPositionNotifyCount(3); |
| RequestStopAndExpectNoPositionNotifications(); |
| WaitForError(); |
| }); |
| DEFINE_ADMIN_TEST_CLASS(PositionNotifyNone, { |
| RequestFormats(); |
| RequestMaxFormat(); |
| RequestBuffer(8000, 0); |
| SetFailingPositionNotification(); |
| RequestStart(); |
| WaitForError(); |
| }); |
| |
| // Register separate test case instances for each enumerated device |
| // |
| // See googletest/docs/advanced.md for details |
| #define REGISTER_ADMIN_TEST(CLASS_NAME, DEVICE) \ |
| testing::RegisterTest("AdminTest", TestNameForEntry(#CLASS_NAME, DEVICE).c_str(), nullptr, \ |
| DevNameForEntry(DEVICE).c_str(), __FILE__, __LINE__, \ |
| [=]() -> AdminTest* { return new CLASS_NAME(DEVICE); }) |
| |
| void RegisterAdminTestsForDevice(const DeviceEntry& device_entry, |
| bool expect_audio_core_connected) { |
| // If audio_core is connected to the audio driver, admin tests will fail. |
| // We test a hermetic instance of the A2DP driver, so audio_core is never connected. |
| if (!expect_audio_core_connected || device_entry.dir_fd == DeviceEntry::kA2dp) { |
| REGISTER_ADMIN_TEST(GetRingBufferProperties, device_entry); |
| REGISTER_ADMIN_TEST(GetBuffer, device_entry); |
| REGISTER_ADMIN_TEST(Start, device_entry); |
| REGISTER_ADMIN_TEST(Stop, device_entry); |
| |
| // For now, the following test cases fail on a2dp-source. |
| // TODO(fxbug.dev/66431): fix a2dp-source and enable these test cases for all devices. |
| if (device_entry.dir_fd != DeviceEntry::kA2dp) { |
| REGISTER_ADMIN_TEST(PositionNotifyFast, device_entry); |
| REGISTER_ADMIN_TEST(PositionNotifySlow, device_entry); |
| REGISTER_ADMIN_TEST(NoPositionNotifyAfterStop, device_entry); |
| } |
| |
| REGISTER_ADMIN_TEST(PositionNotifyNone, device_entry); |
| } |
| } |
| |
| } // namespace media::audio::drivers::test |