chromium/chromeos/ash/services/assistant/platform/audio_input_host_unittest.cc

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

#include <optional>

#include "base/run_loop.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"
#include "chromeos/ash/services/assistant/platform/audio_input_host_impl.h"
#include "chromeos/ash/services/assistant/public/cpp/features.h"
#include "chromeos/ash/services/libassistant/public/mojom/audio_input_controller.mojom.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::assistant {

namespace {

using LidState = chromeos::PowerManagerClient::LidState;
using MojomLidState = libassistant::mojom::LidState;
using MojomAudioInputController = libassistant::mojom::AudioInputController;
using ::testing::_;
using ::testing::NiceMock;

class AudioInputControllerMock : public MojomAudioInputController {
 public:
  AudioInputControllerMock() = default;
  AudioInputControllerMock(const AudioInputControllerMock&) = delete;
  AudioInputControllerMock& operator=(const AudioInputControllerMock&) = delete;
  ~AudioInputControllerMock() override = default;

  mojo::PendingRemote<MojomAudioInputController> BindNewPipeAndPassRemote() {
    receiver_.reset();
    return receiver_.BindNewPipeAndPassRemote();
  }

  MOCK_METHOD(void, SetMicOpen, (bool mic_open));
  MOCK_METHOD(void, SetHotwordEnabled, (bool enable));
  MOCK_METHOD(void, SetDeviceId, (const std::optional<std::string>& device_id));
  MOCK_METHOD(void,
              SetHotwordDeviceId,
              (const std::optional<std::string>& device_id));
  MOCK_METHOD(void, SetLidState, (MojomLidState new_state));
  MOCK_METHOD(void, OnConversationTurnStarted, ());

 private:
  mojo::Receiver<MojomAudioInputController> receiver_{this};
};

class AssistantAudioInputHostTest : public testing::Test {
 public:
  AssistantAudioInputHostTest() {
    chromeos::PowerManagerClient::InitializeFake();
  }

  AssistantAudioInputHostTest(const AssistantAudioInputHostTest&) = delete;
  AssistantAudioInputHostTest& operator=(const AssistantAudioInputHostTest&) =
      delete;
  ~AssistantAudioInputHostTest() override {
    // |audio_input_host_| uses the fake power manager client, so must be
    // destroyed before the power manager client.
    audio_input_host_.reset();
    chromeos::PowerManagerClient::Shutdown();
  }

  void SetUp() override {
    // Enable DSP Hotword
    scoped_feature_list_.InitAndEnableFeature(features::kEnableDspHotword);

    CreateNewAudioInputHost();
  }

  AudioInputControllerMock& mojom_audio_input_controller() {
    return audio_input_controller_;
  }

  AudioInputHostImpl& audio_input_host() {
    CHECK(audio_input_host_);
    return *audio_input_host_;
  }

  void CreateNewAudioInputHost() {
    audio_input_host_ = std::make_unique<AudioInputHostImpl>(
        audio_input_controller_.BindNewPipeAndPassRemote(),
        &cras_audio_handler_.Get(), chromeos::FakePowerManagerClient::Get(),
        "default-locale");

    FlushPendingMojomCalls();
  }

  void DestroyAudioInputHost() { audio_input_host_ = nullptr; }

  void ReportLidEvent(LidState state) {
    chromeos::FakePowerManagerClient::Get()->SetLidState(
        state, base::TimeTicks::UnixEpoch());
    FlushPendingMojomCalls();
  }

  void SetLidState(LidState state) { ReportLidEvent(state); }

  void SetDeviceId(const std::optional<std::string>& device_id) {
    audio_input_host().SetDeviceId(device_id);
    FlushPendingMojomCalls();
  }

  void SetHotwordDeviceId(const std::optional<std::string>& device_id) {
    audio_input_host().SetHotwordDeviceId(device_id);
    FlushPendingMojomCalls();
  }

  void OnHotwordEnabled(bool enabled) {
    audio_input_host().OnHotwordEnabled(enabled);
    FlushPendingMojomCalls();
  }

  void SetMicState(bool mic_open) {
    audio_input_host().SetMicState(mic_open);
    FlushPendingMojomCalls();
  }

  void OnConversationTurnStarted() {
    audio_input_host().OnConversationTurnStarted();
    FlushPendingMojomCalls();
  }

  void FlushPendingMojomCalls() { base::RunLoop().RunUntilIdle(); }

 private:
  base::test::TaskEnvironment task_environment_;
  base::test::ScopedFeatureList scoped_feature_list_;
  ScopedCrasAudioHandlerForTesting cras_audio_handler_;
  NiceMock<AudioInputControllerMock> audio_input_controller_;
  std::unique_ptr<AudioInputHostImpl> audio_input_host_;
};

}  // namespace

TEST_F(AssistantAudioInputHostTest, ShouldSendLidOpenEventsToMojom) {
  EXPECT_CALL(mojom_audio_input_controller(),
              SetLidState(MojomLidState::kOpen));
  ReportLidEvent(LidState::OPEN);
}

TEST_F(AssistantAudioInputHostTest, ShouldSendLidClosedEventsToMojom) {
  EXPECT_CALL(mojom_audio_input_controller(),
              SetLidState(MojomLidState::kClosed));
  ReportLidEvent(LidState::CLOSED);
}

TEST_F(AssistantAudioInputHostTest, ShouldSendLidNotPresentEventsToMojom) {
  // If there is no lid it can not be closed by the user so we consider it to be
  // open.
  EXPECT_CALL(mojom_audio_input_controller(),
              SetLidState(MojomLidState::kOpen));
  ReportLidEvent(LidState::NOT_PRESENT);
}

TEST_F(AssistantAudioInputHostTest, ShouldReadCurrentLidStateWhenLaunching) {
  DestroyAudioInputHost();
  SetLidState(LidState::OPEN);
  EXPECT_CALL(mojom_audio_input_controller(),
              SetLidState(MojomLidState::kOpen));
  CreateNewAudioInputHost();

  DestroyAudioInputHost();
  SetLidState(LidState::CLOSED);
  EXPECT_CALL(mojom_audio_input_controller(),
              SetLidState(MojomLidState::kClosed));
  CreateNewAudioInputHost();
}

TEST_F(AssistantAudioInputHostTest, ShouldSendDeviceIdToMojom) {
  EXPECT_CALL(mojom_audio_input_controller(),
              SetDeviceId(std::optional<std::string>("device-id")));
  SetDeviceId("device-id");
}

TEST_F(AssistantAudioInputHostTest, ShouldUnsetDeviceIdWhenItsEmpty) {
  // Note this variable is required as directly passing std::nullopt into the
  // EXPECT_CALL doesn't compile.
  const std::optional<std::string> expected = std::nullopt;
  EXPECT_CALL(mojom_audio_input_controller(), SetDeviceId(expected));

  SetDeviceId(std::nullopt);
}

TEST_F(AssistantAudioInputHostTest, ShouldSendHotwordDeviceIdToMojom) {
  EXPECT_CALL(
      mojom_audio_input_controller(),
      SetHotwordDeviceId(std::optional<std::string>("hotword-device-id")));
  SetHotwordDeviceId("hotword-device-id");
}

TEST_F(AssistantAudioInputHostTest, ShouldUnsetHotwordDeviceIdWhenItsEmpty) {
  // Note this variable is required as directly passing std::nullopt into the
  // EXPECT_CALL doesn't compile.
  const std::optional<std::string> expected = std::nullopt;
  EXPECT_CALL(mojom_audio_input_controller(), SetHotwordDeviceId(expected));

  SetHotwordDeviceId(std::nullopt);
}
TEST_F(AssistantAudioInputHostTest, ShouldSendHotwordEnabledToMojom) {
  EXPECT_CALL(mojom_audio_input_controller(), SetHotwordEnabled(true));
  OnHotwordEnabled(true);

  EXPECT_CALL(mojom_audio_input_controller(), SetHotwordEnabled(false));
  OnHotwordEnabled(false);
}

TEST_F(AssistantAudioInputHostTest, ShouldSendMicOpenToMojom) {
  EXPECT_CALL(mojom_audio_input_controller(), SetMicOpen(true));
  SetMicState(/*mic_open=*/true);

  EXPECT_CALL(mojom_audio_input_controller(), SetMicOpen(false));
  SetMicState(/*mic_open=*/false);
}

TEST_F(AssistantAudioInputHostTest,
       ShouldSendOnConversationTurnStartedToMojom) {
  EXPECT_CALL(mojom_audio_input_controller(), OnConversationTurnStarted);
  OnConversationTurnStarted();
}

}  // namespace ash::assistant