chromium/chromeos/ash/services/ime/ime_service_unittest.cc

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

#include "chromeos/ash/services/ime/ime_service.h"

#include "ash/constants/ash_features.h"
#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "chromeos/ash/services/ime/ime_shared_library_wrapper.h"
#include "chromeos/ash/services/ime/mock_input_channel.h"
#include "chromeos/ash/services/ime/public/mojom/input_engine.mojom.h"
#include "chromeos/ash/services/ime/public/mojom/input_method.mojom.h"
#include "chromeos/ash/services/ime/public/mojom/input_method_host.mojom.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using testing::_;

namespace ash {
namespace ime {

namespace {

const char kInvalidImeSpec[] = "ime_spec_never_support";
constexpr char kValidImeSpec[] = "valid_spec";
const std::vector<uint8_t> extra{0x66, 0x77, 0x88};

void ConnectCallback(bool* success, bool result) {
  *success = result;
}

class TestDecoderState;

// The fake decoder state has to be available globally because
// ImeSharedLibraryWrapper::EntryPoints is a list of stateless C functions, so
// the only way to have a stateful fake is to have a global reference to it.
TestDecoderState* g_test_decoder_state = nullptr;

mojo::ScopedMessagePipeHandle MessagePipeHandleFromInt(uint32_t handle) {
  return mojo::ScopedMessagePipeHandle(mojo::MessagePipeHandle(handle));
}

class TestDecoderState : public mojom::ConnectionFactory {
 public:
  bool InitializeConnectionFactory(uint32_t receiver_pipe_handle) {
    connection_factory_.reset();
    connection_factory_.Bind(mojo::PendingReceiver<mojom::ConnectionFactory>(
        MessagePipeHandleFromInt(receiver_pipe_handle)));
    connection_factory_.set_disconnect_handler(
        base::BindOnce(&mojo::Receiver<mojom::ConnectionFactory>::reset,
                       base::Unretained(&connection_factory_)));
    return true;
  }

  bool IsConnected() { return connection_factory_.is_bound(); }

  // mojom::ConnectionFactory overrides
  void ConnectToInputMethod(
      const std::string& ime_spec,
      mojo::PendingAssociatedReceiver<ime::mojom::InputMethod> input_method,
      mojo::PendingAssociatedRemote<ime::mojom::InputMethodHost>
          input_method_host,
      mojom::InputMethodSettingsPtr settings,
      ConnectToInputMethodCallback callback) override {
    std::move(callback).Run(/*bound=*/false);
  }
  void Unused(
      mojo::PendingAssociatedReceiver<ime::mojom::JpUnused> japanese_decoder,
      UnusedCallback callback) override {
    std::move(callback).Run(/*bound=*/false);
  }

 private:
  mojo::Receiver<ime::mojom::ConnectionFactory> connection_factory_{this};
};

class TestImeSharedLibraryWrapper : public ImeSharedLibraryWrapper {
 public:
  static TestImeSharedLibraryWrapper* GetInstance() {
    static base::NoDestructor<TestImeSharedLibraryWrapper> instance;
    return instance.get();
  }

  std::optional<ImeSharedLibraryWrapper::EntryPoints>
  MaybeLoadThenReturnEntryPoints() override {
    return entry_points_;
  }

  void ResetState() {
    delete g_test_decoder_state;
    g_test_decoder_state = new TestDecoderState();

    entry_points_ = {
        .init_proto_mode = [](ImeCrosPlatform* platform) {},
        .close_proto_mode = []() {},
        .proto_mode_supports =
            [](const char* ime_spec) {
              return strcmp(kInvalidImeSpec, ime_spec) != 0;
            },
        .proto_mode_activate_ime =
            [](const char* ime_spec, ImeClientDelegate* delegate) {
              return true;
            },
        .proto_mode_process = [](const uint8_t* data, size_t size) {},
        .init_mojo_mode = [](ImeCrosPlatform* platform) {},
        .close_mojo_mode = []() {},
        .mojo_mode_initialize_connection_factory =
            [](uint32_t receiver_pipe_handle) {
              return g_test_decoder_state->InitializeConnectionFactory(
                  receiver_pipe_handle);
            },
        .mojo_mode_is_input_method_connected =
            []() { return g_test_decoder_state->IsConnected(); },
    };
  }

 private:
  friend class base::NoDestructor<TestImeSharedLibraryWrapper>;

  explicit TestImeSharedLibraryWrapper() { ResetState(); }

  ~TestImeSharedLibraryWrapper() override = default;

  std::optional<ImeSharedLibraryWrapper::EntryPoints> entry_points_;
};

struct MockInputMethodHost : public mojom::InputMethodHost {
  void CommitText(const std::u16string& text,
                  mojom::CommitTextCursorBehavior cursor_behavior) override {
    last_commit = text;
  }
  void DEPRECATED_SetComposition(
      const std::u16string& text,
      std::vector<mojom::CompositionSpanPtr> spans) override {
    last_composition = text;
  }
  void SetComposition(const std::u16string& text,
                      std::vector<mojom::CompositionSpanPtr> spans,
                      uint32_t new_cursor_position) override {
    last_composition = text;
  }
  void SetCompositionRange(uint32_t start_index, uint32_t end_index) override {}
  void FinishComposition() override {}
  void DeleteSurroundingText(uint32_t num_before_cursor,
                             uint32_t num_after_cursor) override {}
  void ReplaceSurroundingText(uint32_t num_before_cursor,
                              uint32_t num_after_cursor,
                              const std::u16string& text) override {}
  void HandleAutocorrect(mojom::AutocorrectSpanPtr autocorrect_span) override {}
  void RequestSuggestions(mojom::SuggestionsRequestPtr request,
                          RequestSuggestionsCallback callback) override {}
  void DisplaySuggestions(
      const std::vector<AssistiveSuggestion>& suggestions,
      const std::optional<SuggestionsTextContext>& context) override {}
  void UpdateCandidatesWindow(mojom::CandidatesWindowPtr window) override {}
  void RecordUkm(mojom::UkmEntryPtr entry) override {}
  void DEPRECATED_ReportKoreanAction(mojom::KoreanAction action) override {}
  void DEPRECATED_ReportKoreanSettings(
      mojom::KoreanSettingsPtr settings) override {}
  void DEPRECATED_ReportSuggestionOpportunity(
      AssistiveSuggestionMode mode) override {}
  void ReportHistogramSample(base::Histogram* histogram,
                             uint16_t value) override {}
  void UpdateQuickSettings(
      mojom::InputMethodQuickSettingsPtr settings) override {}

  std::u16string last_commit;
  std::u16string last_composition;
};

class TestFieldTrialParamsRetriever : public FieldTrialParamsRetriever {
 public:
  explicit TestFieldTrialParamsRetriever() = default;
  ~TestFieldTrialParamsRetriever() override = default;
  TestFieldTrialParamsRetriever(const TestFieldTrialParamsRetriever&) = delete;
  TestFieldTrialParamsRetriever& operator=(
      const TestFieldTrialParamsRetriever&) = delete;

  std::string GetFieldTrialParamValueByFeature(
      const base::Feature& feature,
      const std::string& param_name) override {
    return base::StrCat({feature.name, "::", param_name});
  }
};

class ImeServiceTest : public testing::Test, public mojom::InputMethodHost {
 public:
  ImeServiceTest() = default;

  ImeServiceTest(const ImeServiceTest&) = delete;
  ImeServiceTest& operator=(const ImeServiceTest&) = delete;

  ~ImeServiceTest() override = default;

  void CommitText(const std::u16string& text,
                  mojom::CommitTextCursorBehavior cursor_behavior) override {}
  void DEPRECATED_SetComposition(
      const std::u16string& text,
      std::vector<mojom::CompositionSpanPtr> spans) override {}
  void SetComposition(const std::u16string& text,
                      std::vector<mojom::CompositionSpanPtr> spans,
                      uint32_t new_cursor_position) override {}
  void SetCompositionRange(uint32_t start_index, uint32_t end_index) override {}
  void FinishComposition() override {}
  void DeleteSurroundingText(uint32_t num_before_cursor,
                             uint32_t num_after_cursor) override {}
  void ReplaceSurroundingText(uint32_t num_before_cursor,
                              uint32_t num_after_cursor,
                              const std::u16string& text) override {}
  void HandleAutocorrect(mojom::AutocorrectSpanPtr autocorrect_span) override {}
  void RequestSuggestions(mojom::SuggestionsRequestPtr request,
                          RequestSuggestionsCallback callback) override {}
  void DisplaySuggestions(
      const std::vector<AssistiveSuggestion>& suggestions,
      const std::optional<SuggestionsTextContext>& context) override {}
  void UpdateCandidatesWindow(mojom::CandidatesWindowPtr window) override {}
  void RecordUkm(mojom::UkmEntryPtr entry) override {}
  void DEPRECATED_ReportKoreanAction(mojom::KoreanAction action) override {}
  void DEPRECATED_ReportKoreanSettings(
      mojom::KoreanSettingsPtr settings) override {}
  void DEPRECATED_ReportSuggestionOpportunity(
      AssistiveSuggestionMode mode) override {}
  void ReportHistogramSample(base::Histogram* histogram,
                             uint16_t value) override {}
  void UpdateQuickSettings(
      mojom::InputMethodQuickSettingsPtr settings) override {}

 protected:
  void SetUp() override {
    service_ = std::make_unique<ImeService>(
        remote_service_.BindNewPipeAndPassReceiver(),
        TestImeSharedLibraryWrapper::GetInstance(),
        std::make_unique<TestFieldTrialParamsRetriever>());
    remote_service_->BindInputEngineManager(
        remote_manager_.BindNewPipeAndPassReceiver());
  }

  void TearDown() override {
    service_.reset();
    TestImeSharedLibraryWrapper::GetInstance()->ResetState();
  }

  mojo::Remote<mojom::ImeService> remote_service_;
  mojo::Remote<mojom::InputEngineManager> remote_manager_;

 protected:
  std::unique_ptr<ImeService> service_;

 private:
  base::test::TaskEnvironment task_environment_;
};

}  // namespace

// Tests that the service is instantiated and it will return false when
// activating an IME engine with an invalid IME spec.
TEST_F(ImeServiceTest, ConnectInvalidImeEngineDoesNotConnectRemote) {
  bool success = true;
  MockInputChannel test_channel;
  mojo::Remote<mojom::InputChannel> remote_engine;

  remote_manager_->ConnectToImeEngine(
      kInvalidImeSpec, remote_engine.BindNewPipeAndPassReceiver(),
      test_channel.CreatePendingRemote(), extra,
      base::BindOnce(&ConnectCallback, &success));
  remote_manager_.FlushForTesting();

  EXPECT_FALSE(success);
  EXPECT_FALSE(remote_engine.is_connected());
}

TEST_F(ImeServiceTest, ConnectToValidEngineConnectsRemote) {
  bool success = true;
  MockInputChannel test_channel;
  mojo::Remote<mojom::InputChannel> remote_engine;

  remote_manager_->ConnectToImeEngine(
      kValidImeSpec, remote_engine.BindNewPipeAndPassReceiver(),
      test_channel.CreatePendingRemote(), {},
      base::BindOnce(&ConnectCallback, &success));
  remote_manager_.FlushForTesting();

  EXPECT_TRUE(success);
  EXPECT_TRUE(remote_engine.is_connected());
}

TEST_F(ImeServiceTest, ConnectToImeEngineWillOverrideExistingImeEngine) {
  bool success1, success2 = true;
  MockInputChannel test_channel1, test_channel2;
  mojo::Remote<mojom::InputChannel> remote_engine1, remote_engine2;

  remote_manager_->ConnectToImeEngine(
      kValidImeSpec, remote_engine1.BindNewPipeAndPassReceiver(),
      test_channel1.CreatePendingRemote(), /*extra=*/{},
      base::BindOnce(&ConnectCallback, &success1));
  remote_manager_->ConnectToImeEngine(
      kValidImeSpec, remote_engine2.BindNewPipeAndPassReceiver(),
      test_channel2.CreatePendingRemote(), /*extra=*/{},
      base::BindOnce(&ConnectCallback, &success2));
  remote_manager_.FlushForTesting();

  EXPECT_TRUE(success1);
  EXPECT_TRUE(success2);
  EXPECT_FALSE(remote_engine1.is_connected());
  EXPECT_TRUE(remote_engine2.is_connected());
}

TEST_F(ImeServiceTest,
       ConnectToImeEngineCannotConnectIfConnectionFactoryIsConnected) {
  bool success1, success2 = true;
  MockInputChannel test_channel;
  mojo::Remote<mojom::ConnectionFactory> connection_factory;
  mojo::Remote<mojom::InputChannel> remote_engine;

  remote_manager_->InitializeConnectionFactory(
      connection_factory.BindNewPipeAndPassReceiver(),
      base::BindOnce(&ConnectCallback, &success1));
  remote_manager_->ConnectToImeEngine(
      kValidImeSpec, remote_engine.BindNewPipeAndPassReceiver(),
      test_channel.CreatePendingRemote(), /*extra=*/{},
      base::BindOnce(&ConnectCallback, &success2));
  remote_manager_.FlushForTesting();

  // The second connection should have failed.
  EXPECT_TRUE(success1);
  EXPECT_FALSE(success2);
  EXPECT_TRUE(connection_factory.is_connected());
  EXPECT_FALSE(remote_engine.is_connected());
}

TEST_F(ImeServiceTest,
       ConnectToImeEngineCanConnectIfConnectionFactoryIsDisconnected) {
  bool success1, success2 = true;
  MockInputChannel test_channel;
  mojo::Remote<mojom::ConnectionFactory> connection_factory;
  mojo::Remote<mojom::InputChannel> remote_engine;

  remote_manager_->InitializeConnectionFactory(
      connection_factory.BindNewPipeAndPassReceiver(),
      base::BindOnce(&ConnectCallback, &success1));
  connection_factory.reset();
  remote_manager_->ConnectToImeEngine(
      kValidImeSpec, remote_engine.BindNewPipeAndPassReceiver(),
      test_channel.CreatePendingRemote(), /*extra=*/{},
      base::BindOnce(&ConnectCallback, &success2));
  remote_manager_.FlushForTesting();

  // The second connection should have succeed since the first connection was
  // disconnected.
  EXPECT_TRUE(success1);
  EXPECT_TRUE(success2);
  EXPECT_FALSE(connection_factory.is_bound());
  EXPECT_TRUE(remote_engine.is_connected());
}

TEST_F(ImeServiceTest, InitializeConnectionFactoryCanOverrideAnyConnection) {
  bool success1, success2, success3 = true;
  MockInputChannel test_channel;
  mojo::Remote<mojom::ConnectionFactory> connection_factory1,
      connection_factory2;
  mojo::Remote<mojom::InputChannel> remote_engine;

  remote_manager_->ConnectToImeEngine(
      kValidImeSpec, remote_engine.BindNewPipeAndPassReceiver(),
      test_channel.CreatePendingRemote(), /*extra=*/{},
      base::BindOnce(&ConnectCallback, &success1));
  remote_manager_->InitializeConnectionFactory(
      connection_factory1.BindNewPipeAndPassReceiver(),
      base::BindOnce(&ConnectCallback, &success2));
  remote_manager_->InitializeConnectionFactory(
      connection_factory2.BindNewPipeAndPassReceiver(),
      base::BindOnce(&ConnectCallback, &success3));
  remote_manager_.FlushForTesting();

  EXPECT_TRUE(success1);
  EXPECT_TRUE(success2);
  EXPECT_TRUE(success3);
  EXPECT_FALSE(remote_engine.is_connected());
  EXPECT_FALSE(connection_factory1.is_connected());
  EXPECT_TRUE(connection_factory2.is_connected());
}

TEST_F(ImeServiceTest, GetFieldTrialParamValueByFeatureNonConsidered) {
  const char* value = service_->GetFieldTrialParamValueByFeature(
      "non-considered-feature", "param-name");

  EXPECT_STREQ(value, "");
  delete[] value;
}

TEST_F(ImeServiceTest, GetFieldTrialParamValueByFeatureConsidered) {
  const char* value = service_->GetFieldTrialParamValueByFeature(
      "AutocorrectParamsTuning", "param-name");

  EXPECT_STREQ(value, "AutocorrectParamsTuning::param-name");
  delete[] value;
}

}  // namespace ime
}  // namespace ash