From e5d747e90894f21d175017e49bb8ef6ef2071f7c Mon Sep 17 00:00:00 2001 From: Mikhail Naganov Date: Tue, 16 Nov 2021 01:31:03 +0000 Subject: [PATCH] audio HAL: initial VTS tests Tests basic functionality for enumerating capabilities of an audio module, audio patches creation, and opening of I/O streams. Bug: 205884982 Test: atest VtsHalAudioCoreTargetTest Merged-In: I7c7c3c7008f2fc43db1542455c74444a08e55534 Change-Id: I7c7c3c7008f2fc43db1542455c74444a08e55534 (cherry picked from commit 7abc70f9085e56aac85c97b30c8c4c1a3b97b63f) --- audio/aidl/vts/Android.bp | 32 + audio/aidl/vts/ModuleConfig.cpp | 330 ++++++ audio/aidl/vts/ModuleConfig.h | 131 +++ audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp | 1027 ++++++++++++++++++ 4 files changed, 1520 insertions(+) create mode 100644 audio/aidl/vts/Android.bp create mode 100644 audio/aidl/vts/ModuleConfig.cpp create mode 100644 audio/aidl/vts/ModuleConfig.h create mode 100644 audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp diff --git a/audio/aidl/vts/Android.bp b/audio/aidl/vts/Android.bp new file mode 100644 index 0000000000..c160d1f5cc --- /dev/null +++ b/audio/aidl/vts/Android.bp @@ -0,0 +1,32 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "hardware_interfaces_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["hardware_interfaces_license"], +} + +cc_test { + name: "VtsHalAudioCoreTargetTest", + defaults: [ + "VtsHalTargetTestDefaults", + "use_libaidlvintf_gtest_helper_static", + ], + srcs: [ + "ModuleConfig.cpp", + "VtsHalAudioCoreTargetTest.cpp", + ], + shared_libs: [ + "libbinder", + ], + static_libs: [ + "android.hardware.audio.common-V1-cpp", + "android.hardware.audio.core-V1-cpp", + "android.media.audio.common.types-V1-cpp", + ], + test_suites: [ + "general-tests", + "vts", + ], +} diff --git a/audio/aidl/vts/ModuleConfig.cpp b/audio/aidl/vts/ModuleConfig.cpp new file mode 100644 index 0000000000..3faa39ab4f --- /dev/null +++ b/audio/aidl/vts/ModuleConfig.cpp @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#include "ModuleConfig.h" + +using namespace android; + +using android::hardware::audio::core::IModule; +using android::media::audio::common::AudioChannelLayout; +using android::media::audio::common::AudioFormatDescription; +using android::media::audio::common::AudioFormatType; +using android::media::audio::common::AudioIoFlags; +using android::media::audio::common::AudioOutputFlags; +using android::media::audio::common::AudioPort; +using android::media::audio::common::AudioPortConfig; +using android::media::audio::common::AudioPortExt; +using android::media::audio::common::AudioProfile; +using android::media::audio::common::Int; + +template +auto findById(const std::vector& v, int32_t id) { + return std::find_if(v.begin(), v.end(), [&](const auto& p) { return p.id == id; }); +} + +ModuleConfig::ModuleConfig(IModule* module) { + mStatus = module->getAudioPorts(&mPorts); + if (!mStatus.isOk()) return; + for (const auto& port : mPorts) { + if (port.ext.getTag() != AudioPortExt::Tag::device) continue; + const auto& devicePort = port.ext.get(); + const bool isInput = port.flags.getTag() == AudioIoFlags::Tag::input; + if (devicePort.device.type.connection.empty()) { + // Permanently attached device. + if (isInput) { + mAttachedSourceDevicePorts.insert(port.id); + } else { + mAttachedSinkDevicePorts.insert(port.id); + } + } + } + if (!mStatus.isOk()) return; + mStatus = module->getAudioRoutes(&mRoutes); + if (!mStatus.isOk()) return; + mStatus = module->getAudioPortConfigs(&mInitialConfigs); +} + +std::vector ModuleConfig::getInputMixPorts() const { + std::vector result; + std::copy_if(mPorts.begin(), mPorts.end(), std::back_inserter(result), [](const auto& port) { + return port.ext.getTag() == AudioPortExt::Tag::mix && + port.flags.getTag() == AudioIoFlags::Tag::input; + }); + return result; +} + +std::vector ModuleConfig::getOutputMixPorts() const { + std::vector result; + std::copy_if(mPorts.begin(), mPorts.end(), std::back_inserter(result), [](const auto& port) { + return port.ext.getTag() == AudioPortExt::Tag::mix && + port.flags.getTag() == AudioIoFlags::Tag::output; + }); + return result; +} + +std::vector ModuleConfig::getAttachedSinkDevicesPortsForMixPort( + const AudioPort& mixPort) const { + std::vector result; + for (const auto& route : mRoutes) { + if (mAttachedSinkDevicePorts.count(route.sinkPortId) != 0 && + std::find(route.sourcePortIds.begin(), route.sourcePortIds.end(), mixPort.id) != + route.sourcePortIds.end()) { + const auto devicePortIt = findById(mPorts, route.sinkPortId); + if (devicePortIt != mPorts.end()) result.push_back(*devicePortIt); + } + } + return result; +} + +std::vector ModuleConfig::getAttachedSourceDevicesPortsForMixPort( + const AudioPort& mixPort) const { + std::vector result; + for (const auto& route : mRoutes) { + if (route.sinkPortId == mixPort.id) { + for (const auto srcId : route.sourcePortIds) { + if (mAttachedSourceDevicePorts.count(srcId) != 0) { + const auto devicePortIt = findById(mPorts, srcId); + if (devicePortIt != mPorts.end()) result.push_back(*devicePortIt); + } + } + } + } + return result; +} + +std::optional ModuleConfig::getSourceMixPortForAttachedDevice() const { + for (const auto& route : mRoutes) { + if (mAttachedSinkDevicePorts.count(route.sinkPortId) != 0) { + const auto mixPortIt = findById(mPorts, route.sourcePortIds[0]); + if (mixPortIt != mPorts.end()) return *mixPortIt; + } + } + return {}; +} + +std::optional ModuleConfig::getNonRoutableSrcSinkPair( + bool isInput) const { + const auto mixPorts = getMixPorts(isInput); + std::set> allowedRoutes; + for (const auto& route : mRoutes) { + for (const auto srcPortId : route.sourcePortIds) { + allowedRoutes.emplace(std::make_pair(srcPortId, route.sinkPortId)); + } + } + auto make_pair = [isInput](auto& device, auto& mix) { + return isInput ? std::make_pair(device, mix) : std::make_pair(mix, device); + }; + for (const auto portId : isInput ? mAttachedSourceDevicePorts : mAttachedSinkDevicePorts) { + const auto devicePortIt = findById(mPorts, portId); + if (devicePortIt == mPorts.end()) continue; + auto devicePortConfig = getSingleConfigForDevicePort(*devicePortIt); + for (const auto& mixPort : mixPorts) { + if (std::find(allowedRoutes.begin(), allowedRoutes.end(), + make_pair(portId, mixPort.id)) == allowedRoutes.end()) { + auto mixPortConfig = getSingleConfigForMixPort(isInput, mixPort); + if (mixPortConfig.has_value()) { + return make_pair(devicePortConfig, mixPortConfig.value()); + } + } + } + } + return {}; +} + +std::optional ModuleConfig::getRoutableSrcSinkPair(bool isInput) const { + if (isInput) { + for (const auto& route : mRoutes) { + auto srcPortIdIt = std::find_if( + route.sourcePortIds.begin(), route.sourcePortIds.end(), + [&](const auto& portId) { return mAttachedSourceDevicePorts.count(portId); }); + if (srcPortIdIt == route.sourcePortIds.end()) continue; + const auto devicePortIt = findById(mPorts, *srcPortIdIt); + const auto mixPortIt = findById(mPorts, route.sinkPortId); + if (devicePortIt == mPorts.end() || mixPortIt == mPorts.end()) continue; + auto devicePortConfig = getSingleConfigForDevicePort(*devicePortIt); + auto mixPortConfig = getSingleConfigForMixPort(isInput, *mixPortIt); + if (!mixPortConfig.has_value()) continue; + return std::make_pair(devicePortConfig, mixPortConfig.value()); + } + } else { + for (const auto& route : mRoutes) { + if (mAttachedSinkDevicePorts.count(route.sinkPortId) == 0) continue; + const auto mixPortIt = findById(mPorts, route.sourcePortIds[0]); + const auto devicePortIt = findById(mPorts, route.sinkPortId); + if (devicePortIt == mPorts.end() || mixPortIt == mPorts.end()) continue; + auto mixPortConfig = getSingleConfigForMixPort(isInput, *mixPortIt); + auto devicePortConfig = getSingleConfigForDevicePort(*devicePortIt); + if (!mixPortConfig.has_value()) continue; + return std::make_pair(mixPortConfig.value(), devicePortConfig); + } + } + return {}; +} + +std::vector ModuleConfig::getRoutableSrcSinkGroups(bool isInput) const { + std::vector result; + if (isInput) { + for (const auto& route : mRoutes) { + std::vector srcPortIds; + std::copy_if(route.sourcePortIds.begin(), route.sourcePortIds.end(), + std::back_inserter(srcPortIds), [&](const auto& portId) { + return mAttachedSourceDevicePorts.count(portId); + }); + if (srcPortIds.empty()) continue; + const auto mixPortIt = findById(mPorts, route.sinkPortId); + if (mixPortIt == mPorts.end()) continue; + auto mixPortConfig = getSingleConfigForMixPort(isInput, *mixPortIt); + if (!mixPortConfig.has_value()) continue; + std::vector pairs; + for (const auto srcPortId : srcPortIds) { + const auto devicePortIt = findById(mPorts, srcPortId); + if (devicePortIt == mPorts.end()) continue; + // Using all configs for every source would be too much. + auto devicePortConfig = getSingleConfigForDevicePort(*devicePortIt); + pairs.emplace_back(devicePortConfig, mixPortConfig.value()); + } + if (!pairs.empty()) { + result.emplace_back(route, std::move(pairs)); + } + } + } else { + for (const auto& route : mRoutes) { + if (mAttachedSinkDevicePorts.count(route.sinkPortId) == 0) continue; + const auto devicePortIt = findById(mPorts, route.sinkPortId); + if (devicePortIt == mPorts.end()) continue; + auto devicePortConfig = getSingleConfigForDevicePort(*devicePortIt); + std::vector pairs; + for (const auto srcPortId : route.sourcePortIds) { + const auto mixPortIt = findById(mPorts, srcPortId); + if (mixPortIt == mPorts.end()) continue; + // Using all configs for every source would be too much. + auto mixPortConfig = getSingleConfigForMixPort(isInput, *mixPortIt); + if (mixPortConfig.has_value()) { + pairs.emplace_back(mixPortConfig.value(), devicePortConfig); + } + } + if (!pairs.empty()) { + result.emplace_back(route, std::move(pairs)); + } + } + } + return result; +} + +static std::vector combineAudioConfigs(const AudioPort& port, + const AudioProfile& profile) { + std::vector configs; + configs.reserve(profile.channelMasks.size() * profile.sampleRates.size()); + for (auto channelMask : profile.channelMasks) { + for (auto sampleRate : profile.sampleRates) { + AudioPortConfig config{}; + config.portId = port.id; + Int sr; + sr.value = sampleRate; + config.sampleRate = sr; + config.channelMask = channelMask; + config.format = profile.format; + config.ext = port.ext; + configs.push_back(config); + } + } + return configs; +} + +std::vector ModuleConfig::generateInputAudioMixPortConfigs( + const std::vector& ports, bool singleProfile) const { + std::vector result; + for (const auto& mixPort : ports) { + if (getAttachedSourceDevicesPortsForMixPort(mixPort).empty()) { + continue; // no attached devices + } + for (const auto& profile : mixPort.profiles) { + if (profile.format.type == AudioFormatType::DEFAULT || profile.sampleRates.empty() || + profile.channelMasks.empty()) { + continue; // dynamic profile + } + auto configs = combineAudioConfigs(mixPort, profile); + for (auto& config : configs) { + config.flags = mixPort.flags; + result.push_back(config); + if (singleProfile) return result; + } + } + } + return result; +} + +static std::tuple generateOutFlags(const AudioPort& mixPort) { + static const AudioIoFlags offloadFlags = AudioIoFlags::make( + (1 << static_cast(AudioOutputFlags::COMPRESS_OFFLOAD)) | + (1 << static_cast(AudioOutputFlags::DIRECT))); + const bool isOffload = (mixPort.flags.get() & + (1 << static_cast(AudioOutputFlags::COMPRESS_OFFLOAD))) != 0; + return {isOffload ? offloadFlags : mixPort.flags, isOffload}; +} + +std::vector ModuleConfig::generateOutputAudioMixPortConfigs( + const std::vector& ports, bool singleProfile) const { + std::vector result; + for (const auto& mixPort : ports) { + if (getAttachedSinkDevicesPortsForMixPort(mixPort).empty()) { + continue; // no attached devices + } + auto [flags, isOffload] = generateOutFlags(mixPort); + (void)isOffload; + for (const auto& profile : mixPort.profiles) { + if (profile.format.type == AudioFormatType::DEFAULT) continue; + auto configs = combineAudioConfigs(mixPort, profile); + for (auto& config : configs) { + // Some combinations of flags declared in the config file require special + // treatment. + // if (isOffload) { + // config.offloadInfo.info(generateOffloadInfo(config.base)); + // } + config.flags = flags; + result.push_back(config); + if (singleProfile) return result; + } + } + } + return result; +} + +std::vector ModuleConfig::generateAudioDevicePortConfigs( + const std::vector& ports, bool singleProfile) const { + std::vector result; + for (const auto& devicePort : ports) { + const size_t resultSizeBefore = result.size(); + for (const auto& profile : devicePort.profiles) { + auto configs = combineAudioConfigs(devicePort, profile); + result.insert(result.end(), configs.begin(), configs.end()); + if (singleProfile && !result.empty()) return result; + } + if (resultSizeBefore == result.size()) { + AudioPortConfig empty; + empty.portId = devicePort.id; + empty.ext = devicePort.ext; + result.push_back(empty); + } + if (singleProfile) return result; + } + return result; +} diff --git a/audio/aidl/vts/ModuleConfig.h b/audio/aidl/vts/ModuleConfig.h new file mode 100644 index 0000000000..2e86b97cf5 --- /dev/null +++ b/audio/aidl/vts/ModuleConfig.h @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +class ModuleConfig { + public: + using SrcSinkPair = std::pair; + using SrcSinkGroup = + std::pair>; + + explicit ModuleConfig(android::hardware::audio::core::IModule* module); + android::binder::Status getStatus() const { return mStatus; } + std::string getError() const { return mStatus.toString8().c_str(); } + + std::vector getInputMixPorts() const; + std::vector getOutputMixPorts() const; + std::vector getMixPorts(bool isInput) const { + return isInput ? getInputMixPorts() : getOutputMixPorts(); + } + + std::vector getAttachedDevicesPortsForMixPort( + bool isInput, const android::media::audio::common::AudioPort& mixPort) const { + return isInput ? getAttachedSourceDevicesPortsForMixPort(mixPort) + : getAttachedSinkDevicesPortsForMixPort(mixPort); + } + std::vector getAttachedSinkDevicesPortsForMixPort( + const android::media::audio::common::AudioPort& mixPort) const; + std::vector getAttachedSourceDevicesPortsForMixPort( + const android::media::audio::common::AudioPort& mixPort) const; + std::optional getSourceMixPortForAttachedDevice() + const; + + std::optional getNonRoutableSrcSinkPair(bool isInput) const; + std::optional getRoutableSrcSinkPair(bool isInput) const; + std::vector getRoutableSrcSinkGroups(bool isInput) const; + + std::vector getPortConfigsForMixPorts() const { + auto inputs = generateInputAudioMixPortConfigs(getInputMixPorts(), false); + auto outputs = generateOutputAudioMixPortConfigs(getOutputMixPorts(), false); + inputs.insert(inputs.end(), outputs.begin(), outputs.end()); + return inputs; + } + std::vector getPortConfigsForMixPorts( + bool isInput) const { + return isInput ? generateInputAudioMixPortConfigs(getInputMixPorts(), false) + : generateOutputAudioMixPortConfigs(getOutputMixPorts(), false); + } + std::vector getPortConfigsForMixPorts( + bool isInput, const android::media::audio::common::AudioPort& port) const { + return isInput ? generateInputAudioMixPortConfigs({port}, false) + : generateOutputAudioMixPortConfigs({port}, false); + } + std::optional getSingleConfigForMixPort( + bool isInput) const { + const auto config = isInput ? generateInputAudioMixPortConfigs(getInputMixPorts(), true) + : generateOutputAudioMixPortConfigs(getOutputMixPorts(), true); + // TODO: Avoid returning configs for offload since they require an extra + // argument to openOutputStream. + if (!config.empty()) { + return *config.begin(); + } else { + return {}; + } + } + std::optional getSingleConfigForMixPort( + bool isInput, const android::media::audio::common::AudioPort& port) const { + const auto config = isInput ? generateInputAudioMixPortConfigs({port}, true) + : generateOutputAudioMixPortConfigs({port}, true); + if (!config.empty()) { + return *config.begin(); + } else { + return {}; + } + } + + android::media::audio::common::AudioPortConfig getSingleConfigForDevicePort( + const android::media::audio::common::AudioPort& port) const { + for (const auto& config : mInitialConfigs) { + if (config.portId == port.id) return config; + } + const auto config = generateAudioDevicePortConfigs({port}, true); + return *config.begin(); + } + + private: + std::vector generateInputAudioMixPortConfigs( + const std::vector& ports, + bool singleProfile) const; + std::vector generateOutputAudioMixPortConfigs( + const std::vector& ports, + bool singleProfile) const; + + // Unlike MixPorts, the generator for DevicePorts always returns a non-empty + // vector for a non-empty input port list. If there are no profiles in the + // port, a vector with an empty config is returned. + std::vector generateAudioDevicePortConfigs( + const std::vector& ports, + bool singleProfile) const; + + android::binder::Status mStatus = android::binder::Status::ok(); + std::vector mPorts; + std::vector mInitialConfigs; + std::set mAttachedSinkDevicePorts; + std::set mAttachedSourceDevicePorts; + std::vector mRoutes; +}; diff --git a/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp b/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp new file mode 100644 index 0000000000..cadeb0c50e --- /dev/null +++ b/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp @@ -0,0 +1,1027 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ModuleConfig.h" + +using namespace android; +using android::binder::Status; +using android::hardware::audio::common::PlaybackTrackMetadata; +using android::hardware::audio::common::RecordTrackMetadata; +using android::hardware::audio::common::SinkMetadata; +using android::hardware::audio::common::SourceMetadata; +using android::hardware::audio::core::AudioPatch; +using android::hardware::audio::core::AudioRoute; +using android::hardware::audio::core::IModule; +using android::hardware::audio::core::IStreamIn; +using android::hardware::audio::core::IStreamOut; +using android::media::audio::common::AudioContentType; +using android::media::audio::common::AudioDevice; +using android::media::audio::common::AudioDeviceType; +using android::media::audio::common::AudioIoFlags; +using android::media::audio::common::AudioOutputFlags; +using android::media::audio::common::AudioPort; +using android::media::audio::common::AudioPortConfig; +using android::media::audio::common::AudioPortDeviceExt; +using android::media::audio::common::AudioPortExt; +using android::media::audio::common::AudioSource; +using android::media::audio::common::AudioUsage; + +template +auto findById(std::vector& v, int32_t id) { + return std::find_if(v.begin(), v.end(), [&](const auto& e) { return e.id == id; }); +} + +template +std::vector getNonExistentIds(const C& allIds) { + if (allIds.empty()) { + return std::vector{-1, 0, 1}; + } + std::vector nonExistentIds; + nonExistentIds.push_back(*std::min_element(allIds.begin(), allIds.end()) - 1); + nonExistentIds.push_back(*std::max_element(allIds.begin(), allIds.end()) + 1); + return nonExistentIds; +} + +struct AidlDeathRecipient : IBinder::DeathRecipient { + std::mutex mutex; + std::condition_variable condition; + bool fired = false; + wp who; + + void binderDied(const wp& who) override { + std::unique_lock lock(mutex); + fired = true; + this->who = who; + condition.notify_one(); + }; + + bool waitForFired(int timeoutMs) { + std::unique_lock lock(mutex); + condition.wait_for(lock, std::chrono::milliseconds(timeoutMs), [this]() { return fired; }); + return fired; + } +}; + +template +struct IsInput { + constexpr operator bool() const; +}; + +template <> +constexpr IsInput::operator bool() const { + return true; +} +template <> +constexpr IsInput::operator bool() const { + return false; +} + +class AudioCoreModule : public testing::TestWithParam { + public: + void SetUp() override { ASSERT_NO_FATAL_FAILURE(ConnectToService()); } + + void ConnectToService() { + module = android::waitForDeclaredService(String16(GetParam().c_str())); + ASSERT_NE(module, nullptr); + } + + void RestartService() { + ASSERT_NE(module, nullptr); + moduleConfig.reset(); + deathHandler = sp::make(); + ASSERT_EQ(NO_ERROR, IModule::asBinder(module)->linkToDeath(deathHandler)); + ASSERT_TRUE(base::SetProperty("sys.audio.restart.hal", "1")); + EXPECT_TRUE(deathHandler->waitForFired(3000)); + deathHandler = nullptr; + ASSERT_NO_FATAL_FAILURE(ConnectToService()); + } + + template + void GetAllEntityIds(std::set* entityIds, + Status (IModule::*getter)(std::vector*), + const std::string& errorMessage) { + std::vector entities; + { + Status status = (module.get()->*getter)(&entities); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + std::transform(entities.begin(), entities.end(), + std::inserter(*entityIds, entityIds->begin()), + [](const auto& entity) { return entity.id; }); + EXPECT_EQ(entities.size(), entityIds->size()) << errorMessage; + } + + void GetAllPatchIds(std::set* patchIds) { + return GetAllEntityIds( + patchIds, &IModule::getAudioPatches, + "IDs of audio patches returned by IModule.getAudioPatches are not unique"); + } + + void GetAllPortIds(std::set* portIds) { + return GetAllEntityIds( + portIds, &IModule::getAudioPorts, + "IDs of audio ports returned by IModule.getAudioPorts are not unique"); + } + + void GetAllPortConfigIds(std::set* portConfigIds) { + return GetAllEntityIds( + portConfigIds, &IModule::getAudioPortConfigs, + "IDs of audio port configs returned by IModule.getAudioPortConfigs are not unique"); + } + + void SetUpModuleConfig() { + if (moduleConfig == nullptr) { + moduleConfig = std::make_unique(module.get()); + ASSERT_EQ(Status::EX_NONE, moduleConfig->getStatus().exceptionCode()) + << "ModuleConfig init error: " << moduleConfig->getError(); + } + } + + sp module; + sp deathHandler; + std::unique_ptr moduleConfig; +}; + +// For consistency, WithAudioPortConfig can start both with a non-existent +// port config, and with an existing one. Existence is determined by the +// id of the provided config. If it's not 0, then WithAudioPortConfig is +// essentially a no-op wrapper. +class WithAudioPortConfig { + public: + WithAudioPortConfig() {} + explicit WithAudioPortConfig(const AudioPortConfig& config) : mInitialConfig(config) {} + ~WithAudioPortConfig() { + if (mModule != nullptr) { + Status status = mModule->resetAudioPortConfig(getId()); + EXPECT_EQ(Status::EX_NONE, status.exceptionCode()) + << status << "; port config id " << getId(); + } + } + void SetUp(IModule* module) { + ASSERT_NE(AudioPortExt::Tag::unspecified, mInitialConfig.ext.getTag()) + << "config: " << mInitialConfig.toString(); + // Negotiation is allowed for device ports because the HAL module is + // allowed to provide an empty profiles list for attached devices. + ASSERT_NO_FATAL_FAILURE( + SetUpImpl(module, mInitialConfig.ext.getTag() == AudioPortExt::Tag::device)); + } + int32_t getId() const { return mConfig.id; } + const AudioPortConfig& get() const { return mConfig; } + + private: + void SetUpImpl(IModule* module, bool negotiate) { + if (mInitialConfig.id == 0) { + AudioPortConfig suggested; + bool applied = false; + Status status = module->setAudioPortConfig(mInitialConfig, &suggested, &applied); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) + << status << "; Config: " << mInitialConfig.toString(); + if (!applied && negotiate) { + mInitialConfig = suggested; + ASSERT_NO_FATAL_FAILURE(SetUpImpl(module, false)) + << " while applying suggested config: " << suggested.toString(); + } else { + ASSERT_TRUE(applied) << "Suggested: " << suggested.toString(); + mConfig = suggested; + mModule = module; + } + } else { + mConfig = mInitialConfig; + } + } + + AudioPortConfig mInitialConfig; + IModule* mModule = nullptr; + AudioPortConfig mConfig; +}; + +template +class WithStream { + public: + WithStream() {} + explicit WithStream(const AudioPortConfig& portConfig) : mPortConfig(portConfig) {} + ~WithStream() { + if (mStream != nullptr) { + Status status = mStream->close(); + EXPECT_EQ(Status::EX_NONE, status.exceptionCode()) + << status << "; port config id " << getPortId(); + } + } + void SetUpPortConfig(IModule* module) { ASSERT_NO_FATAL_FAILURE(mPortConfig.SetUp(module)); } + Status SetUpNoChecks(IModule* module) { return SetUpNoChecks(module, mPortConfig.get()); } + Status SetUpNoChecks(IModule* module, const AudioPortConfig& portConfig); + void SetUp(IModule* module) { + ASSERT_NO_FATAL_FAILURE(SetUpPortConfig(module)); + Status status = SetUpNoChecks(module); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) + << status << "; port config id " << getPortId(); + ASSERT_NE(nullptr, mStream) << "; port config id " << getPortId(); + } + Stream* get() const { return mStream.get(); } + const AudioPortConfig& getPortConfig() const { return mPortConfig.get(); } + int32_t getPortId() const { return mPortConfig.getId(); } + + private: + WithAudioPortConfig mPortConfig; + sp mStream; +}; + +template <> +Status WithStream::SetUpNoChecks(IModule* module, const AudioPortConfig& portConfig) { + RecordTrackMetadata trackMeta; + trackMeta.source = AudioSource::MIC; + trackMeta.gain = 1.0; + trackMeta.channelMask = portConfig.channelMask.value(); + SinkMetadata metadata; + metadata.tracks.push_back(trackMeta); + return module->openInputStream(portConfig.id, metadata, &mStream); +} + +template <> +Status WithStream::SetUpNoChecks(IModule* module, const AudioPortConfig& portConfig) { + PlaybackTrackMetadata trackMeta; + trackMeta.usage = AudioUsage::MEDIA; + trackMeta.contentType = AudioContentType::MUSIC; + trackMeta.gain = 1.0; + trackMeta.channelMask = portConfig.channelMask.value(); + SourceMetadata metadata; + metadata.tracks.push_back(trackMeta); + return module->openOutputStream(portConfig.id, metadata, {}, &mStream); +} + +class WithAudioPatch { + public: + WithAudioPatch() {} + WithAudioPatch(const AudioPortConfig& srcPortConfig, const AudioPortConfig& sinkPortConfig) + : mSrcPortConfig(srcPortConfig), mSinkPortConfig(sinkPortConfig) {} + ~WithAudioPatch() { + if (mModule != nullptr && mPatch.id != 0) { + Status status = mModule->resetAudioPatch(mPatch.id); + EXPECT_EQ(Status::EX_NONE, status.exceptionCode()) + << status << "; patch id " << getId(); + } + } + void SetUpPortConfigs(IModule* module) { + ASSERT_NO_FATAL_FAILURE(mSrcPortConfig.SetUp(module)); + ASSERT_NO_FATAL_FAILURE(mSinkPortConfig.SetUp(module)); + } + Status SetUpNoChecks(IModule* module) { + mModule = module; + mPatch.sourcePortConfigIds = std::vector{mSrcPortConfig.getId()}; + mPatch.sinkPortConfigIds = std::vector{mSinkPortConfig.getId()}; + return mModule->setAudioPatch(mPatch, &mPatch); + } + void SetUp(IModule* module) { + ASSERT_NO_FATAL_FAILURE(SetUpPortConfigs(module)); + Status status = SetUpNoChecks(module); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) + << status << "; source port config id " << mSrcPortConfig.getId() + << "; sink port config id " << mSinkPortConfig.getId(); + } + int32_t getId() const { return mPatch.id; } + const AudioPatch& get() const { return mPatch; } + + private: + WithAudioPortConfig mSrcPortConfig; + WithAudioPortConfig mSinkPortConfig; + IModule* mModule = nullptr; + AudioPatch mPatch; +}; + +TEST_P(AudioCoreModule, Published) { + // SetUp must complete with no failures. +} + +TEST_P(AudioCoreModule, CanBeRestarted) { + ASSERT_NO_FATAL_FAILURE(RestartService()); +} + +TEST_P(AudioCoreModule, PortIdsAreUnique) { + std::set portIds; + ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); +} + +TEST_P(AudioCoreModule, GetAudioPortsIsStatic) { + std::vector ports1; + { + Status status = module->getAudioPorts(&ports1); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + std::vector ports2; + { + Status status = module->getAudioPorts(&ports2); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + ASSERT_EQ(ports1.size(), ports2.size()) + << "Sizes of audio port arrays do not match across calls to getAudioPorts"; + std::sort(ports1.begin(), ports1.end()); + std::sort(ports2.begin(), ports2.end()); + EXPECT_EQ(ports1, ports2); +} + +TEST_P(AudioCoreModule, GetAudioRoutesIsStatic) { + std::vector routes1; + { + Status status = module->getAudioRoutes(&routes1); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + std::vector routes2; + { + Status status = module->getAudioRoutes(&routes2); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + ASSERT_EQ(routes1.size(), routes2.size()) + << "Sizes of audio route arrays do not match across calls to getAudioRoutes"; + std::sort(routes1.begin(), routes1.end()); + std::sort(routes2.begin(), routes2.end()); + EXPECT_EQ(routes1, routes2); +} + +TEST_P(AudioCoreModule, GetAudioRoutesAreValid) { + std::vector routes; + { + Status status = module->getAudioRoutes(&routes); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + for (const auto& route : routes) { + std::set sources(route.sourcePortIds.begin(), route.sourcePortIds.end()); + EXPECT_NE(0, sources.size()) + << "empty audio port sinks in the audio route: " << route.toString(); + EXPECT_EQ(sources.size(), route.sourcePortIds.size()) + << "IDs of audio port sinks are not unique in the audio route: " + << route.toString(); + } +} + +TEST_P(AudioCoreModule, GetAudioRoutesPortIdsAreValid) { + std::set portIds; + ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); + std::vector routes; + { + Status status = module->getAudioRoutes(&routes); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + for (const auto& route : routes) { + EXPECT_EQ(1, portIds.count(route.sinkPortId)) + << route.sinkPortId << " sink port id is unknown"; + for (const auto& source : route.sourcePortIds) { + EXPECT_EQ(1, portIds.count(source)) << source << " source port id is unknown"; + } + } +} + +TEST_P(AudioCoreModule, CheckDevicePorts) { + std::vector ports; + { + Status status = module->getAudioPorts(&ports); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + std::optional defaultOutput, defaultInput; + std::set inputs, outputs; + const int defaultDeviceFlag = 1 << AudioPortDeviceExt::FLAG_INDEX_DEFAULT_DEVICE; + for (const auto& port : ports) { + if (port.ext.getTag() != AudioPortExt::Tag::device) continue; + const auto& devicePort = port.ext.get(); + EXPECT_NE(AudioDeviceType::NONE, devicePort.device.type.type); + EXPECT_NE(AudioDeviceType::IN_DEFAULT, devicePort.device.type.type); + EXPECT_NE(AudioDeviceType::OUT_DEFAULT, devicePort.device.type.type); + if (devicePort.device.type.type > AudioDeviceType::IN_DEFAULT && + devicePort.device.type.type < AudioDeviceType::OUT_DEFAULT) { + EXPECT_EQ(AudioIoFlags::Tag::input, port.flags.getTag()); + } else if (devicePort.device.type.type > AudioDeviceType::OUT_DEFAULT) { + EXPECT_EQ(AudioIoFlags::Tag::output, port.flags.getTag()); + } + EXPECT_FALSE((devicePort.flags & defaultDeviceFlag) != 0 && + !devicePort.device.type.connection.empty()) + << "Device port " << port.id + << " must be permanently attached to be set as default"; + if ((devicePort.flags & defaultDeviceFlag) != 0) { + if (port.flags.getTag() == AudioIoFlags::Tag::output) { + EXPECT_FALSE(defaultOutput.has_value()) + << "At least two output device ports are declared as default: " + << defaultOutput.value() << " and " << port.id; + defaultOutput = port.id; + EXPECT_EQ(0, outputs.count(devicePort.device)) + << "Non-unique output device: " << devicePort.device.toString(); + outputs.insert(devicePort.device); + } else if (port.flags.getTag() == AudioIoFlags::Tag::input) { + EXPECT_FALSE(defaultInput.has_value()) + << "At least two input device ports are declared as default: " + << defaultInput.value() << " and " << port.id; + defaultInput = port.id; + EXPECT_EQ(0, inputs.count(devicePort.device)) + << "Non-unique input device: " << devicePort.device.toString(); + inputs.insert(devicePort.device); + } else { + FAIL() << "Invalid AudioIoFlags Tag: " << toString(port.flags.getTag()); + } + } + } +} + +TEST_P(AudioCoreModule, CheckMixPorts) { + std::vector ports; + { + Status status = module->getAudioPorts(&ports); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + std::optional primaryMixPort; + constexpr int primaryOutputFlag = 1 << static_cast(AudioOutputFlags::PRIMARY); + for (const auto& port : ports) { + if (port.ext.getTag() != AudioPortExt::Tag::mix) continue; + const auto& mixPort = port.ext.get(); + if (port.flags.getTag() == AudioIoFlags::Tag::output && + ((port.flags.get() & primaryOutputFlag) != 0)) { + EXPECT_FALSE(primaryMixPort.has_value()) + << "At least two mix ports have PRIMARY flag set: " << primaryMixPort.value() + << " and " << port.id; + primaryMixPort = port.id; + EXPECT_EQ(1, mixPort.maxOpenStreamCount) + << "Primary mix port " << port.id << " can not have maxOpenStreamCount " + << mixPort.maxOpenStreamCount; + } + } +} + +TEST_P(AudioCoreModule, GetAudioPort) { + std::set portIds; + ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); + if (portIds.empty()) { + GTEST_SKIP() << "No ports in the module."; + } + for (const auto portId : portIds) { + AudioPort port; + Status status = module->getAudioPort(portId, &port); + EXPECT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + EXPECT_EQ(portId, port.id); + } + for (const auto portId : getNonExistentIds(portIds)) { + AudioPort port; + Status status = module->getAudioPort(portId, &port); + EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode()) + << status << " returned for port ID " << portId; + } +} + +TEST_P(AudioCoreModule, OpenStreamInvalidPortConfigId) { + std::set portConfigIds; + ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); + for (const auto portConfigId : getNonExistentIds(portConfigIds)) { + { + sp stream; + Status status = module->openInputStream(portConfigId, {}, &stream); + EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode()) + << status << " openInputStream returned for port config ID " << portConfigId; + EXPECT_EQ(nullptr, stream); + } + { + sp stream; + Status status = module->openOutputStream(portConfigId, {}, {}, &stream); + EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode()) + << status << " openOutputStream returned for port config ID " << portConfigId; + EXPECT_EQ(nullptr, stream); + } + } +} + +TEST_P(AudioCoreModule, PortConfigIdsAreUnique) { + std::set portConfigIds; + ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); +} + +TEST_P(AudioCoreModule, PortConfigPortIdsAreValid) { + std::set portIds; + ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); + std::vector portConfigs; + { + Status status = module->getAudioPortConfigs(&portConfigs); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + for (const auto& config : portConfigs) { + EXPECT_EQ(1, portIds.count(config.portId)) + << config.portId << " port id is unknown, config id " << config.id; + } +} + +TEST_P(AudioCoreModule, ResetAudioPortConfigInvalidId) { + std::set portConfigIds; + ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); + for (const auto portConfigId : getNonExistentIds(portConfigIds)) { + Status status = module->resetAudioPortConfig(portConfigId); + EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode()) + << status << " returned for port config ID " << portConfigId; + } +} + +// Verify that for the audio port configs provided by the HAL after init, resetting +// the config does not delete it, but brings it back to the initial config. +TEST_P(AudioCoreModule, ResetAudioPortConfigToInitialValue) { + std::vector portConfigsBefore; + { + Status status = module->getAudioPortConfigs(&portConfigsBefore); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + // TODO: Change port configs according to port profiles. + for (const auto& c : portConfigsBefore) { + Status status = module->resetAudioPortConfig(c.id); + EXPECT_EQ(Status::EX_NONE, status.exceptionCode()) + << status << " returned for port config ID " << c.id; + } + std::vector portConfigsAfter; + { + Status status = module->getAudioPortConfigs(&portConfigsAfter); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + for (const auto& c : portConfigsBefore) { + auto afterIt = findById(portConfigsAfter, c.id); + EXPECT_NE(portConfigsAfter.end(), afterIt) + << " port config ID " << c.id << " was removed by reset"; + if (afterIt != portConfigsAfter.end()) { + EXPECT_EQ(c, *afterIt); + } + } +} + +TEST_P(AudioCoreModule, SetAudioPortConfigSuggestedConfig) { + ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); + auto srcMixPort = moduleConfig->getSourceMixPortForAttachedDevice(); + if (!srcMixPort.has_value()) { + GTEST_SKIP() << "No mix port for attached output devices"; + } + AudioPortConfig portConfig; + AudioPortConfig suggestedConfig; + portConfig.portId = srcMixPort.value().id; + { + bool applied = true; + Status status = module->setAudioPortConfig(portConfig, &suggestedConfig, &applied); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) + << status << "; Config: " << portConfig.toString(); + EXPECT_FALSE(applied); + } + EXPECT_EQ(0, suggestedConfig.id); + EXPECT_TRUE(suggestedConfig.sampleRate.has_value()); + EXPECT_TRUE(suggestedConfig.channelMask.has_value()); + EXPECT_TRUE(suggestedConfig.format.has_value()); + EXPECT_TRUE(suggestedConfig.flags.has_value()); + WithAudioPortConfig applied(suggestedConfig); + ASSERT_NO_FATAL_FAILURE(applied.SetUp(module.get())); + const AudioPortConfig& appliedConfig = applied.get(); + EXPECT_NE(0, appliedConfig.id); + EXPECT_TRUE(appliedConfig.sampleRate.has_value()); + EXPECT_EQ(suggestedConfig.sampleRate.value(), appliedConfig.sampleRate.value()); + EXPECT_TRUE(appliedConfig.channelMask.has_value()); + EXPECT_EQ(suggestedConfig.channelMask.value(), appliedConfig.channelMask.value()); + EXPECT_TRUE(appliedConfig.format.has_value()); + EXPECT_EQ(suggestedConfig.format.value(), appliedConfig.format.value()); + EXPECT_TRUE(appliedConfig.flags.has_value()); + EXPECT_EQ(suggestedConfig.flags.value(), appliedConfig.flags.value()); +} + +TEST_P(AudioCoreModule, SetAllStaticAudioPortConfigs) { + ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); + const auto allPortConfigs = moduleConfig->getPortConfigsForMixPorts(); + for (const auto& config : allPortConfigs) { + ASSERT_NE(0, config.portId); + WithAudioPortConfig portConfig(config); + ASSERT_NO_FATAL_FAILURE(portConfig.SetUp(module.get())); + EXPECT_EQ(config.portId, portConfig.get().portId); + std::vector retrievedPortConfigs; + { + Status status = module->getAudioPortConfigs(&retrievedPortConfigs); + ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status; + } + const int32_t portConfigId = portConfig.getId(); + auto configIt = std::find_if( + retrievedPortConfigs.begin(), retrievedPortConfigs.end(), + [&portConfigId](const auto& retrConf) { return retrConf.id == portConfigId; }); + EXPECT_NE(configIt, retrievedPortConfigs.end()) + << "Port config id returned by setAudioPortConfig: " << portConfigId + << " is not found in the list returned by getPortConfigsForMixPorts"; + if (configIt != retrievedPortConfigs.end()) { + EXPECT_EQ(portConfig.get(), *configIt) + << "Port config returned by getPortConfigsForMixPorts: " << configIt->toString() + << " is not the same as returned by setAudioPortConfig: " + << portConfig.get().toString(); + } + } +} + +TEST_P(AudioCoreModule, SetAudioPortConfigInvalidPortId) { + std::set portIds; + ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); + for (const auto portId : getNonExistentIds(portIds)) { + AudioPortConfig portConfig, suggestedConfig; + bool applied; + portConfig.portId = portId; + Status status = module->setAudioPortConfig(portConfig, &suggestedConfig, &applied); + EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode()) + << status << " returned for port ID " << portId; + EXPECT_FALSE(suggestedConfig.format.has_value()); + EXPECT_FALSE(suggestedConfig.channelMask.has_value()); + EXPECT_FALSE(suggestedConfig.sampleRate.has_value()); + } +} + +TEST_P(AudioCoreModule, SetAudioPortConfigInvalidPortConfigId) { + std::set portConfigIds; + ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); + for (const auto portConfigId : getNonExistentIds(portConfigIds)) { + AudioPortConfig portConfig, suggestedConfig; + bool applied; + portConfig.id = portConfigId; + Status status = module->setAudioPortConfig(portConfig, &suggestedConfig, &applied); + EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode()) + << status << " returned for port config ID " << portConfigId; + EXPECT_FALSE(suggestedConfig.format.has_value()); + EXPECT_FALSE(suggestedConfig.channelMask.has_value()); + EXPECT_FALSE(suggestedConfig.sampleRate.has_value()); + } +} + +template +class AudioStream : public AudioCoreModule { + public: + static std::string direction(bool capitalize); + + void SetUp() override { + ASSERT_NO_FATAL_FAILURE(AudioCoreModule::SetUp()); + ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); + } + + void CloseTwice() { + const auto portConfig = moduleConfig->getSingleConfigForMixPort(IsInput()); + if (!portConfig.has_value()) { + GTEST_SKIP() << "No mix port for attached devices"; + } + sp heldStream; + { + WithStream stream(portConfig.value()); + ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get())); + heldStream = stream.get(); + } + Status status = heldStream->close(); + EXPECT_EQ(Status::EX_ILLEGAL_STATE, status.exceptionCode()) + << status << " when closing the stream twice"; + } + + void OpenAllConfigs() { + const auto allPortConfigs = moduleConfig->getPortConfigsForMixPorts(IsInput()); + for (const auto& portConfig : allPortConfigs) { + WithStream stream(portConfig); + ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get())); + } + } + + void OpenOverMaxCount() { + constexpr bool isInput = IsInput(); + auto ports = moduleConfig->getMixPorts(isInput); + bool hasSingleRun = false; + for (const auto& port : ports) { + const size_t maxStreamCount = port.ext.get().maxOpenStreamCount; + if (maxStreamCount == 0 || + moduleConfig->getAttachedDevicesPortsForMixPort(isInput, port).empty()) { + // No restrictions or no permanently attached devices. + continue; + } + auto portConfigs = moduleConfig->getPortConfigsForMixPorts(isInput, port); + if (portConfigs.size() < maxStreamCount + 1) { + // Not able to open a sufficient number of streams for this port. + continue; + } + hasSingleRun = true; + std::optional> streamWraps[maxStreamCount + 1]; + for (size_t i = 0; i <= maxStreamCount; ++i) { + streamWraps[i].emplace(portConfigs[i]); + WithStream& stream = streamWraps[i].value(); + if (i < maxStreamCount) { + ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get())); + } else { + ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get())); + Status status = stream.SetUpNoChecks(module.get()); + EXPECT_EQ(Status::EX_ILLEGAL_STATE, status.exceptionCode()) + << status << " open" << direction(true) + << "Stream" + " returned for port config ID " + << stream.getPortId() << ", maxOpenStreamCount is " << maxStreamCount; + } + } + } + if (!hasSingleRun) { + GTEST_SKIP() << "Not enough " << direction(false) + << " ports to test max open stream count"; + } + } + + void OpenInvalidDirection() { + // Important! The direction of the port config must be reversed. + const auto portConfig = moduleConfig->getSingleConfigForMixPort(!IsInput()); + if (!portConfig.has_value()) { + GTEST_SKIP() << "No mix port for attached devices"; + } + WithStream stream(portConfig.value()); + ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get())); + Status status = stream.SetUpNoChecks(module.get()); + EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode()) + << status << " open" << direction(true) << "Stream returned for port config ID " + << stream.getPortId(); + EXPECT_EQ(nullptr, stream.get()); + } + + void OpenTwiceSamePortConfig() { + const auto portConfig = moduleConfig->getSingleConfigForMixPort(IsInput()); + if (!portConfig.has_value()) { + GTEST_SKIP() << "No mix port for attached devices"; + } + EXPECT_NO_FATAL_FAILURE(OpenTwiceSamePortConfigImpl(portConfig.value())); + } + + void ResetPortConfigWithOpenStream() { + const auto portConfig = moduleConfig->getSingleConfigForMixPort(IsInput()); + if (!portConfig.has_value()) { + GTEST_SKIP() << "No mix port for attached devices"; + } + WithStream stream(portConfig.value()); + ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get())); + Status status = module->resetAudioPortConfig(stream.getPortId()); + EXPECT_EQ(Status::EX_ILLEGAL_STATE, status.exceptionCode()) + << status << " returned for port config ID " << stream.getPortId(); + } + + void OpenTwiceSamePortConfigImpl(const AudioPortConfig& portConfig) { + WithStream stream1(portConfig); + ASSERT_NO_FATAL_FAILURE(stream1.SetUp(module.get())); + WithStream stream2; + Status status = stream2.SetUpNoChecks(module.get(), stream1.getPortConfig()); + EXPECT_EQ(Status::EX_ILLEGAL_STATE, status.exceptionCode()) + << status << " when opening " << direction(false) + << " stream twice for the same port config ID " << stream1.getPortId(); + } +}; +using AudioStreamIn = AudioStream; +using AudioStreamOut = AudioStream; + +template <> +std::string AudioStreamIn::direction(bool capitalize) { + return capitalize ? "Input" : "input"; +} +template <> +std::string AudioStreamOut::direction(bool capitalize) { + return capitalize ? "Output" : "output"; +} + +#define TEST_IO_STREAM(method_name) \ + TEST_P(AudioStreamIn, method_name) { ASSERT_NO_FATAL_FAILURE(method_name()); } \ + TEST_P(AudioStreamOut, method_name) { ASSERT_NO_FATAL_FAILURE(method_name()); } + +TEST_IO_STREAM(CloseTwice); +TEST_IO_STREAM(OpenAllConfigs); +TEST_IO_STREAM(OpenInvalidDirection); +TEST_IO_STREAM(OpenOverMaxCount); +TEST_IO_STREAM(OpenTwiceSamePortConfig); +TEST_IO_STREAM(ResetPortConfigWithOpenStream); + +TEST_P(AudioStreamOut, OpenTwicePrimary) { + const auto mixPorts = moduleConfig->getMixPorts(false); + auto primaryPortIt = std::find_if(mixPorts.begin(), mixPorts.end(), [](const AudioPort& port) { + constexpr int primaryOutputFlag = 1 << static_cast(AudioOutputFlags::PRIMARY); + return port.flags.getTag() == AudioIoFlags::Tag::output && + ((port.flags.get() & primaryOutputFlag) != 0); + }); + if (primaryPortIt == mixPorts.end()) { + GTEST_SKIP() << "No primary mix port"; + } + if (moduleConfig->getAttachedSinkDevicesPortsForMixPort(*primaryPortIt).empty()) { + GTEST_SKIP() << "Primary mix port can not be routed to any of attached devices"; + } + const auto portConfig = moduleConfig->getSingleConfigForMixPort(false, *primaryPortIt); + ASSERT_TRUE(portConfig.has_value()) << "No profiles specified for the primary mix port"; + EXPECT_NO_FATAL_FAILURE(OpenTwiceSamePortConfigImpl(portConfig.value())); +} + +// Tests specific to audio patches. The fixure class is named 'AudioModulePatch' +// to avoid clashing with 'AudioPatch' class. +class AudioModulePatch : public AudioCoreModule { + public: + static std::string direction(bool isInput, bool capitalize) { + return isInput ? (capitalize ? "Input" : "input") : (capitalize ? "Output" : "output"); + } + + void SetUp() override { + ASSERT_NO_FATAL_FAILURE(AudioCoreModule::SetUp()); + ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); + } + + void SetInvalidPatchHelper(int32_t expectedException, const std::vector& sources, + const std::vector& sinks) { + AudioPatch patch; + patch.sourcePortConfigIds = sources; + patch.sinkPortConfigIds = sinks; + Status status = module->setAudioPatch(patch, &patch); + ASSERT_EQ(expectedException, status.exceptionCode()) + << status << ": patch source ids: " << android::internal::ToString(sources) + << "; sink ids: " << android::internal::ToString(sinks); + } + + void ResetPortConfigUsedByPatch(bool isInput) { + auto srcSinkGroups = moduleConfig->getRoutableSrcSinkGroups(isInput); + if (srcSinkGroups.empty()) { + GTEST_SKIP() << "No routes to any attached " << direction(isInput, false) << " devices"; + } + auto srcSinkGroup = *srcSinkGroups.begin(); + auto srcSink = *srcSinkGroup.second.begin(); + WithAudioPatch patch(srcSink.first, srcSink.second); + ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get())); + std::vector sourceAndSinkPortConfigIds(patch.get().sourcePortConfigIds); + sourceAndSinkPortConfigIds.insert(sourceAndSinkPortConfigIds.end(), + patch.get().sinkPortConfigIds.begin(), + patch.get().sinkPortConfigIds.end()); + for (const auto portConfigId : sourceAndSinkPortConfigIds) { + Status status = module->resetAudioPortConfig(portConfigId); + EXPECT_EQ(Status::EX_ILLEGAL_STATE, status.exceptionCode()) + << status << " returned for port config ID " << portConfigId; + } + } + + void SetInvalidPatch(bool isInput) { + auto srcSinkPair = moduleConfig->getRoutableSrcSinkPair(isInput); + if (!srcSinkPair.has_value()) { + GTEST_SKIP() << "No routes to any attached " << direction(isInput, false) << " devices"; + } + WithAudioPortConfig srcPortConfig(srcSinkPair.value().first); + ASSERT_NO_FATAL_FAILURE(srcPortConfig.SetUp(module.get())); + WithAudioPortConfig sinkPortConfig(srcSinkPair.value().second); + ASSERT_NO_FATAL_FAILURE(sinkPortConfig.SetUp(module.get())); + { // Check that the pair can actually be used for setting up a patch. + WithAudioPatch patch(srcPortConfig.get(), sinkPortConfig.get()); + ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get())); + } + EXPECT_NO_FATAL_FAILURE( + SetInvalidPatchHelper(Status::EX_ILLEGAL_ARGUMENT, {}, {sinkPortConfig.getId()})); + EXPECT_NO_FATAL_FAILURE(SetInvalidPatchHelper( + Status::EX_ILLEGAL_ARGUMENT, {srcPortConfig.getId(), srcPortConfig.getId()}, + {sinkPortConfig.getId()})); + EXPECT_NO_FATAL_FAILURE( + SetInvalidPatchHelper(Status::EX_ILLEGAL_ARGUMENT, {srcPortConfig.getId()}, {})); + EXPECT_NO_FATAL_FAILURE( + SetInvalidPatchHelper(Status::EX_ILLEGAL_ARGUMENT, {srcPortConfig.getId()}, + {sinkPortConfig.getId(), sinkPortConfig.getId()})); + + std::set portConfigIds; + ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); + for (const auto portConfigId : getNonExistentIds(portConfigIds)) { + EXPECT_NO_FATAL_FAILURE(SetInvalidPatchHelper( + Status::EX_ILLEGAL_ARGUMENT, {portConfigId}, {sinkPortConfig.getId()})); + EXPECT_NO_FATAL_FAILURE(SetInvalidPatchHelper(Status::EX_ILLEGAL_ARGUMENT, + {srcPortConfig.getId()}, {portConfigId})); + } + } + + void SetNonRoutablePatch(bool isInput) { + auto srcSinkPair = moduleConfig->getNonRoutableSrcSinkPair(isInput); + if (!srcSinkPair.has_value()) { + GTEST_SKIP() << "All possible source/sink pairs are routable"; + } + WithAudioPatch patch(srcSinkPair.value().first, srcSinkPair.value().second); + ASSERT_NO_FATAL_FAILURE(patch.SetUpPortConfigs(module.get())); + Status status = patch.SetUpNoChecks(module.get()); + EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode()) + << status << ": when setting up a patch from " + << srcSinkPair.value().first.toString() << " to " + << srcSinkPair.value().second.toString() << " that does not have a route"; + } + + void SetPatch(bool isInput) { + auto srcSinkGroups = moduleConfig->getRoutableSrcSinkGroups(isInput); + if (srcSinkGroups.empty()) { + GTEST_SKIP() << "No routes to any attached " << direction(isInput, false) << " devices"; + } + for (const auto& srcSinkGroup : srcSinkGroups) { + const auto& route = srcSinkGroup.first; + std::vector patches; + for (const auto& srcSink : srcSinkGroup.second) { + if (!route.isExclusive) { + patches.emplace_back(srcSink.first, srcSink.second); + EXPECT_NO_FATAL_FAILURE(patches[patches.size() - 1].SetUp(module.get())); + } else { + WithAudioPatch patch(srcSink.first, srcSink.second); + EXPECT_NO_FATAL_FAILURE(patch.SetUp(module.get())); + } + } + } + } + + void UpdatePatch(bool isInput) { + auto srcSinkGroups = moduleConfig->getRoutableSrcSinkGroups(isInput); + if (srcSinkGroups.empty()) { + GTEST_SKIP() << "No routes to any attached " << direction(isInput, false) << " devices"; + } + for (const auto& srcSinkGroup : srcSinkGroups) { + for (const auto& srcSink : srcSinkGroup.second) { + WithAudioPatch patch(srcSink.first, srcSink.second); + ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get())); + AudioPatch ignored; + EXPECT_NO_FATAL_FAILURE(module->setAudioPatch(patch.get(), &ignored)); + } + } + } + + void UpdateInvalidPatchId(bool isInput) { + auto srcSinkGroups = moduleConfig->getRoutableSrcSinkGroups(isInput); + if (srcSinkGroups.empty()) { + GTEST_SKIP() << "No routes to any attached " << direction(isInput, false) << " devices"; + } + // First, set up a patch to ensure that its settings are accepted. + auto srcSinkGroup = *srcSinkGroups.begin(); + auto srcSink = *srcSinkGroup.second.begin(); + WithAudioPatch patch(srcSink.first, srcSink.second); + ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get())); + // Then use the same patch setting, except for having an invalid ID. + std::set patchIds; + ASSERT_NO_FATAL_FAILURE(GetAllPatchIds(&patchIds)); + for (const auto patchId : getNonExistentIds(patchIds)) { + AudioPatch patchWithNonExistendId = patch.get(); + patchWithNonExistendId.id = patchId; + Status status = module->setAudioPatch(patchWithNonExistendId, &patchWithNonExistendId); + EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode()) + << status << " returned for patch ID " << patchId; + } + } +}; + +// Not all tests require both directions, so parametrization would require +// more abstractions. +#define TEST_PATCH_BOTH_DIRECTIONS(method_name) \ + TEST_P(AudioModulePatch, method_name##Input) { ASSERT_NO_FATAL_FAILURE(method_name(true)); } \ + TEST_P(AudioModulePatch, method_name##Output) { ASSERT_NO_FATAL_FAILURE(method_name(false)); } + +TEST_PATCH_BOTH_DIRECTIONS(ResetPortConfigUsedByPatch); +TEST_PATCH_BOTH_DIRECTIONS(SetInvalidPatch); +TEST_PATCH_BOTH_DIRECTIONS(SetNonRoutablePatch); +TEST_PATCH_BOTH_DIRECTIONS(SetPatch); +TEST_PATCH_BOTH_DIRECTIONS(UpdateInvalidPatchId); +TEST_PATCH_BOTH_DIRECTIONS(UpdatePatch); + +TEST_P(AudioModulePatch, ResetInvalidPatchId) { + std::set patchIds; + ASSERT_NO_FATAL_FAILURE(GetAllPatchIds(&patchIds)); + for (const auto patchId : getNonExistentIds(patchIds)) { + Status status = module->resetAudioPatch(patchId); + EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode()) + << status << " returned for patch ID " << patchId; + } +} + +INSTANTIATE_TEST_SUITE_P(AudioCoreModuleTest, AudioCoreModule, + testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), + android::PrintInstanceNameToString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioCoreModule); +INSTANTIATE_TEST_SUITE_P(AudioStreamInTest, AudioStreamIn, + testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), + android::PrintInstanceNameToString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioStreamIn); +INSTANTIATE_TEST_SUITE_P(AudioStreamOutTest, AudioStreamOut, + testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), + android::PrintInstanceNameToString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioStreamOut); +INSTANTIATE_TEST_SUITE_P(AudioPatchTest, AudioModulePatch, + testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), + android::PrintInstanceNameToString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioModulePatch); + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ProcessState::self()->setThreadPoolMaxThreadCount(1); + ProcessState::self()->startThreadPool(); + return RUN_ALL_TESTS(); +}