mirror of
https://github.com/Evolution-X/hardware_interfaces
synced 2026-02-01 16:50:18 +00:00
Merge "audio: Add VTS tests for reads and writes" am: bbc243fd44
Original change: https://android-review.googlesource.com/c/platform/hardware/interfaces/+/1642746 Change-Id: I239c891186594d2e958f5e4c821d4242e458dfdf
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(¶ms));
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
215
audio/core/all-versions/vts/functional/7.0/PolicyConfig.cpp
Normal file
215
audio/core/all-versions/vts/functional/7.0/PolicyConfig.cpp
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(¶ms)) {
|
||||
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);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
|
||||
146
audio/core/all-versions/vts/functional/StreamWorker.h
Normal file
146
audio/core/all-versions/vts/functional/StreamWorker.h
Normal 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);
|
||||
};
|
||||
@@ -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());
|
||||
Reference in New Issue
Block a user