chromium/chromeos/ash/components/language_packs/language_packs_util_unittest.cc

// Copyright 2023 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/components/language_packs/language_packs_util.h"

#include <optional>
#include <string>

#include "ash/constants/ash_pref_names.h"
#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice.pb.h"
#include "chromeos/ash/components/language_packs/language_pack_manager.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/cros_system_api/dbus/dlcservice/dbus-constants.h"

namespace ash::language_packs {

using ::dlcservice::DlcState;
using ::dlcservice::DlcState_State_INSTALLED;
using ::dlcservice::DlcState_State_INSTALLING;
using ::dlcservice::DlcState_State_NOT_INSTALLED;
using ::testing::IsEmpty;
using ::testing::UnorderedElementsAre;

TEST(LanguagePacksUtil, ConvertDlcState_EmptyInput) {
  DlcState input;
  PackResult output = ConvertDlcStateToPackResult(input);

  // The default value in the input is 'NOT_INSTALLED'.
  EXPECT_EQ(output.pack_state, PackResult::StatusCode::kNotInstalled);
  EXPECT_EQ(output.operation_error, PackResult::ErrorCode::kNone);
}

TEST(LanguagePacksUtil, ConvertDlcState_NotInstalled) {
  DlcState input;
  input.set_state(DlcState_State_NOT_INSTALLED);
  PackResult output = ConvertDlcStateToPackResult(input);

  EXPECT_EQ(output.pack_state, PackResult::StatusCode::kNotInstalled);
  EXPECT_EQ(output.operation_error, PackResult::ErrorCode::kNone);

  // Even if the path is set (by mistake) in the input, we should not return it.
  input.set_root_path("/var/somepath");
  output = ConvertDlcStateToPackResult(input);

  EXPECT_EQ(output.pack_state, PackResult::StatusCode::kNotInstalled);
  EXPECT_TRUE(output.path.empty());
}

TEST(LanguagePacksUtil, ConvertDlcState_Installing) {
  DlcState input;
  input.set_state(DlcState_State_INSTALLING);
  PackResult output = ConvertDlcStateToPackResult(input);

  EXPECT_EQ(output.pack_state, PackResult::StatusCode::kInProgress);
  EXPECT_EQ(output.operation_error, PackResult::ErrorCode::kNone);

  // Even if the path is set (by mistake) in the input, we should not return it.
  input.set_root_path("/var/somepath");
  output = ConvertDlcStateToPackResult(input);

  EXPECT_EQ(output.pack_state, PackResult::StatusCode::kInProgress);
  EXPECT_TRUE(output.path.empty());
}

TEST(LanguagePacksUtil, ConvertDlcState_Installed) {
  DlcState input;
  input.set_state(DlcState_State_INSTALLED);
  input.set_root_path("/var/somepath");
  PackResult output = ConvertDlcStateToPackResult(input);

  EXPECT_EQ(output.pack_state, PackResult::StatusCode::kInstalled);
  EXPECT_EQ(output.operation_error, PackResult::ErrorCode::kNone);
  EXPECT_EQ(output.path, "/var/somepath");
}

// Tests the behaviour in case the state received from the input in not a valid
// value. This could happen for example if the proto changes without notice.
TEST(LanguagePacksUtil, ConvertDlcState_MalformedProto) {
  DlcState input;
  // Enum value '3' is beyond currently defined values.
  input.set_state(static_cast<dlcservice::DlcState_State>(3));
  input.set_root_path("/var/somepath");
  PackResult output = ConvertDlcStateToPackResult(input);

  EXPECT_EQ(output.pack_state, PackResult::StatusCode::kUnknown);
  EXPECT_EQ(output.operation_error, PackResult::ErrorCode::kNone);
  EXPECT_TRUE(output.path.empty());
}

TEST(LanguagePacksUtil, ConvertDlcState_ErrorSet) {
  DlcState input;
  input.set_last_error_code(dlcservice::kErrorNeedReboot);
  PackResult output = ConvertDlcStateToPackResult(input);

  EXPECT_EQ(output.pack_state, PackResult::StatusCode::kNotInstalled);
  EXPECT_EQ(output.operation_error, PackResult::ErrorCode::kNeedReboot);
  EXPECT_TRUE(output.path.empty());
}

TEST(LanguagePacksUtil, ConvertDlcInstallResult_Success) {
  DlcserviceClient::InstallResult input;
  input.error = "";
  input.root_path = "/var/somepath";
  PackResult output = ConvertDlcInstallResultToPackResult(input);

  EXPECT_EQ(output.pack_state, PackResult::StatusCode::kInstalled);
  EXPECT_EQ(output.operation_error, PackResult::ErrorCode::kNone);
  EXPECT_EQ(output.path, "/var/somepath");
}

TEST(LanguagePacksUtil, ConvertDlcInstallResult_Error) {
  DlcserviceClient::InstallResult input;
  input.error = dlcservice::kErrorInternal;
  PackResult output = ConvertDlcInstallResultToPackResult(input);

  EXPECT_EQ(output.pack_state, PackResult::StatusCode::kUnknown);
  EXPECT_EQ(output.operation_error, PackResult::ErrorCode::kOther);
  EXPECT_TRUE(output.path.empty());
}

// Tests the conversion of all error types returned by DlcserviceClient.
TEST(LanguagePacksUtil, ConvertDlcError_AllErrorsTypes) {
  EXPECT_EQ(ConvertDlcErrorToErrorCode(""), PackResult::ErrorCode::kNone);
  EXPECT_EQ(ConvertDlcErrorToErrorCode(dlcservice::kErrorNone),
            PackResult::ErrorCode::kNone);
  EXPECT_EQ(ConvertDlcErrorToErrorCode(dlcservice::kErrorAllocation),
            PackResult::ErrorCode::kAllocation);
  EXPECT_EQ(ConvertDlcErrorToErrorCode(dlcservice::kErrorInvalidDlc),
            PackResult::ErrorCode::kWrongId);
  EXPECT_EQ(ConvertDlcErrorToErrorCode(dlcservice::kErrorNeedReboot),
            PackResult::ErrorCode::kNeedReboot);
  EXPECT_EQ(ConvertDlcErrorToErrorCode(dlcservice::kErrorNoImageFound),
            PackResult::ErrorCode::kOther);
  EXPECT_EQ(ConvertDlcErrorToErrorCode(dlcservice::kErrorInternal),
            PackResult::ErrorCode::kOther);
}

// For Handwriting we only keep the language part, not the country/region.
TEST(LanguagePacksUtil, ResolveLocaleHandwriting) {
  EXPECT_EQ(ResolveLocale(kHandwritingFeatureId, "en-US"), "en");
  EXPECT_EQ(ResolveLocale(kHandwritingFeatureId, "en-us"), "en");
  EXPECT_EQ(ResolveLocale(kHandwritingFeatureId, "fr"), "fr");
  EXPECT_EQ(ResolveLocale(kHandwritingFeatureId, "it-IT"), "it");
  EXPECT_EQ(ResolveLocale(kHandwritingFeatureId, "zh"), "zh");
  EXPECT_EQ(ResolveLocale(kHandwritingFeatureId, "zh-TW"), "zh");

  // Chinese HongKong is an exception.
  EXPECT_EQ(ResolveLocale(kHandwritingFeatureId, "zh-HK"), "zh-HK");
}

TEST(LanguagePacksUtil, ResolveLocaleTts) {
  // For these locales we keep the region.
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "en-AU"), "en-au");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "en-au"), "en-au");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "en-GB"), "en-gb");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "en-gb"), "en-gb");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "en-US"), "en-us");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "en-us"), "en-us");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "es-ES"), "es-es");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "es-es"), "es-es");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "es-US"), "es-us");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "es-us"), "es-us");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "pt-BR"), "pt-br");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "pt-br"), "pt-br");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "pt-PT"), "pt-pt");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "pt-pt"), "pt-pt");

  // For all other locales we only keep the language.
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "bn-bd"), "bn");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "fil-ph"), "fil");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "it-it"), "it");
  EXPECT_EQ(ResolveLocale(kTtsFeatureId, "ja-jp"), "ja");
}

TEST(LanguagePacksUtil, ResolveLocaleFonts) {
  // Language pack resolution is handled by the client.
  EXPECT_EQ(ResolveLocale(kFontsFeatureId, "ja"), "ja");
  EXPECT_EQ(ResolveLocale(kFontsFeatureId, "ko"), "ko");
}

TEST(LanguagePacksUtil, MapThenFilterStringsNoInput) {
  EXPECT_THAT(MapThenFilterStrings(
                  {}, base::BindRepeating(
                          [](const std::string&) -> std::optional<std::string> {
                            return "ignored";
                          })),
              IsEmpty());
}

TEST(LanguagePacksUtil, MapThenFilterStringsAllToNullopt) {
  EXPECT_THAT(MapThenFilterStrings(
                  {{"en", "de"}},
                  base::BindRepeating(
                      [](const std::string&) -> std::optional<std::string> {
                        return std::nullopt;
                      })),
              IsEmpty());
}

TEST(LanguagePacksUtil, MapThenFilterStringsAllToUniqueStrings) {
  EXPECT_THAT(
      MapThenFilterStrings(
          {{"en", "de"}},
          base::BindRepeating(
              [](const std::string& input) -> std::optional<std::string> {
                return input;
              })),
      UnorderedElementsAre("en", "de"));
}

TEST(LanguagePacksUtil, MapThenFilterStringsRepeatedString) {
  EXPECT_THAT(
      MapThenFilterStrings(
          {{"repeat", "unique", "repeat"}},
          base::BindRepeating(
              [](const std::string& input) -> std::optional<std::string> {
                return input;
              })),
      UnorderedElementsAre("repeat", "unique"));
}

TEST(LanguagePacksUtil, MapThenFilterStringsSomeNullopt) {
  EXPECT_THAT(
      MapThenFilterStrings(
          {{"pass_1", "fail", "pass_2"}},
          base::BindRepeating(
              [](const std::string& input) -> std::optional<std::string> {
                return (input == "fail") ? std::nullopt
                                         : std::optional<std::string>(input);
              })),
      UnorderedElementsAre("pass_1", "pass_2"));
}

TEST(LanguagePacksUtil, MapThenFilterStringsDeduplicateOutput) {
  EXPECT_THAT(
      MapThenFilterStrings(
          {{"a", "dedup-1", "dedup-2"}},
          base::BindRepeating(
              [](const std::string& input) -> std::optional<std::string> {
                return (input.length() < 2) ? input : "dedup";
              })),
      UnorderedElementsAre("a", "dedup"));
}

TEST(LanguagePacksUtil, MapThenFilterStringsDisjointSet) {
  EXPECT_THAT(
      MapThenFilterStrings(
          {{"a", "b", "d"}},
          base::BindRepeating(
              [](const std::string& input) -> std::optional<std::string> {
                return "something else";
              })),
      UnorderedElementsAre("something else"));
}

TEST(LanguagePacksUtil, ExtractInputMethodsFromPrefsEmpty) {
  auto pref = std::make_unique<TestingPrefServiceSimple>();
  pref->registry()->RegisterStringPref(prefs::kLanguagePreloadEngines,
                                       std::string());

  EXPECT_THAT(ExtractInputMethodsFromPrefs(pref.get()), IsEmpty());
}

TEST(LanguagePacksUtil, ExtractInputMethodsFromPrefsOne) {
  auto pref = std::make_unique<TestingPrefServiceSimple>();
  pref->registry()->RegisterStringPref(prefs::kLanguagePreloadEngines,
                                       "xkb:it::ita");

  EXPECT_THAT(ExtractInputMethodsFromPrefs(pref.get()),
              UnorderedElementsAre("xkb:it::ita"));
}

TEST(LanguagePacksUtil, ExtractInputMethodsFromPrefsMultiple) {
  auto pref = std::make_unique<TestingPrefServiceSimple>();
  pref->registry()->RegisterStringPref(prefs::kLanguagePreloadEngines,
                                       "xkb:it::ita,xkb:fr::fra,xkb:de::ger");

  EXPECT_THAT(
      ExtractInputMethodsFromPrefs(pref.get()),
      UnorderedElementsAre("xkb:it::ita", "xkb:fr::fra", "xkb:de::ger"));
}

}  // namespace ash::language_packs