From 5a78d9c22148739e830301c35a4b99eb5441ad89 Mon Sep 17 00:00:00 2001 From: Mikhail Naganov Date: Wed, 3 Mar 2021 14:13:49 -0800 Subject: [PATCH] audio: Add VTS tests for reads and writes Tests verify actual reading / writing from input and output streams and the capture / presentation position reporting. Tests use audio policy manager configuration. Bug: 161253754 Test: atest VtsHalAudioV7_0TargetTest Change-Id: I408f7ee8df8671b7496040fe5ddd8a380672c21d --- ...id_audio_policy_configuration_V7_0-enums.h | 27 ++ .../4.0/AudioPrimaryHidlHalTest.cpp | 44 +-- .../7.0/AudioPrimaryHidlHalTest.cpp | 304 ++++++++++++++++++ .../vts/functional/7.0/Generators.cpp | 24 +- .../vts/functional/7.0/PolicyConfig.cpp | 215 +++++++++++++ .../vts/functional/7.0/PolicyConfig.h | 93 ++---- .../all-versions/vts/functional/Android.bp | 14 +- .../vts/functional/AudioPrimaryHidlHalTest.h | 268 ++++++++++++++- .../vts/functional/AudioTestDefinitions.h | 6 +- .../vts/functional/StreamWorker.h | 146 +++++++++ .../functional/tests/streamworker_tests.cpp | 216 +++++++++++++ 11 files changed, 1232 insertions(+), 125 deletions(-) create mode 100644 audio/core/all-versions/vts/functional/7.0/PolicyConfig.cpp create mode 100644 audio/core/all-versions/vts/functional/StreamWorker.h create mode 100644 audio/core/all-versions/vts/functional/tests/streamworker_tests.cpp diff --git a/audio/common/7.0/enums/include/android_audio_policy_configuration_V7_0-enums.h b/audio/common/7.0/enums/include/android_audio_policy_configuration_V7_0-enums.h index fe8eee1d53..88dd12e94b 100644 --- a/audio/common/7.0/enums/include/android_audio_policy_configuration_V7_0-enums.h +++ b/audio/common/7.0/enums/include/android_audio_policy_configuration_V7_0-enums.h @@ -212,6 +212,15 @@ static inline bool isOutputDevice(const std::string& device) { return isOutputDevice(stringToAudioDevice(device)); } +static inline bool isTelephonyDevice(AudioDevice device) { + return device == AudioDevice::AUDIO_DEVICE_OUT_TELEPHONY_TX || + device == AudioDevice::AUDIO_DEVICE_IN_TELEPHONY_RX; +} + +static inline bool isTelephonyDevice(const std::string& device) { + return isTelephonyDevice(stringToAudioDevice(device)); +} + static inline bool maybeVendorExtension(const std::string& s) { // Only checks whether the string starts with the "vendor prefix". static const std::string vendorPrefix = "VX_"; @@ -260,6 +269,24 @@ static inline bool isUnknownAudioUsage(const std::string& usage) { return stringToAudioUsage(usage) == AudioUsage::UNKNOWN; } +static inline bool isLinearPcm(AudioFormat format) { + switch (format) { + case AudioFormat::AUDIO_FORMAT_PCM_16_BIT: + case AudioFormat::AUDIO_FORMAT_PCM_8_BIT: + case AudioFormat::AUDIO_FORMAT_PCM_32_BIT: + case AudioFormat::AUDIO_FORMAT_PCM_8_24_BIT: + case AudioFormat::AUDIO_FORMAT_PCM_FLOAT: + case AudioFormat::AUDIO_FORMAT_PCM_24_BIT_PACKED: + return true; + default: + return false; + } +} + +static inline bool isLinearPcm(const std::string& format) { + return isLinearPcm(stringToAudioFormat(format)); +} + } // namespace android::audio::policy::configuration::V7_0 #endif // ANDROID_AUDIO_POLICY_CONFIGURATION_V7_0__ENUMS_H diff --git a/audio/core/all-versions/vts/functional/4.0/AudioPrimaryHidlHalTest.cpp b/audio/core/all-versions/vts/functional/4.0/AudioPrimaryHidlHalTest.cpp index f87e5ed565..b96cc83673 100644 --- a/audio/core/all-versions/vts/functional/4.0/AudioPrimaryHidlHalTest.cpp +++ b/audio/core/all-versions/vts/functional/4.0/AudioPrimaryHidlHalTest.cpp @@ -77,7 +77,6 @@ TEST_P(AudioHidlDeviceTest, GetMicrophonesTest) { .tags = {}, .channelMask = toString(xsd::AudioChannelMask::AUDIO_CHANNEL_IN_MONO)}}}; #endif - EventFlag* efGroup; for (auto microphone : microphones) { #if MAJOR_VERSION <= 6 if (microphone.deviceAddress.device != AudioDevice::IN_BUILTIN_MIC) { @@ -96,44 +95,15 @@ TEST_P(AudioHidlDeviceTest, GetMicrophonesTest) { config, flags, initMetadata, cb); }, config, &res, &suggestedConfig)); + StreamReader reader(stream.get(), stream->getBufferSize()); + ASSERT_TRUE(reader.start()); + reader.pause(); // This ensures that at least one read has happened. + EXPECT_FALSE(reader.hasError()); + hidl_vec activeMicrophones; - Result readRes; - typedef MessageQueue CommandMQ; - typedef MessageQueue DataMQ; - std::unique_ptr commandMQ; - std::unique_ptr dataMQ; - size_t frameSize = stream->getFrameSize(); - size_t frameCount = stream->getBufferSize() / frameSize; - ASSERT_OK(stream->prepareForReading( - frameSize, frameCount, [&](auto r, auto& c, auto& d, auto&, auto) { - readRes = r; - if (readRes == Result::OK) { - commandMQ.reset(new CommandMQ(c)); - dataMQ.reset(new DataMQ(d)); - if (dataMQ->isValid() && dataMQ->getEventFlagWord()) { - EventFlag::createEventFlag(dataMQ->getEventFlagWord(), &efGroup); - } - } - })); - ASSERT_OK(readRes); - IStreamIn::ReadParameters params; - params.command = IStreamIn::ReadCommand::READ; - ASSERT_TRUE(commandMQ != nullptr); - ASSERT_TRUE(commandMQ->isValid()); - ASSERT_TRUE(commandMQ->write(¶ms)); - efGroup->wake(static_cast(MessageQueueFlagBits::NOT_FULL)); - uint32_t efState = 0; - efGroup->wait(static_cast(MessageQueueFlagBits::NOT_EMPTY), &efState); - if (efState & static_cast(MessageQueueFlagBits::NOT_EMPTY)) { - ASSERT_OK(stream->getActiveMicrophones(returnIn(res, activeMicrophones))); - ASSERT_OK(res); - ASSERT_NE(0U, activeMicrophones.size()); - } - helper.close(true /*clear*/, &res); + ASSERT_OK(stream->getActiveMicrophones(returnIn(res, activeMicrophones))); ASSERT_OK(res); - if (efGroup) { - EventFlag::deleteEventFlag(&efGroup); - } + EXPECT_NE(0U, activeMicrophones.size()); } } } diff --git a/audio/core/all-versions/vts/functional/7.0/AudioPrimaryHidlHalTest.cpp b/audio/core/all-versions/vts/functional/7.0/AudioPrimaryHidlHalTest.cpp index c1923f1230..657b42dfe0 100644 --- a/audio/core/all-versions/vts/functional/7.0/AudioPrimaryHidlHalTest.cpp +++ b/audio/core/all-versions/vts/functional/7.0/AudioPrimaryHidlHalTest.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include + #include "Generators.h" // pull in all the <= 6.0 tests @@ -487,3 +489,305 @@ TEST_P(SingleConfigInputStreamTest, UpdateInvalidSinkMetadata) { << ::testing::PrintToString(metadata); } } + +static const std::vector& getOutputDevicePcmOnlyConfigParameters() { + static const std::vector parameters = [] { + auto allParams = getOutputDeviceConfigParameters(); + std::vector pcmParams; + std::copy_if(allParams.begin(), allParams.end(), std::back_inserter(pcmParams), [](auto cfg) { + const auto& flags = std::get(cfg); + return xsd::isLinearPcm(std::get(cfg).base.format) + // MMAP NOIRQ and HW A/V Sync profiles use special writing protocols. + && + std::find_if(flags.begin(), flags.end(), + [](const auto& flag) { + return flag == toString(xsd::AudioInOutFlag:: + AUDIO_OUTPUT_FLAG_MMAP_NOIRQ) || + flag == toString(xsd::AudioInOutFlag:: + AUDIO_OUTPUT_FLAG_HW_AV_SYNC); + }) == flags.end() && + !getCachedPolicyConfig() + .getAttachedSinkDeviceForMixPort( + std::get(std::get(cfg)), + std::get(cfg)) + .empty(); + }); + return pcmParams; + }(); + return parameters; +} + +class PcmOnlyConfigOutputStreamTest : public OutputStreamTest { + public: + void TearDown() override { + releasePatchIfNeeded(); + OutputStreamTest::TearDown(); + } + + bool canQueryPresentationPosition() const { + auto maybeSinkAddress = + getCachedPolicyConfig().getSinkDeviceForMixPort(getDeviceName(), getMixPortName()); + // Returning 'true' when no sink is found so the test can fail later with a more clear + // problem description. + return !maybeSinkAddress.has_value() || + !xsd::isTelephonyDevice(maybeSinkAddress.value().deviceType); + } + + void createPatchIfNeeded() { + auto maybeSinkAddress = + getCachedPolicyConfig().getSinkDeviceForMixPort(getDeviceName(), getMixPortName()); + ASSERT_TRUE(maybeSinkAddress.has_value()) + << "No sink device found for mix port " << getMixPortName() << " (module " + << getDeviceName() << ")"; + if (areAudioPatchesSupported()) { + AudioPortConfig source; + source.base.format.value(getConfig().base.format); + source.base.sampleRateHz.value(getConfig().base.sampleRateHz); + source.base.channelMask.value(getConfig().base.channelMask); + source.ext.mix({}); + source.ext.mix().ioHandle = helper.getIoHandle(); + source.ext.mix().useCase.stream({}); + AudioPortConfig sink; + sink.ext.device(maybeSinkAddress.value()); + EXPECT_OK(getDevice()->createAudioPatch(hidl_vec{source}, + hidl_vec{sink}, + returnIn(res, mPatchHandle))); + mHasPatch = res == Result::OK; + } else { + EXPECT_OK(stream->setDevices({maybeSinkAddress.value()})); + } + } + + void releasePatchIfNeeded() { + if (areAudioPatchesSupported()) { + if (mHasPatch) { + EXPECT_OK(getDevice()->releaseAudioPatch(mPatchHandle)); + mHasPatch = false; + } + } else { + EXPECT_OK(stream->setDevices({address})); + } + } + + const std::string& getMixPortName() const { return std::get(GetParam()); } + + void waitForPresentationPositionAdvance(StreamWriter& writer, uint64_t* firstPosition = nullptr, + uint64_t* lastPosition = nullptr) { + static constexpr int kWriteDurationUs = 50 * 1000; + static constexpr std::chrono::milliseconds kPositionChangeTimeout{10000}; + uint64_t framesInitial; + TimeSpec ts; + // Starting / resuming of streams is asynchronous at HAL level. + // Sometimes HAL doesn't have enough information until the audio data actually gets + // consumed by the hardware. + do { + ASSERT_OK(stream->getPresentationPosition(returnIn(res, framesInitial, ts))); + ASSERT_RESULT(okOrInvalidState, res); + } while (res != Result::OK); + uint64_t frames = framesInitial; + bool timedOut = false; + for (android::base::Timer elapsed; + frames <= framesInitial && !writer.hasError() && + !(timedOut = (elapsed.duration() >= kPositionChangeTimeout));) { + usleep(kWriteDurationUs); + ASSERT_OK(stream->getPresentationPosition(returnIn(res, frames, ts))); + ASSERT_RESULT(Result::OK, res); + } + EXPECT_FALSE(timedOut); + EXPECT_FALSE(writer.hasError()); + EXPECT_GT(frames, framesInitial); + if (firstPosition) *firstPosition = framesInitial; + if (lastPosition) *lastPosition = frames; + } + + private: + AudioPatchHandle mPatchHandle = {}; + bool mHasPatch = false; +}; + +TEST_P(PcmOnlyConfigOutputStreamTest, Write) { + doc::test("Check that output streams opened for PCM output accepts audio data"); + StreamWriter writer(stream.get(), stream->getBufferSize()); + ASSERT_TRUE(writer.start()); + EXPECT_TRUE(writer.waitForAtLeastOneCycle()); +} + +TEST_P(PcmOnlyConfigOutputStreamTest, PresentationPositionAdvancesWithWrites) { + doc::test("Check that the presentation position advances with writes"); + if (!canQueryPresentationPosition()) { + GTEST_SKIP() << "Presentation position retrieval is not possible"; + } + + ASSERT_NO_FATAL_FAILURE(createPatchIfNeeded()); + StreamWriter writer(stream.get(), stream->getBufferSize()); + ASSERT_TRUE(writer.start()); + ASSERT_TRUE(writer.waitForAtLeastOneCycle()); + ASSERT_NO_FATAL_FAILURE(waitForPresentationPositionAdvance(writer)); + + writer.stop(); + releasePatchIfNeeded(); +} + +TEST_P(PcmOnlyConfigOutputStreamTest, PresentationPositionPreservedOnStandby) { + doc::test("Check that the presentation position does not reset on standby"); + if (!canQueryPresentationPosition()) { + GTEST_SKIP() << "Presentation position retrieval is not possible"; + } + + ASSERT_NO_FATAL_FAILURE(createPatchIfNeeded()); + StreamWriter writer(stream.get(), stream->getBufferSize()); + ASSERT_TRUE(writer.start()); + ASSERT_TRUE(writer.waitForAtLeastOneCycle()); + + uint64_t framesInitial; + ASSERT_NO_FATAL_FAILURE(waitForPresentationPositionAdvance(writer, nullptr, &framesInitial)); + writer.pause(); + ASSERT_OK(stream->standby()); + writer.resume(); + + uint64_t frames; + ASSERT_NO_FATAL_FAILURE(waitForPresentationPositionAdvance(writer, &frames)); + EXPECT_GT(frames, framesInitial); + + writer.stop(); + releasePatchIfNeeded(); +} + +INSTANTIATE_TEST_CASE_P(PcmOnlyConfigOutputStream, PcmOnlyConfigOutputStreamTest, + ::testing::ValuesIn(getOutputDevicePcmOnlyConfigParameters()), + &DeviceConfigParameterToString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(PcmOnlyConfigOutputStreamTest); + +static const std::vector& getInputDevicePcmOnlyConfigParameters() { + static const std::vector parameters = [] { + auto allParams = getInputDeviceConfigParameters(); + std::vector pcmParams; + std::copy_if( + allParams.begin(), allParams.end(), std::back_inserter(pcmParams), [](auto cfg) { + const auto& flags = std::get(cfg); + return xsd::isLinearPcm(std::get(cfg).base.format) + // MMAP NOIRQ profiles use different reading protocol. + && + std::find(flags.begin(), flags.end(), + toString(xsd::AudioInOutFlag::AUDIO_INPUT_FLAG_MMAP_NOIRQ)) == + flags.end() && + !getCachedPolicyConfig() + .getAttachedSourceDeviceForMixPort( + std::get( + std::get(cfg)), + std::get(cfg)) + .empty(); + }); + return pcmParams; + }(); + return parameters; +} + +class PcmOnlyConfigInputStreamTest : public InputStreamTest { + public: + void TearDown() override { + releasePatchIfNeeded(); + InputStreamTest::TearDown(); + } + + void createPatchIfNeeded() { + auto maybeSourceAddress = getCachedPolicyConfig().getSourceDeviceForMixPort( + getDeviceName(), getMixPortName()); + ASSERT_TRUE(maybeSourceAddress.has_value()) + << "No source device found for mix port " << getMixPortName() << " (module " + << getDeviceName() << ")"; + if (areAudioPatchesSupported()) { + AudioPortConfig source; + source.ext.device(maybeSourceAddress.value()); + AudioPortConfig sink; + sink.base.format.value(getConfig().base.format); + sink.base.sampleRateHz.value(getConfig().base.sampleRateHz); + sink.base.channelMask.value(getConfig().base.channelMask); + sink.ext.mix({}); + sink.ext.mix().ioHandle = helper.getIoHandle(); + sink.ext.mix().useCase.source(toString(xsd::AudioSource::AUDIO_SOURCE_MIC)); + EXPECT_OK(getDevice()->createAudioPatch(hidl_vec{source}, + hidl_vec{sink}, + returnIn(res, mPatchHandle))); + mHasPatch = res == Result::OK; + } else { + EXPECT_OK(stream->setDevices({maybeSourceAddress.value()})); + } + } + void releasePatchIfNeeded() { + if (areAudioPatchesSupported()) { + if (mHasPatch) { + EXPECT_OK(getDevice()->releaseAudioPatch(mPatchHandle)); + mHasPatch = false; + } + } else { + EXPECT_OK(stream->setDevices({address})); + } + } + const std::string& getMixPortName() const { return std::get(GetParam()); } + + private: + AudioPatchHandle mPatchHandle = {}; + bool mHasPatch = false; +}; + +TEST_P(PcmOnlyConfigInputStreamTest, Read) { + doc::test("Check that input streams opened for PCM input retrieve audio data"); + StreamReader reader(stream.get(), stream->getBufferSize()); + ASSERT_TRUE(reader.start()); + EXPECT_TRUE(reader.waitForAtLeastOneCycle()); +} + +TEST_P(PcmOnlyConfigInputStreamTest, CapturePositionAdvancesWithReads) { + doc::test("Check that the capture position advances with reads"); + + ASSERT_NO_FATAL_FAILURE(createPatchIfNeeded()); + StreamReader reader(stream.get(), stream->getBufferSize()); + ASSERT_TRUE(reader.start()); + EXPECT_TRUE(reader.waitForAtLeastOneCycle()); + + uint64_t framesInitial, ts; + ASSERT_OK(stream->getCapturePosition(returnIn(res, framesInitial, ts))); + ASSERT_RESULT(Result::OK, res); + + EXPECT_TRUE(reader.waitForAtLeastOneCycle()); + + uint64_t frames; + ASSERT_OK(stream->getCapturePosition(returnIn(res, frames, ts))); + ASSERT_RESULT(Result::OK, res); + EXPECT_GT(frames, framesInitial); + + reader.stop(); + releasePatchIfNeeded(); +} + +TEST_P(PcmOnlyConfigInputStreamTest, CapturePositionPreservedOnStandby) { + doc::test("Check that the capture position does not reset on standby"); + + ASSERT_NO_FATAL_FAILURE(createPatchIfNeeded()); + StreamReader reader(stream.get(), stream->getBufferSize()); + ASSERT_TRUE(reader.start()); + EXPECT_TRUE(reader.waitForAtLeastOneCycle()); + + uint64_t framesInitial, ts; + ASSERT_OK(stream->getCapturePosition(returnIn(res, framesInitial, ts))); + ASSERT_RESULT(Result::OK, res); + + reader.pause(); + ASSERT_OK(stream->standby()); + reader.resume(); + EXPECT_FALSE(reader.hasError()); + + uint64_t frames; + ASSERT_OK(stream->getCapturePosition(returnIn(res, frames, ts))); + ASSERT_RESULT(Result::OK, res); + EXPECT_GT(frames, framesInitial); + + reader.stop(); + releasePatchIfNeeded(); +} + +INSTANTIATE_TEST_CASE_P(PcmOnlyConfigInputStream, PcmOnlyConfigInputStreamTest, + ::testing::ValuesIn(getInputDevicePcmOnlyConfigParameters()), + &DeviceConfigParameterToString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(PcmOnlyConfigInputStreamTest); diff --git a/audio/core/all-versions/vts/functional/7.0/Generators.cpp b/audio/core/all-versions/vts/functional/7.0/Generators.cpp index eafc813cf9..d2ba3397af 100644 --- a/audio/core/all-versions/vts/functional/7.0/Generators.cpp +++ b/audio/core/all-versions/vts/functional/7.0/Generators.cpp @@ -110,7 +110,7 @@ std::vector generateOutputDeviceConfigParameters(bool one if (isOffload) { config.offloadInfo.info(generateOffloadInfo(config.base)); } - result.emplace_back(device, config, flags); + result.emplace_back(device, mixPort.getName(), config, flags); if (oneProfilePerDevice) break; } if (oneProfilePerDevice) break; @@ -160,7 +160,7 @@ const std::vector& getOutputDeviceInvalidConfigParameters if (isOffload) { config.offloadInfo.info(generateOffloadInfo(validBase)); } - result.emplace_back(device, config, validFlags); + result.emplace_back(device, mixPort.getName(), config, validFlags); } { AudioConfig config{.base = validBase}; @@ -168,7 +168,7 @@ const std::vector& getOutputDeviceInvalidConfigParameters if (isOffload) { config.offloadInfo.info(generateOffloadInfo(validBase)); } - result.emplace_back(device, config, validFlags); + result.emplace_back(device, mixPort.getName(), config, validFlags); } if (generateInvalidFlags) { AudioConfig config{.base = validBase}; @@ -176,32 +176,32 @@ const std::vector& getOutputDeviceInvalidConfigParameters config.offloadInfo.info(generateOffloadInfo(validBase)); } std::vector flags = {"random_string", ""}; - result.emplace_back(device, config, flags); + result.emplace_back(device, mixPort.getName(), config, flags); } if (isOffload) { { AudioConfig config{.base = validBase}; config.offloadInfo.info(generateOffloadInfo(validBase)); config.offloadInfo.info().base.channelMask = "random_string"; - result.emplace_back(device, config, validFlags); + result.emplace_back(device, mixPort.getName(), config, validFlags); } { AudioConfig config{.base = validBase}; config.offloadInfo.info(generateOffloadInfo(validBase)); config.offloadInfo.info().base.format = "random_string"; - result.emplace_back(device, config, validFlags); + result.emplace_back(device, mixPort.getName(), config, validFlags); } { AudioConfig config{.base = validBase}; config.offloadInfo.info(generateOffloadInfo(validBase)); config.offloadInfo.info().streamType = "random_string"; - result.emplace_back(device, config, validFlags); + result.emplace_back(device, mixPort.getName(), config, validFlags); } { AudioConfig config{.base = validBase}; config.offloadInfo.info(generateOffloadInfo(validBase)); config.offloadInfo.info().usage = "random_string"; - result.emplace_back(device, config, validFlags); + result.emplace_back(device, mixPort.getName(), config, validFlags); } hasOffloadConfig = true; } else { @@ -234,7 +234,7 @@ std::vector generateInputDeviceConfigParameters(bool oneP auto configs = combineAudioConfig(profile.getChannelMasks(), profile.getSamplingRates(), profile.getFormat()); for (const auto& config : configs) { - result.emplace_back(device, config, flags); + result.emplace_back(device, mixPort.getName(), config, flags); if (oneProfilePerDevice) break; } if (oneProfilePerDevice) break; @@ -285,17 +285,17 @@ const std::vector& getInputDeviceInvalidConfigParameters( { AudioConfig config{.base = validBase}; config.base.channelMask = "random_string"; - result.emplace_back(device, config, validFlags); + result.emplace_back(device, mixPort.getName(), config, validFlags); } { AudioConfig config{.base = validBase}; config.base.format = "random_string"; - result.emplace_back(device, config, validFlags); + result.emplace_back(device, mixPort.getName(), config, validFlags); } if (generateInvalidFlags) { AudioConfig config{.base = validBase}; std::vector flags = {"random_string", ""}; - result.emplace_back(device, config, flags); + result.emplace_back(device, mixPort.getName(), config, flags); } hasConfig = true; break; diff --git a/audio/core/all-versions/vts/functional/7.0/PolicyConfig.cpp b/audio/core/all-versions/vts/functional/7.0/PolicyConfig.cpp new file mode 100644 index 0000000000..29882077d1 --- /dev/null +++ b/audio/core/all-versions/vts/functional/7.0/PolicyConfig.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2021 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 "DeviceManager.h" +#include "PolicyConfig.h" +#include "common/all-versions/HidlSupport.h" + +using ::android::NO_ERROR; +using ::android::OK; + +using namespace ::android::hardware::audio::common::CPP_VERSION; +using namespace ::android::hardware::audio::CPP_VERSION; +using ::android::hardware::audio::common::CPP_VERSION::implementation::HidlUtils; +using ::android::hardware::audio::common::utils::splitString; +namespace xsd { +using namespace ::android::audio::policy::configuration::CPP_VERSION; +using Module = Modules::Module; +} // namespace xsd + +std::string PolicyConfig::getError() const { + if (mFilePath.empty()) { + return "Could not find " + mConfigFileName + + " file in: " + testing::PrintToString(android::audio_get_configuration_paths()); + } else { + return "Invalid config file: " + mFilePath; + } +} + +const xsd::Module* PolicyConfig::getModuleFromName(const std::string& name) const { + if (mConfig && mConfig->getFirstModules()) { + for (const auto& module : mConfig->getFirstModules()->get_module()) { + if (module.getName() == name) return &module; + } + } + return nullptr; +} + +std::optional PolicyConfig::getSinkDeviceForMixPort( + const std::string& moduleName, const std::string& mixPortName) const { + std::string device; + if (auto module = getModuleFromName(moduleName); module) { + auto possibleDevices = getSinkDevicesForMixPort(moduleName, mixPortName); + if (module->hasDefaultOutputDevice() && + possibleDevices.count(module->getDefaultOutputDevice())) { + device = module->getDefaultOutputDevice(); + } else { + device = getAttachedSinkDeviceForMixPort(moduleName, mixPortName); + } + } + if (!device.empty()) { + return getDeviceAddressOfDevicePort(moduleName, device); + } + ALOGE("Could not find a route for the mix port \"%s\" in module \"%s\"", mixPortName.c_str(), + moduleName.c_str()); + return std::optional{}; +} + +std::optional PolicyConfig::getSourceDeviceForMixPort( + const std::string& moduleName, const std::string& mixPortName) const { + const std::string device = getAttachedSourceDeviceForMixPort(moduleName, mixPortName); + if (!device.empty()) { + return getDeviceAddressOfDevicePort(moduleName, device); + } + ALOGE("Could not find a route for the mix port \"%s\" in module \"%s\"", mixPortName.c_str(), + moduleName.c_str()); + return std::optional{}; +} + +bool PolicyConfig::haveInputProfilesInModule(const std::string& name) const { + auto module = getModuleFromName(name); + if (module && module->getFirstMixPorts()) { + for (const auto& mixPort : module->getFirstMixPorts()->getMixPort()) { + if (mixPort.getRole() == xsd::Role::sink) return true; + } + } + return false; +} + +// static +std::string PolicyConfig::findExistingConfigurationFile(const std::string& fileName) { + for (const auto& location : android::audio_get_configuration_paths()) { + std::string path = location + '/' + fileName; + if (access(path.c_str(), F_OK) == 0) { + return path; + } + } + return {}; +} + +std::string PolicyConfig::findAttachedDevice(const std::vector& attachedDevices, + const std::set& possibleDevices) const { + for (const auto& device : attachedDevices) { + if (possibleDevices.count(device)) return device; + } + return {}; +} + +const std::vector& PolicyConfig::getAttachedDevices( + const std::string& moduleName) const { + static const std::vector empty; + auto module = getModuleFromName(moduleName); + if (module && module->getFirstAttachedDevices()) { + return module->getFirstAttachedDevices()->getItem(); + } + return empty; +} + +std::optional PolicyConfig::getDeviceAddressOfDevicePort( + const std::string& moduleName, const std::string& devicePortName) const { + auto module = getModuleFromName(moduleName); + if (module->getFirstDevicePorts()) { + const auto& devicePorts = module->getFirstDevicePorts()->getDevicePort(); + const auto& devicePort = std::find_if( + devicePorts.begin(), devicePorts.end(), + [&devicePortName](auto dp) { return dp.getTagName() == devicePortName; }); + if (devicePort != devicePorts.end()) { + audio_devices_t halDeviceType; + if (HidlUtils::audioDeviceTypeToHal(devicePort->getType(), &halDeviceType) == + NO_ERROR) { + // For AOSP device types use the standard parser for the device address. + const std::string address = + devicePort->hasAddress() ? devicePort->getAddress() : ""; + DeviceAddress result; + if (HidlUtils::deviceAddressFromHal(halDeviceType, address.c_str(), &result) == + NO_ERROR) { + return result; + } + } else if (xsd::isVendorExtension(devicePort->getType())) { + DeviceAddress result; + result.deviceType = devicePort->getType(); + if (devicePort->hasAddress()) { + result.address.id(devicePort->getAddress()); + } + return result; + } + } else { + ALOGE("Device port \"%s\" not found in module \"%s\"", devicePortName.c_str(), + moduleName.c_str()); + } + } else { + ALOGE("Module \"%s\" has no device ports", moduleName.c_str()); + } + return std::optional{}; +} + +std::set PolicyConfig::getSinkDevicesForMixPort(const std::string& moduleName, + const std::string& mixPortName) const { + std::set result; + auto module = getModuleFromName(moduleName); + if (module && module->getFirstRoutes()) { + for (const auto& route : module->getFirstRoutes()->getRoute()) { + const auto sources = splitString(route.getSources(), ','); + if (std::find(sources.begin(), sources.end(), mixPortName) != sources.end()) { + result.insert(route.getSink()); + } + } + } + return result; +} + +std::set PolicyConfig::getSourceDevicesForMixPort( + const std::string& moduleName, const std::string& mixPortName) const { + std::set result; + auto module = getModuleFromName(moduleName); + if (module && module->getFirstRoutes()) { + const auto& routes = module->getFirstRoutes()->getRoute(); + const auto route = std::find_if(routes.begin(), routes.end(), [&mixPortName](auto rte) { + return rte.getSink() == mixPortName; + }); + if (route != routes.end()) { + const auto sources = splitString(route->getSources(), ','); + std::copy(sources.begin(), sources.end(), std::inserter(result, result.end())); + } + } + return result; +} + +void PolicyConfig::init() { + if (mConfig) { + mStatus = OK; + mPrimaryModule = getModuleFromName(DeviceManager::kPrimaryDevice); + if (mConfig->getFirstModules()) { + for (const auto& module : mConfig->getFirstModules()->get_module()) { + if (module.getFirstAttachedDevices()) { + auto attachedDevices = module.getFirstAttachedDevices()->getItem(); + if (!attachedDevices.empty()) { + mModulesWithDevicesNames.insert(module.getName()); + } + } + } + } + } +} diff --git a/audio/core/all-versions/vts/functional/7.0/PolicyConfig.h b/audio/core/all-versions/vts/functional/7.0/PolicyConfig.h index feb4d4b3aa..f798839cb8 100644 --- a/audio/core/all-versions/vts/functional/7.0/PolicyConfig.h +++ b/audio/core/all-versions/vts/functional/7.0/PolicyConfig.h @@ -16,15 +16,12 @@ #pragma once -#include -#include - #include #include #include +#include #include -#include #include // clang-format off @@ -35,12 +32,6 @@ #include #include -#include "DeviceManager.h" - -using ::android::NO_INIT; -using ::android::OK; -using ::android::status_t; - using namespace ::android::hardware::audio::common::CPP_VERSION; using namespace ::android::hardware::audio::CPP_VERSION; namespace xsd { @@ -62,69 +53,49 @@ class PolicyConfig { mConfig{xsd::read(mFilePath.c_str())} { init(); } - status_t getStatus() const { return mStatus; } - std::string getError() const { - if (mFilePath.empty()) { - return std::string{"Could not find "} + mConfigFileName + - " file in: " + testing::PrintToString(android::audio_get_configuration_paths()); - } else { - return "Invalid config file: " + mFilePath; - } - } + android::status_t getStatus() const { return mStatus; } + std::string getError() const; const std::string& getFilePath() const { return mFilePath; } - const xsd::Module* getModuleFromName(const std::string& name) const { - if (mConfig && mConfig->getFirstModules()) { - for (const auto& module : mConfig->getFirstModules()->get_module()) { - if (module.getName() == name) return &module; - } - } - return nullptr; - } + const xsd::Module* getModuleFromName(const std::string& name) const; const xsd::Module* getPrimaryModule() const { return mPrimaryModule; } const std::set& getModulesWithDevicesNames() const { return mModulesWithDevicesNames; } - bool haveInputProfilesInModule(const std::string& name) const { - auto module = getModuleFromName(name); - if (module && module->getFirstMixPorts()) { - for (const auto& mixPort : module->getFirstMixPorts()->getMixPort()) { - if (mixPort.getRole() == xsd::Role::sink) return true; - } - } - return false; + std::string getAttachedSinkDeviceForMixPort(const std::string& moduleName, + const std::string& mixPortName) const { + return findAttachedDevice(getAttachedDevices(moduleName), + getSinkDevicesForMixPort(moduleName, mixPortName)); } + std::string getAttachedSourceDeviceForMixPort(const std::string& moduleName, + const std::string& mixPortName) const { + return findAttachedDevice(getAttachedDevices(moduleName), + getSourceDevicesForMixPort(moduleName, mixPortName)); + } + std::optional getSinkDeviceForMixPort(const std::string& moduleName, + const std::string& mixPortName) const; + std::optional getSourceDeviceForMixPort(const std::string& moduleName, + const std::string& mixPortName) const; + bool haveInputProfilesInModule(const std::string& name) const; private: - static std::string findExistingConfigurationFile(const std::string& fileName) { - for (const auto& location : android::audio_get_configuration_paths()) { - std::string path = location + '/' + fileName; - if (access(path.c_str(), F_OK) == 0) { - return path; - } - } - return std::string{}; - } - void init() { - if (mConfig) { - mStatus = OK; - mPrimaryModule = getModuleFromName(DeviceManager::kPrimaryDevice); - if (mConfig->getFirstModules()) { - for (const auto& module : mConfig->getFirstModules()->get_module()) { - if (module.getFirstAttachedDevices()) { - auto attachedDevices = module.getFirstAttachedDevices()->getItem(); - if (!attachedDevices.empty()) { - mModulesWithDevicesNames.insert(module.getName()); - } - } - } - } - } - } + static std::string findExistingConfigurationFile(const std::string& fileName); + std::string findAttachedDevice(const std::vector& attachedDevices, + const std::set& possibleDevices) const; + const std::vector& getAttachedDevices(const std::string& moduleName) const; + std::optional getDeviceAddressOfDevicePort( + const std::string& moduleName, const std::string& devicePortName) const; + std::string getDevicePortTagNameFromType(const std::string& moduleName, + const AudioDevice& deviceType) const; + std::set getSinkDevicesForMixPort(const std::string& moduleName, + const std::string& mixPortName) const; + std::set getSourceDevicesForMixPort(const std::string& moduleName, + const std::string& mixPortName) const; + void init(); const std::string mConfigFileName; const std::string mFilePath; std::optional mConfig; - status_t mStatus = NO_INIT; + android::status_t mStatus = android::NO_INIT; const xsd::Module* mPrimaryModule; std::set mModulesWithDevicesNames; }; diff --git a/audio/core/all-versions/vts/functional/Android.bp b/audio/core/all-versions/vts/functional/Android.bp index 91c54dce35..91831912dd 100644 --- a/audio/core/all-versions/vts/functional/Android.bp +++ b/audio/core/all-versions/vts/functional/Android.bp @@ -154,6 +154,7 @@ cc_test { srcs: [ "7.0/AudioPrimaryHidlHalTest.cpp", "7.0/Generators.cpp", + "7.0/PolicyConfig.cpp", ], generated_headers: ["audio_policy_configuration_V7_0_parser"], generated_sources: ["audio_policy_configuration_V7_0_parser"], @@ -161,6 +162,7 @@ cc_test { "android.hardware.audio@7.0", "android.hardware.audio.common@7.0", "android.hardware.audio.common@7.0-enums", + "android.hardware.audio.common@7.0-util", ], cflags: [ "-DMAJOR_VERSION=7", @@ -176,7 +178,15 @@ cc_test { } // Note: the following aren't VTS tests, but rather unit tests -// to verify correctness of test parameter generator utilities. +// to verify correctness of test utilities. +cc_test { + name: "HalAudioStreamWorkerTest", + host_supported: true, + srcs: [ + "tests/streamworker_tests.cpp", + ], +} + cc_test { name: "HalAudioV6_0GeneratorTest", defaults: ["VtsHalAudioTargetTest_defaults"], @@ -208,6 +218,7 @@ cc_test { defaults: ["VtsHalAudioTargetTest_defaults"], srcs: [ "7.0/Generators.cpp", + "7.0/PolicyConfig.cpp", "tests/generators_tests.cpp", ], generated_headers: ["audio_policy_configuration_V7_0_parser"], @@ -216,6 +227,7 @@ cc_test { "android.hardware.audio@7.0", "android.hardware.audio.common@7.0", "android.hardware.audio.common@7.0-enums", + "android.hardware.audio.common@7.0-util", ], cflags: [ "-DMAJOR_VERSION=7", diff --git a/audio/core/all-versions/vts/functional/AudioPrimaryHidlHalTest.h b/audio/core/all-versions/vts/functional/AudioPrimaryHidlHalTest.h index 56939fe6e6..ae1467d406 100644 --- a/audio/core/all-versions/vts/functional/AudioPrimaryHidlHalTest.h +++ b/audio/core/all-versions/vts/functional/AudioPrimaryHidlHalTest.h @@ -89,6 +89,10 @@ using ::android::hardware::details::toHexString; using namespace ::android::hardware::audio::common::CPP_VERSION; using namespace ::android::hardware::audio::common::test::utility; using namespace ::android::hardware::audio::CPP_VERSION; +using ReadParameters = ::android::hardware::audio::CPP_VERSION::IStreamIn::ReadParameters; +using ReadStatus = ::android::hardware::audio::CPP_VERSION::IStreamIn::ReadStatus; +using WriteCommand = ::android::hardware::audio::CPP_VERSION::IStreamOut::WriteCommand; +using WriteStatus = ::android::hardware::audio::CPP_VERSION::IStreamOut::WriteStatus; #if MAJOR_VERSION >= 7 // Make an alias for enumerations generated from the APM config XSD. namespace xsd { @@ -100,6 +104,7 @@ using namespace ::android::audio::policy::configuration::CPP_VERSION; static auto okOrNotSupported = {Result::OK, Result::NOT_SUPPORTED}; static auto okOrNotSupportedOrInvalidArgs = {Result::OK, Result::NOT_SUPPORTED, Result::INVALID_ARGUMENTS}; +static auto okOrInvalidState = {Result::OK, Result::INVALID_STATE}; static auto okOrInvalidStateOrNotSupported = {Result::OK, Result::INVALID_STATE, Result::NOT_SUPPORTED}; static auto invalidArgsOrNotSupported = {Result::INVALID_ARGUMENTS, Result::NOT_SUPPORTED}; @@ -115,6 +120,7 @@ static auto invalidStateOrNotSupported = {Result::INVALID_STATE, Result::NOT_SUP #include "7.0/Generators.h" #include "7.0/PolicyConfig.h" #endif +#include "StreamWorker.h" class HidlTest : public ::testing::Test { public: @@ -778,6 +784,11 @@ TEST_P(AudioHidlDeviceTest, DebugDumpInvalidArguments) { ////////////////////////// open{Output,Input}Stream ////////////////////////// ////////////////////////////////////////////////////////////////////////////// +static inline AudioIoHandle getNextIoHandle() { + static AudioIoHandle lastHandle{}; + return ++lastHandle; +} + // This class is also used by some device tests. template class StreamHelper { @@ -787,16 +798,13 @@ class StreamHelper { template void open(Open openStream, const AudioConfig& config, Result* res, AudioConfig* suggestedConfigPtr) { - // FIXME: Open a stream without an IOHandle - // This is not required to be accepted by hal implementations - AudioIoHandle ioHandle{}; AudioConfig suggestedConfig{}; bool retryWithSuggestedConfig = true; if (suggestedConfigPtr == nullptr) { suggestedConfigPtr = &suggestedConfig; retryWithSuggestedConfig = false; } - ASSERT_OK(openStream(ioHandle, config, returnIn(*res, mStream, *suggestedConfigPtr))); + ASSERT_OK(openStream(mIoHandle, config, returnIn(*res, mStream, *suggestedConfigPtr))); switch (*res) { case Result::OK: ASSERT_TRUE(mStream != nullptr); @@ -806,7 +814,7 @@ class StreamHelper { ASSERT_TRUE(mStream == nullptr); if (retryWithSuggestedConfig) { AudioConfig suggestedConfigRetry; - ASSERT_OK(openStream(ioHandle, *suggestedConfigPtr, + ASSERT_OK(openStream(mIoHandle, *suggestedConfigPtr, returnIn(*res, mStream, suggestedConfigRetry))); ASSERT_OK(*res); ASSERT_TRUE(mStream != nullptr); @@ -834,8 +842,10 @@ class StreamHelper { #endif } } + AudioIoHandle getIoHandle() const { return mIoHandle; } private: + const AudioIoHandle mIoHandle = getNextIoHandle(); sp& mStream; }; @@ -861,7 +871,6 @@ class OpenStreamTest : public AudioHidlTestWithDeviceConfigParameter { return res; } - private: void TearDown() override { if (open) { ASSERT_OK(closeStream()); @@ -879,6 +888,116 @@ class OpenStreamTest : public AudioHidlTestWithDeviceConfigParameter { ////////////////////////////// openOutputStream ////////////////////////////// +class StreamWriter : public StreamWorker { + public: + StreamWriter(IStreamOut* stream, size_t bufferSize) + : mStream(stream), mBufferSize(bufferSize), mData(mBufferSize) {} + ~StreamWriter() { + stop(); + if (mEfGroup) { + EventFlag::deleteEventFlag(&mEfGroup); + } + } + + typedef MessageQueue CommandMQ; + typedef MessageQueue DataMQ; + typedef MessageQueue StatusMQ; + + bool workerInit() { + std::unique_ptr tempCommandMQ; + std::unique_ptr tempDataMQ; + std::unique_ptr tempStatusMQ; + Result retval; + Return ret = mStream->prepareForWriting( + 1, mBufferSize, + [&](Result r, const CommandMQ::Descriptor& commandMQ, + const DataMQ::Descriptor& dataMQ, const StatusMQ::Descriptor& statusMQ, + const auto& /*halThreadInfo*/) { + retval = r; + if (retval == Result::OK) { + tempCommandMQ.reset(new CommandMQ(commandMQ)); + tempDataMQ.reset(new DataMQ(dataMQ)); + tempStatusMQ.reset(new StatusMQ(statusMQ)); + if (tempDataMQ->isValid() && tempDataMQ->getEventFlagWord()) { + EventFlag::createEventFlag(tempDataMQ->getEventFlagWord(), &mEfGroup); + } + } + }); + if (!ret.isOk()) { + ALOGE("Transport error while calling prepareForWriting: %s", ret.description().c_str()); + return false; + } + if (retval != Result::OK) { + ALOGE("Error from prepareForWriting: %d", retval); + return false; + } + if (!tempCommandMQ || !tempCommandMQ->isValid() || !tempDataMQ || !tempDataMQ->isValid() || + !tempStatusMQ || !tempStatusMQ->isValid() || !mEfGroup) { + ALOGE_IF(!tempCommandMQ, "Failed to obtain command message queue for writing"); + ALOGE_IF(tempCommandMQ && !tempCommandMQ->isValid(), + "Command message queue for writing is invalid"); + ALOGE_IF(!tempDataMQ, "Failed to obtain data message queue for writing"); + ALOGE_IF(tempDataMQ && !tempDataMQ->isValid(), + "Data message queue for writing is invalid"); + ALOGE_IF(!tempStatusMQ, "Failed to obtain status message queue for writing"); + ALOGE_IF(tempStatusMQ && !tempStatusMQ->isValid(), + "Status message queue for writing is invalid"); + ALOGE_IF(!mEfGroup, "Event flag creation for writing failed"); + return false; + } + mCommandMQ = std::move(tempCommandMQ); + mDataMQ = std::move(tempDataMQ); + mStatusMQ = std::move(tempStatusMQ); + return true; + } + + bool workerCycle() { + WriteCommand cmd = WriteCommand::WRITE; + if (!mCommandMQ->write(&cmd)) { + ALOGE("command message queue write failed"); + return false; + } + const size_t dataSize = std::min(mData.size(), mDataMQ->availableToWrite()); + bool success = mDataMQ->write(mData.data(), dataSize); + ALOGE_IF(!success, "data message queue write failed"); + mEfGroup->wake(static_cast(MessageQueueFlagBits::NOT_EMPTY)); + + uint32_t efState = 0; + retry: + status_t ret = + mEfGroup->wait(static_cast(MessageQueueFlagBits::NOT_FULL), &efState); + if (efState & static_cast(MessageQueueFlagBits::NOT_FULL)) { + WriteStatus writeStatus; + writeStatus.retval = Result::NOT_INITIALIZED; + if (!mStatusMQ->read(&writeStatus)) { + ALOGE("status message read failed"); + success = false; + } + if (writeStatus.retval != Result::OK) { + ALOGE("bad write status: %d", writeStatus.retval); + success = false; + } + } + if (ret == -EAGAIN || ret == -EINTR) { + // Spurious wakeup. This normally retries no more than once. + goto retry; + } else if (ret) { + ALOGE("bad wait status: %d", ret); + success = false; + } + return success; + } + + private: + IStreamOut* const mStream; + const size_t mBufferSize; + std::vector mData; + std::unique_ptr mCommandMQ; + std::unique_ptr mDataMQ; + std::unique_ptr mStatusMQ; + EventFlag* mEfGroup = nullptr; +}; + class OutputStreamTest : public OpenStreamTest { void SetUp() override { ASSERT_NO_FATAL_FAILURE(OpenStreamTest::SetUp()); // setup base @@ -954,6 +1073,121 @@ GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OutputStreamTest); ////////////////////////////// openInputStream ////////////////////////////// +class StreamReader : public StreamWorker { + public: + StreamReader(IStreamIn* stream, size_t bufferSize) + : mStream(stream), mBufferSize(bufferSize), mData(mBufferSize) {} + ~StreamReader() { + stop(); + if (mEfGroup) { + EventFlag::deleteEventFlag(&mEfGroup); + } + } + + typedef MessageQueue CommandMQ; + typedef MessageQueue DataMQ; + typedef MessageQueue StatusMQ; + + bool workerInit() { + std::unique_ptr tempCommandMQ; + std::unique_ptr tempDataMQ; + std::unique_ptr tempStatusMQ; + Result retval; + Return ret = mStream->prepareForReading( + 1, mBufferSize, + [&](Result r, const CommandMQ::Descriptor& commandMQ, + const DataMQ::Descriptor& dataMQ, const StatusMQ::Descriptor& statusMQ, + const auto& /*halThreadInfo*/) { + retval = r; + if (retval == Result::OK) { + tempCommandMQ.reset(new CommandMQ(commandMQ)); + tempDataMQ.reset(new DataMQ(dataMQ)); + tempStatusMQ.reset(new StatusMQ(statusMQ)); + if (tempDataMQ->isValid() && tempDataMQ->getEventFlagWord()) { + EventFlag::createEventFlag(tempDataMQ->getEventFlagWord(), &mEfGroup); + } + } + }); + if (!ret.isOk()) { + ALOGE("Transport error while calling prepareForReading: %s", ret.description().c_str()); + return false; + } + if (retval != Result::OK) { + ALOGE("Error from prepareForReading: %d", retval); + return false; + } + if (!tempCommandMQ || !tempCommandMQ->isValid() || !tempDataMQ || !tempDataMQ->isValid() || + !tempStatusMQ || !tempStatusMQ->isValid() || !mEfGroup) { + ALOGE_IF(!tempCommandMQ, "Failed to obtain command message queue for reading"); + ALOGE_IF(tempCommandMQ && !tempCommandMQ->isValid(), + "Command message queue for reading is invalid"); + ALOGE_IF(!tempDataMQ, "Failed to obtain data message queue for reading"); + ALOGE_IF(tempDataMQ && !tempDataMQ->isValid(), + "Data message queue for reading is invalid"); + ALOGE_IF(!tempStatusMQ, "Failed to obtain status message queue for reading"); + ALOGE_IF(tempStatusMQ && !tempStatusMQ->isValid(), + "Status message queue for reading is invalid"); + ALOGE_IF(!mEfGroup, "Event flag creation for reading failed"); + return false; + } + mCommandMQ = std::move(tempCommandMQ); + mDataMQ = std::move(tempDataMQ); + mStatusMQ = std::move(tempStatusMQ); + return true; + } + + bool workerCycle() { + ReadParameters params; + params.command = IStreamIn::ReadCommand::READ; + params.params.read = mBufferSize; + if (!mCommandMQ->write(¶ms)) { + ALOGE("command message queue write failed"); + return false; + } + mEfGroup->wake(static_cast(MessageQueueFlagBits::NOT_FULL)); + + uint32_t efState = 0; + bool success = true; + retry: + status_t ret = + mEfGroup->wait(static_cast(MessageQueueFlagBits::NOT_EMPTY), &efState); + if (efState & static_cast(MessageQueueFlagBits::NOT_EMPTY)) { + ReadStatus readStatus; + readStatus.retval = Result::NOT_INITIALIZED; + if (!mStatusMQ->read(&readStatus)) { + ALOGE("status message read failed"); + success = false; + } + if (readStatus.retval != Result::OK) { + ALOGE("bad read status: %d", readStatus.retval); + success = false; + } + const size_t dataSize = std::min(mData.size(), mDataMQ->availableToRead()); + if (!mDataMQ->read(mData.data(), dataSize)) { + ALOGE("data message queue read failed"); + success = false; + } + } + if (ret == -EAGAIN || ret == -EINTR) { + // Spurious wakeup. This normally retries no more than once. + goto retry; + } else if (ret) { + ALOGE("bad wait status: %d", ret); + success = false; + } + return success; + } + + private: + IStreamIn* const mStream; + const size_t mBufferSize; + std::vector mData; + std::unique_ptr mCommandMQ; + std::unique_ptr mDataMQ; + std::unique_ptr mStatusMQ; + EventFlag* mEfGroup = nullptr; +}; + class InputStreamTest : public OpenStreamTest { void SetUp() override { ASSERT_NO_FATAL_FAILURE(OpenStreamTest::SetUp()); // setup base @@ -1377,6 +1611,12 @@ TEST_P(InputStreamTest, getCapturePosition) { uint64_t frames; uint64_t time; ASSERT_OK(stream->getCapturePosition(returnIn(res, frames, time))); + // Although 'getCapturePosition' is mandatory in V7, legacy implementations + // may return -ENOSYS (which is translated to NOT_SUPPORTED) in cases when + // the capture position can't be retrieved, e.g. when the stream isn't + // running. Because of this, we don't fail when getting NOT_SUPPORTED + // in this test. Behavior of 'getCapturePosition' for running streams is + // tested in 'PcmOnlyConfigInputStreamTest' for V7. ASSERT_RESULT(okOrInvalidStateOrNotSupported, res); if (res == Result::OK) { ASSERT_EQ(0U, frames); @@ -1560,15 +1800,19 @@ TEST_P(OutputStreamTest, GetPresentationPositionStop) { "If supported, a stream should always succeed to retrieve the " "presentation position"); uint64_t frames; - TimeSpec mesureTS; - ASSERT_OK(stream->getPresentationPosition(returnIn(res, frames, mesureTS))); + TimeSpec measureTS; + ASSERT_OK(stream->getPresentationPosition(returnIn(res, frames, measureTS))); +#if MAJOR_VERSION <= 6 if (res == Result::NOT_SUPPORTED) { - doc::partialTest("getpresentationPosition is not supported"); + doc::partialTest("getPresentationPosition is not supported"); return; } +#else + ASSERT_NE(Result::NOT_SUPPORTED, res) << "getPresentationPosition is mandatory in V7"; +#endif ASSERT_EQ(0U, frames); - if (mesureTS.tvNSec == 0 && mesureTS.tvSec == 0) { + if (measureTS.tvNSec == 0 && measureTS.tvSec == 0) { // As the stream has never written a frame yet, // the timestamp does not really have a meaning, allow to return 0 return; @@ -1580,8 +1824,8 @@ TEST_P(OutputStreamTest, GetPresentationPositionStop) { auto toMicroSec = [](uint64_t sec, auto nsec) { return sec * 1e+6 + nsec / 1e+3; }; auto currentTime = toMicroSec(currentTS.tv_sec, currentTS.tv_nsec); - auto mesureTime = toMicroSec(mesureTS.tvSec, mesureTS.tvNSec); - ASSERT_PRED2([](auto c, auto m) { return c - m < 1e+6; }, currentTime, mesureTime); + auto measureTime = toMicroSec(measureTS.tvSec, measureTS.tvNSec); + ASSERT_PRED2([](auto c, auto m) { return c - m < 1e+6; }, currentTime, measureTime); } ////////////////////////////////////////////////////////////////////////////// diff --git a/audio/core/all-versions/vts/functional/AudioTestDefinitions.h b/audio/core/all-versions/vts/functional/AudioTestDefinitions.h index 5b14a216c9..aa676308cb 100644 --- a/audio/core/all-versions/vts/functional/AudioTestDefinitions.h +++ b/audio/core/all-versions/vts/functional/AudioTestDefinitions.h @@ -31,15 +31,17 @@ using DeviceParameter = std::tuple; // Nesting a tuple in another tuple allows to use GTest Combine function to generate // all combinations of devices and configs. -enum { PARAM_DEVICE, PARAM_CONFIG, PARAM_FLAGS }; #if MAJOR_VERSION <= 6 +enum { PARAM_DEVICE, PARAM_CONFIG, PARAM_FLAGS }; enum { INDEX_INPUT, INDEX_OUTPUT }; using DeviceConfigParameter = std::tuple>; #elif MAJOR_VERSION >= 7 +enum { PARAM_DEVICE, PARAM_PORT_NAME, PARAM_CONFIG, PARAM_FLAGS }; using DeviceConfigParameter = - std::tuple>; #endif diff --git a/audio/core/all-versions/vts/functional/StreamWorker.h b/audio/core/all-versions/vts/functional/StreamWorker.h new file mode 100644 index 0000000000..68a8024b1e --- /dev/null +++ b/audio/core/all-versions/vts/functional/StreamWorker.h @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2021 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 + +template +class StreamWorker { + enum class WorkerState { STOPPED, RUNNING, PAUSE_REQUESTED, PAUSED, RESUME_REQUESTED, ERROR }; + + public: + StreamWorker() = default; + ~StreamWorker() { stop(); } + bool start() { + mWorker = std::thread(&StreamWorker::workerThread, this); + std::unique_lock lock(mWorkerLock); + mWorkerCv.wait(lock, [&] { return mWorkerState != WorkerState::STOPPED; }); + return mWorkerState == WorkerState::RUNNING; + } + void pause() { switchWorkerStateSync(WorkerState::RUNNING, WorkerState::PAUSE_REQUESTED); } + void resume() { switchWorkerStateSync(WorkerState::PAUSED, WorkerState::RESUME_REQUESTED); } + bool hasError() { + std::lock_guard lock(mWorkerLock); + return mWorkerState == WorkerState::ERROR; + } + void stop() { + { + std::lock_guard lock(mWorkerLock); + if (mWorkerState == WorkerState::STOPPED) return; + mWorkerState = WorkerState::STOPPED; + } + if (mWorker.joinable()) { + mWorker.join(); + } + } + bool waitForAtLeastOneCycle() { + WorkerState newState; + switchWorkerStateSync(WorkerState::RUNNING, WorkerState::PAUSE_REQUESTED, &newState); + if (newState != WorkerState::PAUSED) return false; + switchWorkerStateSync(newState, WorkerState::RESUME_REQUESTED, &newState); + return newState == WorkerState::RUNNING; + } + + // Methods that need to be provided by subclasses: + // + // Called once at the beginning of the thread loop. Must return + // 'true' to enter the thread loop, otherwise the thread loop + // exits and the worker switches into the 'error' state. + // bool workerInit(); + // + // Called for each thread loop unless the thread is in 'paused' state. + // Must return 'true' to continue running, otherwise the thread loop + // exits and the worker switches into the 'error' state. + // bool workerCycle(); + + private: + void switchWorkerStateSync(WorkerState oldState, WorkerState newState, + WorkerState* finalState = nullptr) { + std::unique_lock lock(mWorkerLock); + if (mWorkerState != oldState) { + if (finalState) *finalState = mWorkerState; + return; + } + mWorkerState = newState; + mWorkerCv.wait(lock, [&] { return mWorkerState != newState; }); + if (finalState) *finalState = mWorkerState; + } + void workerThread() { + bool success = static_cast(this)->workerInit(); + { + std::lock_guard lock(mWorkerLock); + mWorkerState = success ? WorkerState::RUNNING : WorkerState::ERROR; + } + mWorkerCv.notify_one(); + if (!success) return; + + for (WorkerState state = WorkerState::RUNNING; state != WorkerState::STOPPED;) { + bool needToNotify = false; + if (state != WorkerState::PAUSED ? static_cast(this)->workerCycle() + : (sched_yield(), true)) { + // + // Pause and resume are synchronous. One worker cycle must complete + // before the worker indicates a state change. This is how 'mWorkerState' and + // 'state' interact: + // + // mWorkerState == RUNNING + // client sets mWorkerState := PAUSE_REQUESTED + // last workerCycle gets executed, state := mWorkerState := PAUSED by us + // (or the workers enters the 'error' state if workerCycle fails) + // client gets notified about state change in any case + // thread is doing a busy wait while 'state == PAUSED' + // client sets mWorkerState := RESUME_REQUESTED + // state := mWorkerState (RESUME_REQUESTED) + // mWorkerState := RUNNING, but we don't notify the client yet + // first workerCycle gets executed, the code below triggers a client notification + // (or if workerCycle fails, worker enters 'error' state and also notifies) + // state := mWorkerState (RUNNING) + if (state == WorkerState::RESUME_REQUESTED) { + needToNotify = true; + } + std::lock_guard lock(mWorkerLock); + state = mWorkerState; + if (mWorkerState == WorkerState::PAUSE_REQUESTED) { + state = mWorkerState = WorkerState::PAUSED; + needToNotify = true; + } else if (mWorkerState == WorkerState::RESUME_REQUESTED) { + mWorkerState = WorkerState::RUNNING; + } + } else { + std::lock_guard lock(mWorkerLock); + if (state == WorkerState::RESUME_REQUESTED || + mWorkerState == WorkerState::PAUSE_REQUESTED) { + needToNotify = true; + } + mWorkerState = WorkerState::ERROR; + state = WorkerState::STOPPED; + } + if (needToNotify) { + mWorkerCv.notify_one(); + } + } + } + + std::thread mWorker; + std::mutex mWorkerLock; + std::condition_variable mWorkerCv; + WorkerState mWorkerState = WorkerState::STOPPED; // GUARDED_BY(mWorkerLock); +}; diff --git a/audio/core/all-versions/vts/functional/tests/streamworker_tests.cpp b/audio/core/all-versions/vts/functional/tests/streamworker_tests.cpp new file mode 100644 index 0000000000..75116affc2 --- /dev/null +++ b/audio/core/all-versions/vts/functional/tests/streamworker_tests.cpp @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2021 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 "StreamWorker.h" + +#include +#include +#include + +#include +#define LOG_TAG "StreamWorker_Test" +#include + +struct TestStream { + std::atomic error = false; +}; + +class TestWorker : public StreamWorker { + public: + // Use nullptr to test error reporting from the worker thread. + explicit TestWorker(TestStream* stream) : mStream(stream) {} + + void ensureWorkerCycled() { + const size_t cyclesBefore = mWorkerCycles; + while (mWorkerCycles == cyclesBefore && !hasError()) { + sched_yield(); + } + } + size_t getWorkerCycles() const { return mWorkerCycles; } + bool hasWorkerCycleCalled() const { return mWorkerCycles != 0; } + bool hasNoWorkerCycleCalled(useconds_t usec) { + const size_t cyclesBefore = mWorkerCycles; + usleep(usec); + return mWorkerCycles == cyclesBefore; + } + + bool workerInit() { return mStream; } + bool workerCycle() { + do { + mWorkerCycles++; + } while (mWorkerCycles == 0); + return !mStream->error; + } + + private: + TestStream* const mStream; + std::atomic mWorkerCycles = 0; +}; + +// The parameter specifies whether an extra call to 'stop' is made at the end. +class StreamWorkerInvalidTest : public testing::TestWithParam { + public: + StreamWorkerInvalidTest() : StreamWorkerInvalidTest(nullptr) {} + void TearDown() override { + if (GetParam()) { + worker.stop(); + } + } + + protected: + StreamWorkerInvalidTest(TestStream* stream) : testing::TestWithParam(), worker(stream) {} + TestWorker worker; +}; + +TEST_P(StreamWorkerInvalidTest, Uninitialized) { + EXPECT_FALSE(worker.hasWorkerCycleCalled()); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerInvalidTest, UninitializedPauseIgnored) { + EXPECT_FALSE(worker.hasError()); + worker.pause(); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerInvalidTest, UninitializedResumeIgnored) { + EXPECT_FALSE(worker.hasError()); + worker.resume(); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerInvalidTest, Start) { + EXPECT_FALSE(worker.start()); + EXPECT_FALSE(worker.hasWorkerCycleCalled()); + EXPECT_TRUE(worker.hasError()); +} + +TEST_P(StreamWorkerInvalidTest, PauseIgnored) { + EXPECT_FALSE(worker.start()); + EXPECT_TRUE(worker.hasError()); + worker.pause(); + EXPECT_TRUE(worker.hasError()); +} + +TEST_P(StreamWorkerInvalidTest, ResumeIgnored) { + EXPECT_FALSE(worker.start()); + EXPECT_TRUE(worker.hasError()); + worker.resume(); + EXPECT_TRUE(worker.hasError()); +} + +INSTANTIATE_TEST_SUITE_P(StreamWorkerInvalid, StreamWorkerInvalidTest, testing::Bool()); + +class StreamWorkerTest : public StreamWorkerInvalidTest { + public: + StreamWorkerTest() : StreamWorkerInvalidTest(&stream) {} + + protected: + TestStream stream; +}; + +static constexpr unsigned kWorkerIdleCheckTime = 50 * 1000; + +TEST_P(StreamWorkerTest, Uninitialized) { + EXPECT_FALSE(worker.hasWorkerCycleCalled()); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, Start) { + ASSERT_TRUE(worker.start()); + worker.ensureWorkerCycled(); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, WorkerError) { + ASSERT_TRUE(worker.start()); + stream.error = true; + worker.ensureWorkerCycled(); + EXPECT_TRUE(worker.hasError()); + EXPECT_TRUE(worker.hasNoWorkerCycleCalled(kWorkerIdleCheckTime)); +} + +TEST_P(StreamWorkerTest, PauseResume) { + ASSERT_TRUE(worker.start()); + worker.ensureWorkerCycled(); + EXPECT_FALSE(worker.hasError()); + worker.pause(); + EXPECT_TRUE(worker.hasNoWorkerCycleCalled(kWorkerIdleCheckTime)); + EXPECT_FALSE(worker.hasError()); + const size_t workerCyclesBefore = worker.getWorkerCycles(); + worker.resume(); + // 'resume' is synchronous and returns after the worker has looped at least once. + EXPECT_GT(worker.getWorkerCycles(), workerCyclesBefore); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, StopPaused) { + ASSERT_TRUE(worker.start()); + worker.ensureWorkerCycled(); + EXPECT_FALSE(worker.hasError()); + worker.pause(); + worker.stop(); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, PauseAfterErrorIgnored) { + ASSERT_TRUE(worker.start()); + stream.error = true; + worker.ensureWorkerCycled(); + EXPECT_TRUE(worker.hasError()); + worker.pause(); + EXPECT_TRUE(worker.hasNoWorkerCycleCalled(kWorkerIdleCheckTime)); + EXPECT_TRUE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, ResumeAfterErrorIgnored) { + ASSERT_TRUE(worker.start()); + stream.error = true; + worker.ensureWorkerCycled(); + EXPECT_TRUE(worker.hasError()); + worker.resume(); + EXPECT_TRUE(worker.hasNoWorkerCycleCalled(kWorkerIdleCheckTime)); + EXPECT_TRUE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, WorkerErrorOnResume) { + ASSERT_TRUE(worker.start()); + worker.ensureWorkerCycled(); + EXPECT_FALSE(worker.hasError()); + worker.pause(); + EXPECT_FALSE(worker.hasError()); + stream.error = true; + EXPECT_FALSE(worker.hasError()); + worker.resume(); + worker.ensureWorkerCycled(); + EXPECT_TRUE(worker.hasError()); + EXPECT_TRUE(worker.hasNoWorkerCycleCalled(kWorkerIdleCheckTime)); +} + +TEST_P(StreamWorkerTest, WaitForAtLeastOneCycle) { + ASSERT_TRUE(worker.start()); + const size_t workerCyclesBefore = worker.getWorkerCycles(); + EXPECT_TRUE(worker.waitForAtLeastOneCycle()); + EXPECT_GT(worker.getWorkerCycles(), workerCyclesBefore); +} + +TEST_P(StreamWorkerTest, WaitForAtLeastOneCycleError) { + ASSERT_TRUE(worker.start()); + stream.error = true; + EXPECT_FALSE(worker.waitForAtLeastOneCycle()); +} + +INSTANTIATE_TEST_SUITE_P(StreamWorker, StreamWorkerTest, testing::Bool());