chromium/chromecast/media/cma/backend/alsa/scoped_alsa_mixer_unittest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromecast/media/cma/backend/alsa/scoped_alsa_mixer.h"

#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/test/mock_log.h"
#include "base/test/task_environment.h"
#include "media/audio/alsa/mock_alsa_wrapper.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace chromecast {
namespace media {
namespace {
using ::testing::_;
using ::testing::DoAll;
using ::testing::HasSubstr;
using ::testing::Return;
using ::testing::SetArgPointee;
using ::testing::StrEq;

constexpr char kMixerDeviceName[] = "test-device";
constexpr char kMixerElementName[] = "test-element";

const int kSuccess = 0;
const int kFailure = -1;

int MixerEventCallback(snd_mixer_elem_t*, unsigned int) {
  return 0;
}

class ScopedAlsaMixerEventTest : public ::testing::Test {
 public:
  ScopedAlsaMixerEventTest()
      : task_environment_(base::test::TaskEnvironment::MainThreadType::IO) {}

  void SetUp() override {
    ASSERT_EQ(0, pipe(pipe_fds_));

    EXPECT_CALL(alsa_, MixerOpen(_, 0))
        .WillOnce(DoAll(SetArgPointee<0>(mixer_), Return(kSuccess)));
    EXPECT_CALL(alsa_, MixerAttach(mixer_, StrEq(kMixerDeviceName)))
        .WillOnce(Return(kSuccess));
    EXPECT_CALL(alsa_, MixerElementRegister(mixer_, nullptr, nullptr))
        .WillOnce(Return(kSuccess));
    EXPECT_CALL(alsa_, MixerLoad(mixer_)).WillOnce(Return(kSuccess));
    EXPECT_CALL(alsa_, MixerSelemIdMalloc(_))
        .WillOnce(DoAll(SetArgPointee<0>(mixer_selem_id_), Return(kSuccess)));
    EXPECT_CALL(alsa_, MixerSelemIdSetIndex(mixer_selem_id_, 0));
    EXPECT_CALL(alsa_,
                MixerSelemIdSetName(mixer_selem_id_, StrEq(kMixerElementName)));
    EXPECT_CALL(alsa_, MixerFindSelem(mixer_, mixer_selem_id_))
        .WillOnce(Return(element_));
    EXPECT_CALL(alsa_, MixerSelemIdFree(mixer_selem_id_));

    alsa_mixer_ = std::make_unique<ScopedAlsaMixer>(&alsa_, kMixerDeviceName,
                                                    kMixerElementName);
  }

  void TearDown() override {
    EXPECT_CALL(alsa_, MixerElemSetCallback(_, nullptr));
    EXPECT_CALL(alsa_, MixerElemSetCallbackPrivate(_, nullptr));
    EXPECT_CALL(alsa_, MixerClose(mixer_));
    alsa_mixer_.reset();
    EXPECT_EQ(0, IGNORE_EINTR(close(pipe_fds_[0])));
    EXPECT_EQ(0, IGNORE_EINTR(close(pipe_fds_[1])));
  }

  void ReadByte() {
    char buffer;
    ASSERT_TRUE(base::ReadFromFD(pipe_fds_[0], base::span_from_ref(buffer)));
  }

  void WriteByte() {
    constexpr char kByte = '!';
    ASSERT_TRUE(
        base::WriteFileDescriptor(pipe_fds_[1], byte_span_from_ref(kByte)));
  }

  base::test::TaskEnvironment task_environment_;
  int pipe_fds_[2];

  ::testing::StrictMock<::media::MockAlsaWrapper> alsa_;
  snd_mixer_t* mixer_ = reinterpret_cast<snd_mixer_t*>(0x00001111);
  snd_mixer_selem_id_t* mixer_selem_id_ =
      reinterpret_cast<snd_mixer_selem_id_t*>(0x00002222);
  snd_mixer_elem_t* element_ = reinterpret_cast<snd_mixer_elem_t*>(0x00003333);
  void* cb_private_data_ = reinterpret_cast<void*>(0x00004444);

  std::unique_ptr<ScopedAlsaMixer> alsa_mixer_ = nullptr;
};

}  // namespace

TEST(ScopedAlsaMixerTest, NormalLifeCycle) {
  ::testing::StrictMock<::media::MockAlsaWrapper> alsa;
  snd_mixer_t* mixer = reinterpret_cast<snd_mixer_t*>(0x00001111);
  snd_mixer_selem_id_t* mixer_selem_id =
      reinterpret_cast<snd_mixer_selem_id_t*>(0x00002222);
  snd_mixer_elem_t* element = reinterpret_cast<snd_mixer_elem_t*>(0x00003333);

  EXPECT_CALL(alsa, MixerOpen(_, 0))
      .WillOnce(DoAll(SetArgPointee<0>(mixer), Return(kSuccess)));
  EXPECT_CALL(alsa, MixerAttach(mixer, StrEq(kMixerDeviceName)))
      .WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerElementRegister(mixer, nullptr, nullptr))
      .WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerLoad(mixer)).WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerSelemIdMalloc(_))
      .WillOnce(DoAll(SetArgPointee<0>(mixer_selem_id), Return(kSuccess)));
  EXPECT_CALL(alsa, MixerSelemIdSetIndex(mixer_selem_id, 0));
  EXPECT_CALL(alsa,
              MixerSelemIdSetName(mixer_selem_id, StrEq(kMixerElementName)));
  EXPECT_CALL(alsa, MixerFindSelem(mixer, mixer_selem_id))
      .WillOnce(Return(element));
  EXPECT_CALL(alsa, MixerSelemIdFree(mixer_selem_id));

  ScopedAlsaMixer alsa_mixer(&alsa, kMixerDeviceName, kMixerElementName);
  EXPECT_EQ(alsa_mixer.GetMixerForTest(), mixer);
  EXPECT_EQ(alsa_mixer.element, element);

  EXPECT_CALL(alsa, MixerElemSetCallback(element, nullptr));
  EXPECT_CALL(alsa, MixerElemSetCallbackPrivate(element, nullptr));
  EXPECT_CALL(alsa, MixerClose(mixer));
}

TEST(ScopedAlsaMixerTest, RefreshElement) {
  ::testing::NiceMock<::media::MockAlsaWrapper> alsa;
  snd_mixer_t* mixer = reinterpret_cast<snd_mixer_t*>(0x00001111);
  snd_mixer_selem_id_t* mixer_selem_id =
      reinterpret_cast<snd_mixer_selem_id_t*>(0x00002222);
  snd_mixer_elem_t* element = reinterpret_cast<snd_mixer_elem_t*>(0x00003333);
  ScopedAlsaMixer alsa_mixer(&alsa, kMixerDeviceName, kMixerElementName);

  EXPECT_CALL(alsa, MixerOpen(_, 0))
      .WillOnce(DoAll(SetArgPointee<0>(mixer), Return(kSuccess)));
  EXPECT_CALL(alsa, MixerAttach(mixer, StrEq(kMixerDeviceName)))
      .WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerElementRegister(mixer, nullptr, nullptr))
      .WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerLoad(mixer)).WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerSelemIdMalloc(_))
      .WillOnce(DoAll(SetArgPointee<0>(mixer_selem_id), Return(kSuccess)));
  EXPECT_CALL(alsa, MixerSelemIdSetIndex(mixer_selem_id, 0));
  EXPECT_CALL(alsa,
              MixerSelemIdSetName(mixer_selem_id, StrEq(kMixerElementName)));
  EXPECT_CALL(alsa, MixerFindSelem(mixer, mixer_selem_id))
      .WillOnce(Return(element));
  EXPECT_CALL(alsa, MixerSelemIdFree(mixer_selem_id));

  alsa_mixer.RefreshElement();

  EXPECT_CALL(alsa, MixerClose(mixer));
}

TEST(ScopedAlsaMixerTest, MixerOpenFailure) {
  ::testing::StrictMock<::media::MockAlsaWrapper> alsa;
  snd_mixer_t* mixer = reinterpret_cast<snd_mixer_t*>(0x00001111);
  base::test::MockLog mock_log;

  EXPECT_CALL(alsa, MixerOpen(_, 0))
      .WillOnce(DoAll(SetArgPointee<0>(mixer), Return(kFailure)));
  EXPECT_CALL(mock_log, Log(logging::LOGGING_INFO,
                            ::testing::EndsWith("/scoped_alsa_mixer.cc"),
                            /*line=*/_,
                            /*message_start=*/_, /*str=*/_));
  EXPECT_CALL(alsa, StrError(kFailure)).WillOnce(Return(""));
  EXPECT_CALL(
      mock_log,
      Log(logging::LOGGING_ERROR, ::testing::EndsWith("/scoped_alsa_mixer.cc"),
          /*line=*/_,
          /*message_start=*/_, /*str=*/HasSubstr("MixerOpen error")));

  mock_log.StartCapturingLogs();
  ScopedAlsaMixer alsa_mixer(&alsa, kMixerDeviceName, kMixerElementName);
  EXPECT_EQ(alsa_mixer.GetMixerForTest(), nullptr);
  EXPECT_EQ(alsa_mixer.element, nullptr);
  mock_log.StopCapturingLogs();
}

TEST(ScopedAlsaMixerTest, MixerAttachFailure) {
  ::testing::StrictMock<::media::MockAlsaWrapper> alsa;
  snd_mixer_t* mixer = reinterpret_cast<snd_mixer_t*>(0x00001111);
  base::test::MockLog mock_log;

  EXPECT_CALL(alsa, MixerOpen(_, 0))
      .WillOnce(DoAll(SetArgPointee<0>(mixer), Return(kSuccess)));
  EXPECT_CALL(alsa, MixerAttach(mixer, StrEq(kMixerDeviceName)))
      .WillOnce(Return(kFailure));
  EXPECT_CALL(mock_log, Log(logging::LOGGING_INFO,
                            ::testing::EndsWith("/scoped_alsa_mixer.cc"),
                            /*line=*/_,
                            /*message_start=*/_, /*str=*/_));
  EXPECT_CALL(alsa, StrError(kFailure)).WillOnce(Return(""));
  EXPECT_CALL(
      mock_log,
      Log(logging::LOGGING_ERROR, ::testing::EndsWith("/scoped_alsa_mixer.cc"),
          /*line=*/_,
          /*message_start=*/_, HasSubstr("MixerAttach error")));
  EXPECT_CALL(alsa, MixerClose(mixer));

  mock_log.StartCapturingLogs();
  ScopedAlsaMixer alsa_mixer(&alsa, kMixerDeviceName, kMixerElementName);
  EXPECT_EQ(alsa_mixer.GetMixerForTest(), nullptr);
  EXPECT_EQ(alsa_mixer.element, nullptr);
  mock_log.StopCapturingLogs();
}

TEST(ScopedAlsaMixerTest, MixerLoadFailure) {
  ::testing::StrictMock<::media::MockAlsaWrapper> alsa;
  snd_mixer_t* mixer = reinterpret_cast<snd_mixer_t*>(0x00001111);
  base::test::MockLog mock_log;

  EXPECT_CALL(alsa, MixerOpen(_, 0))
      .WillOnce(DoAll(SetArgPointee<0>(mixer), Return(kSuccess)));
  EXPECT_CALL(alsa, MixerAttach(mixer, StrEq(kMixerDeviceName)))
      .WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerElementRegister(mixer, nullptr, nullptr))
      .WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerLoad(mixer)).WillOnce(Return(kFailure));
  EXPECT_CALL(mock_log, Log(logging::LOGGING_INFO,
                            ::testing::EndsWith("/scoped_alsa_mixer.cc"),
                            /*line=*/_,
                            /*message_start=*/_, /*str=*/_));
  EXPECT_CALL(alsa, StrError(kFailure)).WillOnce(Return(""));
  EXPECT_CALL(mock_log, Log(logging::LOGGING_ERROR,
                            ::testing::EndsWith("/scoped_alsa_mixer.cc"),
                            /*line=*/_,
                            /*message_start=*/_, HasSubstr("MixerLoad error")));
  EXPECT_CALL(alsa, MixerClose(mixer));

  mock_log.StartCapturingLogs();
  ScopedAlsaMixer alsa_mixer(&alsa, kMixerDeviceName, kMixerElementName);
  EXPECT_EQ(alsa_mixer.GetMixerForTest(), nullptr);
  EXPECT_EQ(alsa_mixer.element, nullptr);
  mock_log.StopCapturingLogs();
}

TEST(ScopedAlsaMixerTest, MixerFindSelemFailure) {
  ::testing::StrictMock<::media::MockAlsaWrapper> alsa;
  snd_mixer_t* mixer = reinterpret_cast<snd_mixer_t*>(0x00001111);
  snd_mixer_selem_id_t* mixer_selem_id =
      reinterpret_cast<snd_mixer_selem_id_t*>(0x00002222);
  base::test::MockLog mock_log;

  EXPECT_CALL(alsa, MixerOpen(_, 0))
      .WillOnce(DoAll(SetArgPointee<0>(mixer), Return(kSuccess)));
  EXPECT_CALL(alsa, MixerAttach(mixer, StrEq(kMixerDeviceName)))
      .WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerElementRegister(mixer, nullptr, nullptr))
      .WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerLoad(mixer)).WillOnce(Return(kSuccess));
  EXPECT_CALL(alsa, MixerSelemIdMalloc(_))
      .WillOnce(DoAll(SetArgPointee<0>(mixer_selem_id), Return(kSuccess)));
  EXPECT_CALL(alsa, MixerSelemIdSetIndex(mixer_selem_id, 0));
  EXPECT_CALL(alsa,
              MixerSelemIdSetName(mixer_selem_id, StrEq(kMixerElementName)));
  EXPECT_CALL(alsa, MixerFindSelem(mixer, mixer_selem_id))
      .WillOnce(Return(nullptr));
  EXPECT_CALL(mock_log, Log(logging::LOGGING_INFO,
                            ::testing::EndsWith("/scoped_alsa_mixer.cc"),
                            /*line=*/_,
                            /*message_start=*/_, /*str=*/_));
  EXPECT_CALL(mock_log, Log(logging::LOGGING_ERROR,
                            ::testing::EndsWith("/scoped_alsa_mixer.cc"),
                            /*line=*/_,
                            /*message_start=*/_, /*str=*/_));
  EXPECT_CALL(alsa, MixerSelemIdFree(mixer_selem_id));

  mock_log.StartCapturingLogs();
  ScopedAlsaMixer alsa_mixer(&alsa, kMixerDeviceName, kMixerElementName);
  EXPECT_EQ(alsa_mixer.GetMixerForTest(), mixer);
  EXPECT_EQ(alsa_mixer.element, nullptr);
  mock_log.StopCapturingLogs();

  EXPECT_CALL(alsa, MixerClose(mixer));
}

TEST(ScopedAlsaMixerDeathTest, MixerElementRegisterFailure) {
  EXPECT_DEATH(
      {
        ::testing::StrictMock<::media::MockAlsaWrapper> alsa;
        snd_mixer_t* mixer = reinterpret_cast<snd_mixer_t*>(0x00001111);
        base::test::MockLog mock_log;

        EXPECT_CALL(alsa, MixerOpen(_, 0))
            .WillOnce(DoAll(SetArgPointee<0>(mixer), Return(kSuccess)));
        EXPECT_CALL(alsa, MixerAttach(mixer, StrEq(kMixerDeviceName)))
            .WillOnce(Return(kSuccess));
        EXPECT_CALL(alsa, MixerElementRegister(mixer, nullptr, nullptr))
            .WillOnce(Return(kFailure));
        EXPECT_CALL(mock_log, Log(logging::LOGGING_INFO,
                                  ::testing::EndsWith("/scoped_alsa_mixer.cc"),
                                  /*line=*/_,
                                  /*message_start=*/_, /*str=*/_));
        EXPECT_CALL(alsa, StrError(kFailure)).WillOnce(Return(""));
        EXPECT_CALL(mock_log, Log(logging::LOGGING_FATAL,
                                  ::testing::EndsWith("/scoped_alsa_mixer.cc"),
                                  /*line=*/_,
                                  /*message_start=*/_,
                                  HasSubstr("MixerElementRegister error")));

        mock_log.StartCapturingLogs();
        ScopedAlsaMixer alsa_mixer(&alsa, kMixerDeviceName, kMixerElementName);
        mock_log.StopCapturingLogs();
      },
      _);
}

TEST(ScopedAlsaMixerDeathTest, MixerSelemIdMallocFailure) {
  EXPECT_DEATH(
      {
        ::testing::StrictMock<::media::MockAlsaWrapper> alsa;
        snd_mixer_t* mixer = reinterpret_cast<snd_mixer_t*>(0x00001111);
        snd_mixer_selem_id_t* mixer_selem_id =
            reinterpret_cast<snd_mixer_selem_id_t*>(0x00002222);
        base::test::MockLog mock_log;

        EXPECT_CALL(alsa, MixerOpen(_, 0))
            .WillOnce(DoAll(SetArgPointee<0>(mixer), Return(kSuccess)));
        EXPECT_CALL(alsa, MixerAttach(mixer, StrEq(kMixerDeviceName)))
            .WillOnce(Return(kSuccess));
        EXPECT_CALL(alsa, MixerElementRegister(mixer, nullptr, nullptr))
            .WillOnce(Return(kSuccess));
        EXPECT_CALL(alsa, MixerLoad(mixer)).WillOnce(Return(kSuccess));
        EXPECT_CALL(alsa, MixerSelemIdMalloc(_))
            .WillOnce(
                DoAll(SetArgPointee<0>(mixer_selem_id), Return(kFailure)));
        EXPECT_CALL(mock_log, Log(logging::LOGGING_INFO,
                                  ::testing::EndsWith("/scoped_alsa_mixer.cc"),
                                  /*line=*/_,
                                  /*message_start=*/_, /*str=*/_));
        EXPECT_CALL(alsa, StrError(kFailure)).WillOnce(Return(""));
        EXPECT_CALL(mock_log, Log(logging::LOGGING_FATAL,
                                  ::testing::EndsWith("/scoped_alsa_mixer.cc"),
                                  /*line=*/_,
                                  /*message_start=*/_,
                                  HasSubstr("MixerSelemIdMalloc error")));

        mock_log.StartCapturingLogs();
        ScopedAlsaMixer alsa_mixer(&alsa, kMixerDeviceName, kMixerElementName);
        mock_log.StopCapturingLogs();
      },
      _);
}

TEST_F(ScopedAlsaMixerEventTest, NullableCallback) {
  EXPECT_CALL(alsa_, MixerElemSetCallback(_, _)).Times(0);
  EXPECT_CALL(alsa_, MixerElemSetCallbackPrivate(_, _)).Times(0);
}

TEST_F(ScopedAlsaMixerEventTest, RealCallback) {
  base::RunLoop run_loop;

  EXPECT_CALL(alsa_, MixerElemSetCallback(element_, &MixerEventCallback));
  EXPECT_CALL(alsa_, MixerElemSetCallbackPrivate(element_, cb_private_data_));

  EXPECT_CALL(alsa_, MixerPollDescriptorsCount(mixer_)).WillOnce(Return(1));
  EXPECT_CALL(alsa_, MixerPollDescriptors(mixer_, _, 1))
      .WillOnce(testing::Invoke(
          [this](snd_mixer_t* mixer_, struct pollfd* pfds, unsigned int space) {
            for (unsigned int i = 0; i < space; ++i) {
              pfds[i].fd = pipe_fds_[0];
            }
            return space;
          }));

  EXPECT_EQ(alsa_mixer_->element, element_);
  alsa_mixer_->WatchForEvents(&MixerEventCallback, cb_private_data_);

  EXPECT_CALL(alsa_, MixerHandleEvents(mixer_))
      .WillOnce(testing::Invoke([this, &run_loop]() {
        ReadByte();
        run_loop.Quit();
        return 0;
      }));
  WriteByte();

  run_loop.Run();
}

}  // namespace media
}  // namespace chromecast