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
This commit is contained in:
Mikhail Naganov
2021-03-03 14:13:49 -08:00
parent 7665b917eb
commit 5a78d9c221
11 changed files with 1232 additions and 125 deletions

View File

@@ -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

View File

@@ -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<MicrophoneInfo> activeMicrophones;
Result readRes;
typedef MessageQueue<IStreamIn::ReadParameters, kSynchronizedReadWrite> CommandMQ;
typedef MessageQueue<uint8_t, kSynchronizedReadWrite> DataMQ;
std::unique_ptr<CommandMQ> commandMQ;
std::unique_ptr<DataMQ> 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(&params));
efGroup->wake(static_cast<uint32_t>(MessageQueueFlagBits::NOT_FULL));
uint32_t efState = 0;
efGroup->wait(static_cast<uint32_t>(MessageQueueFlagBits::NOT_EMPTY), &efState);
if (efState & static_cast<uint32_t>(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());
}
}
}

View File

@@ -14,6 +14,8 @@
* limitations under the License.
*/
#include <android-base/chrono_utils.h>
#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<DeviceConfigParameter>& getOutputDevicePcmOnlyConfigParameters() {
static const std::vector<DeviceConfigParameter> parameters = [] {
auto allParams = getOutputDeviceConfigParameters();
std::vector<DeviceConfigParameter> pcmParams;
std::copy_if(allParams.begin(), allParams.end(), std::back_inserter(pcmParams), [](auto cfg) {
const auto& flags = std::get<PARAM_FLAGS>(cfg);
return xsd::isLinearPcm(std::get<PARAM_CONFIG>(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<PARAM_DEVICE_NAME>(std::get<PARAM_DEVICE>(cfg)),
std::get<PARAM_PORT_NAME>(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<AudioPortConfig>{source},
hidl_vec<AudioPortConfig>{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<PARAM_PORT_NAME>(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<DeviceConfigParameter>& getInputDevicePcmOnlyConfigParameters() {
static const std::vector<DeviceConfigParameter> parameters = [] {
auto allParams = getInputDeviceConfigParameters();
std::vector<DeviceConfigParameter> pcmParams;
std::copy_if(
allParams.begin(), allParams.end(), std::back_inserter(pcmParams), [](auto cfg) {
const auto& flags = std::get<PARAM_FLAGS>(cfg);
return xsd::isLinearPcm(std::get<PARAM_CONFIG>(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<PARAM_DEVICE_NAME>(
std::get<PARAM_DEVICE>(cfg)),
std::get<PARAM_PORT_NAME>(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<AudioPortConfig>{source},
hidl_vec<AudioPortConfig>{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<PARAM_PORT_NAME>(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);

View File

@@ -110,7 +110,7 @@ std::vector<DeviceConfigParameter> 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<DeviceConfigParameter>& 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<DeviceConfigParameter>& 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<DeviceConfigParameter>& getOutputDeviceInvalidConfigParameters
config.offloadInfo.info(generateOffloadInfo(validBase));
}
std::vector<AudioInOutFlag> 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<DeviceConfigParameter> 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<DeviceConfigParameter>& 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<AudioInOutFlag> flags = {"random_string", ""};
result.emplace_back(device, config, flags);
result.emplace_back(device, mixPort.getName(), config, flags);
}
hasConfig = true;
break;

View File

@@ -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 <fcntl.h>
#include <unistd.h>
#include <algorithm>
#include <HidlUtils.h>
#include <system/audio.h>
#include <system/audio_config.h>
#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<DeviceAddress> 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<DeviceAddress>{};
}
std::optional<DeviceAddress> 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<DeviceAddress>{};
}
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<std::string>& attachedDevices,
const std::set<std::string>& possibleDevices) const {
for (const auto& device : attachedDevices) {
if (possibleDevices.count(device)) return device;
}
return {};
}
const std::vector<std::string>& PolicyConfig::getAttachedDevices(
const std::string& moduleName) const {
static const std::vector<std::string> empty;
auto module = getModuleFromName(moduleName);
if (module && module->getFirstAttachedDevices()) {
return module->getFirstAttachedDevices()->getItem();
}
return empty;
}
std::optional<DeviceAddress> 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<DeviceAddress>{};
}
std::set<std::string> PolicyConfig::getSinkDevicesForMixPort(const std::string& moduleName,
const std::string& mixPortName) const {
std::set<std::string> 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<std::string> PolicyConfig::getSourceDevicesForMixPort(
const std::string& moduleName, const std::string& mixPortName) const {
std::set<std::string> 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());
}
}
}
}
}
}

View File

@@ -16,15 +16,12 @@
#pragma once
#include <fcntl.h>
#include <unistd.h>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include <system/audio_config.h>
#include <utils/Errors.h>
// clang-format off
@@ -35,12 +32,6 @@
#include <android_audio_policy_configuration_V7_0-enums.h>
#include <android_audio_policy_configuration_V7_0.h>
#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<std::string>& 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<DeviceAddress> getSinkDeviceForMixPort(const std::string& moduleName,
const std::string& mixPortName) const;
std::optional<DeviceAddress> 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<std::string>& attachedDevices,
const std::set<std::string>& possibleDevices) const;
const std::vector<std::string>& getAttachedDevices(const std::string& moduleName) const;
std::optional<DeviceAddress> getDeviceAddressOfDevicePort(
const std::string& moduleName, const std::string& devicePortName) const;
std::string getDevicePortTagNameFromType(const std::string& moduleName,
const AudioDevice& deviceType) const;
std::set<std::string> getSinkDevicesForMixPort(const std::string& moduleName,
const std::string& mixPortName) const;
std::set<std::string> getSourceDevicesForMixPort(const std::string& moduleName,
const std::string& mixPortName) const;
void init();
const std::string mConfigFileName;
const std::string mFilePath;
std::optional<xsd::AudioPolicyConfiguration> mConfig;
status_t mStatus = NO_INIT;
android::status_t mStatus = android::NO_INIT;
const xsd::Module* mPrimaryModule;
std::set<std::string> mModulesWithDevicesNames;
};

View File

@@ -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",

View File

@@ -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 Stream>
class StreamHelper {
@@ -787,16 +798,13 @@ class StreamHelper {
template <class Open>
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<Stream>& 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<StreamWriter> {
public:
StreamWriter(IStreamOut* stream, size_t bufferSize)
: mStream(stream), mBufferSize(bufferSize), mData(mBufferSize) {}
~StreamWriter() {
stop();
if (mEfGroup) {
EventFlag::deleteEventFlag(&mEfGroup);
}
}
typedef MessageQueue<WriteCommand, ::android::hardware::kSynchronizedReadWrite> CommandMQ;
typedef MessageQueue<uint8_t, ::android::hardware::kSynchronizedReadWrite> DataMQ;
typedef MessageQueue<WriteStatus, ::android::hardware::kSynchronizedReadWrite> StatusMQ;
bool workerInit() {
std::unique_ptr<CommandMQ> tempCommandMQ;
std::unique_ptr<DataMQ> tempDataMQ;
std::unique_ptr<StatusMQ> tempStatusMQ;
Result retval;
Return<void> 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<uint32_t>(MessageQueueFlagBits::NOT_EMPTY));
uint32_t efState = 0;
retry:
status_t ret =
mEfGroup->wait(static_cast<uint32_t>(MessageQueueFlagBits::NOT_FULL), &efState);
if (efState & static_cast<uint32_t>(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<uint8_t> mData;
std::unique_ptr<CommandMQ> mCommandMQ;
std::unique_ptr<DataMQ> mDataMQ;
std::unique_ptr<StatusMQ> mStatusMQ;
EventFlag* mEfGroup = nullptr;
};
class OutputStreamTest : public OpenStreamTest<IStreamOut> {
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<StreamReader> {
public:
StreamReader(IStreamIn* stream, size_t bufferSize)
: mStream(stream), mBufferSize(bufferSize), mData(mBufferSize) {}
~StreamReader() {
stop();
if (mEfGroup) {
EventFlag::deleteEventFlag(&mEfGroup);
}
}
typedef MessageQueue<ReadParameters, ::android::hardware::kSynchronizedReadWrite> CommandMQ;
typedef MessageQueue<uint8_t, ::android::hardware::kSynchronizedReadWrite> DataMQ;
typedef MessageQueue<ReadStatus, ::android::hardware::kSynchronizedReadWrite> StatusMQ;
bool workerInit() {
std::unique_ptr<CommandMQ> tempCommandMQ;
std::unique_ptr<DataMQ> tempDataMQ;
std::unique_ptr<StatusMQ> tempStatusMQ;
Result retval;
Return<void> 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(&params)) {
ALOGE("command message queue write failed");
return false;
}
mEfGroup->wake(static_cast<uint32_t>(MessageQueueFlagBits::NOT_FULL));
uint32_t efState = 0;
bool success = true;
retry:
status_t ret =
mEfGroup->wait(static_cast<uint32_t>(MessageQueueFlagBits::NOT_EMPTY), &efState);
if (efState & static_cast<uint32_t>(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<uint8_t> mData;
std::unique_ptr<CommandMQ> mCommandMQ;
std::unique_ptr<DataMQ> mDataMQ;
std::unique_ptr<StatusMQ> mStatusMQ;
EventFlag* mEfGroup = nullptr;
};
class InputStreamTest : public OpenStreamTest<IStreamIn> {
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);
}
//////////////////////////////////////////////////////////////////////////////

View File

@@ -31,15 +31,17 @@ using DeviceParameter = std::tuple<std::string, std::string>;
// 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<DeviceParameter, android::hardware::audio::common::CPP_VERSION::AudioConfig,
std::variant<android::hardware::audio::common::CPP_VERSION::AudioInputFlag,
android::hardware::audio::common::CPP_VERSION::AudioOutputFlag>>;
#elif MAJOR_VERSION >= 7
enum { PARAM_DEVICE, PARAM_PORT_NAME, PARAM_CONFIG, PARAM_FLAGS };
using DeviceConfigParameter =
std::tuple<DeviceParameter, android::hardware::audio::common::CPP_VERSION::AudioConfig,
std::tuple<DeviceParameter, std::string,
android::hardware::audio::common::CPP_VERSION::AudioConfig,
std::vector<android::hardware::audio::CPP_VERSION::AudioInOutFlag>>;
#endif

View File

@@ -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 <sched.h>
#include <condition_variable>
#include <mutex>
#include <thread>
template <typename Impl>
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<std::mutex> 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<std::mutex> lock(mWorkerLock);
return mWorkerState == WorkerState::ERROR;
}
void stop() {
{
std::lock_guard<std::mutex> 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<std::mutex> 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<Impl*>(this)->workerInit();
{
std::lock_guard<std::mutex> 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<Impl*>(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<std::mutex> 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<std::mutex> 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);
};

View File

@@ -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 <sched.h>
#include <unistd.h>
#include <atomic>
#include <gtest/gtest.h>
#define LOG_TAG "StreamWorker_Test"
#include <log/log.h>
struct TestStream {
std::atomic<bool> error = false;
};
class TestWorker : public StreamWorker<TestWorker> {
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<size_t> mWorkerCycles = 0;
};
// The parameter specifies whether an extra call to 'stop' is made at the end.
class StreamWorkerInvalidTest : public testing::TestWithParam<bool> {
public:
StreamWorkerInvalidTest() : StreamWorkerInvalidTest(nullptr) {}
void TearDown() override {
if (GetParam()) {
worker.stop();
}
}
protected:
StreamWorkerInvalidTest(TestStream* stream) : testing::TestWithParam<bool>(), 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());