chromium/chrome/browser/ash/app_list/search/files/file_result_unittest.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ash/app_list/search/files/file_result.h"

#include <optional>

#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/image_util.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_list/search/common/icon_constants.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/file_manager/volume_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/thumbnail_loader/thumbnail_loader.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/disks/fake_disk_mount_manager.h"
#include "chromeos/ash/components/string_matching/tokenized_string.h"
#include "chromeos/ui/base/file_icon_util.h"
#include "content/public/test/browser_task_environment.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/color/color_provider_manager.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/gfx/skia_util.h"

namespace app_list::test {

namespace {

using ::ash::string_matching::TokenizedString;
using ::testing::DoubleNear;

constexpr uint8_t kJpegData[] = {
    0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xc0, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
    0xff, 0xc4, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xc4,
    0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xda, 0x00, 0x08,
    0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, 0x37, 0xff, 0xd9};
constexpr int kJpegDataSize = sizeof(kJpegData);

class TestThumbnailLoader : public ash::ThumbnailLoader {
 public:
  explicit TestThumbnailLoader(const base::FilePath& expected_path)
      : ash::ThumbnailLoader(nullptr), expected_path_(expected_path) {}

  TestThumbnailLoader(const TestThumbnailLoader&) = delete;
  TestThumbnailLoader& operator=(const TestThumbnailLoader&) = delete;
  ~TestThumbnailLoader() override = default;

  // ash::ThumbnailLoader:
  void Load(const ThumbnailRequest& request, ImageCallback callback) override {
    EXPECT_EQ(gfx::Size(kThumbnailDimension, kThumbnailDimension),
              request.size);
    EXPECT_EQ(expected_path_, request.file_path);
    ASSERT_FALSE(pending_callback_);
    pending_callback_ = std::move(callback);
  }

  void RespondToPendingRequest(const SkBitmap* bitmap,
                               base::File::Error error) {
    ASSERT_TRUE(pending_callback_);
    std::move(pending_callback_).Run(bitmap, error);
  }

  bool HasPendingRequest() const { return !!pending_callback_; }

 private:
  const base::FilePath expected_path_;
  ImageCallback pending_callback_;
};

}  // namespace

class FileResultTest : public testing::Test {
 public:
  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());

    TestingProfile::Builder profile_builder;
    profile_ = profile_builder.Build();

    // The VolumeManager is null in tests by default. Explicitly create one here
    // to ensure file_manager::util::GetDisplayablePath() which is used for
    // image search works.
    file_manager::VolumeManagerFactory::GetInstance()->SetTestingFactory(
        profile_.get(),
        base::BindLambdaForTesting([&](content::BrowserContext* context) {
          return std::unique_ptr<KeyedService>(
              std::make_unique<file_manager::VolumeManager>(
                  Profile::FromBrowserContext(context), nullptr, nullptr,
                  &disk_mount_manager_, nullptr,
                  file_manager::VolumeManager::GetMtpStorageInfoCallback()));
        }));
  }

  void Wait() { task_environment_.RunUntilIdle(); }

  content::BrowserTaskEnvironment task_environment_;
  base::ScopedTempDir temp_dir_;
  ash::disks::FakeDiskMountManager disk_mount_manager_;
  std::unique_ptr<Profile> profile_;
};

TEST_F(FileResultTest, CheckMetadata) {
  const base::FilePath path("/my/test/MIXED_case_FILE.Pdf");
  FileResult result(
      /*id=*/"zero_state_file://" + path.value(), path, u"some details",
      ash::AppListSearchResultType::kZeroStateFile,
      ash::SearchResultDisplayType::kList, 0.2f, std::u16string(),
      FileResult::Type::kFile, profile_.get(), nullptr);
  EXPECT_EQ(base::UTF16ToUTF8(result.title()),
            std::string("MIXED_case_FILE.Pdf"));
  EXPECT_EQ(result.details(), u"some details");
  EXPECT_EQ(result.id(), "zero_state_file:///my/test/MIXED_case_FILE.Pdf");
  EXPECT_EQ(result.result_type(), ash::AppListSearchResultType::kZeroStateFile);
  EXPECT_EQ(result.display_type(), ash::SearchResultDisplayType::kList);
  EXPECT_EQ(result.relevance(), 0.2f);
}

TEST_F(FileResultTest, HostedExtensionsIgnored) {
  const base::FilePath path1("my/Document.gdoc");
  FileResult result_1(
      /*id=*/"zero_state_file://" + path1.value(), path1, std::nullopt,
      ash::AppListSearchResultType::kZeroStateFile,
      ash::SearchResultDisplayType::kList, 0.2f, std::u16string(),
      FileResult::Type::kFile, profile_.get(), nullptr);
  const base::FilePath path2("my/Map.gmaps");
  FileResult result_2(
      /*id=*/"zero_state_file://" + path2.value(), path2, std::nullopt,
      ash::AppListSearchResultType::kZeroStateFile,
      ash::SearchResultDisplayType::kList, 0.2f, std::u16string(),
      FileResult::Type::kFile, profile_.get(), nullptr);

  EXPECT_EQ(base::UTF16ToUTF8(result_1.title()), std::string("Document"));
  EXPECT_EQ(base::UTF16ToUTF8(result_2.title()), std::string("Map"));
}

TEST_F(FileResultTest, PenalizeScore) {
  base::FilePath path(temp_dir_.GetPath().Append("somefile"));
  std::optional<TokenizedString> query;
  query.emplace(u"somefile");
  base::Time now = base::Time::Now();

  double expected_scores[] = {1.0, 0.9, 0.63};
  int access_days_ago[] = {0, 10, 30};

  for (int i = 0; i < 3; ++i) {
    base::Time last_accessed = now - base::Days(access_days_ago[i]);
    double relevance =
        FileResult::CalculateRelevance(query, path, last_accessed);
    EXPECT_THAT(relevance, DoubleNear(expected_scores[i], 0.01));
  }
}

TEST_F(FileResultTest, Icons) {
  struct TestCase {
    const std::string description;
    const std::string path;
    const FileResult::Type type;
    const chromeos::IconType expected_placeholder;
    base::File::Error thumbnail_load_result;
  } kTestCases[] = {
      {"file", "/my/test/mySheet.xlsx", FileResult::Type::kFile,
       chromeos::IconType::kExcel, base::File::FILE_ERROR_FAILED},
      {"file with  thumbnail", "/my/test/myPicture.jpeg",
       FileResult::Type::kFile, chromeos::IconType::kImage,
       base::File::FILE_OK},
      {"regular folder", "/my/test/Maps", FileResult::Type::kDirectory,
       chromeos::IconType::kFolder, base::File::FILE_ERROR_NOT_A_FILE},
      {"shared folder", "/my/test/shared/Maps",
       FileResult::Type::kSharedDirectory, chromeos::IconType::kFolderShared,
       base::File::FILE_ERROR_NOT_A_FILE}};

  for (const auto& test_case : kTestCases) {
    SCOPED_TRACE(test_case.description);

    TestThumbnailLoader thumbnail_loader(base::FilePath(test_case.path));
    FileResult result(
        /*id=*/"zero_state_file://" + test_case.path,
        base::FilePath(test_case.path), u"some details",
        ash::AppListSearchResultType::kZeroStateFile,
        ash::SearchResultDisplayType::kList, 0.2f, std::u16string(),
        test_case.type, profile_.get(), &thumbnail_loader);
    EXPECT_TRUE(result.chip_icon().isNull());
    EXPECT_EQ(result.icon().dimension, kThumbnailDimension);
    EXPECT_FALSE(thumbnail_loader.HasPendingRequest());

    const gfx::ImageSkia expected_placeholder =
        gfx::ImageSkiaOperations::CreateSuperimposedImage(
            ash::image_util::CreateEmptyImage(
                gfx::Size(kThumbnailDimension, kThumbnailDimension)),
            chromeos::GetIconFromType(test_case.expected_placeholder, true));
    EXPECT_TRUE(
        gfx::BitmapsAreEqual(*result.icon().icon.Rasterize(nullptr).bitmap(),
                             *expected_placeholder.bitmap()));

    EXPECT_TRUE(thumbnail_loader.HasPendingRequest());

    std::optional<const SkBitmap> test_thumbnail;
    if (test_case.thumbnail_load_result == base::File::FILE_OK) {
      test_thumbnail.emplace(gfx::test::CreateBitmap(
          kThumbnailDimension, kThumbnailDimension, SK_ColorRED));
    }

    thumbnail_loader.RespondToPendingRequest(
        test_thumbnail ? &test_thumbnail.value() : nullptr,
        test_case.thumbnail_load_result);

    if (test_thumbnail) {
      EXPECT_TRUE(gfx::BitmapsAreEqual(
          *result.icon().icon.Rasterize(nullptr).bitmap(), *test_thumbnail));
    } else {
      EXPECT_TRUE(
          gfx::BitmapsAreEqual(*result.icon().icon.Rasterize(nullptr).bitmap(),
                               *expected_placeholder.bitmap()));
    }
  }
}

TEST_F(FileResultTest, IconWithNullThumbnailLoader) {
  base::FilePath path("/my/test/file.jpeg");
  FileResult result(
      /*id=*/"zero_state_file://" + path.value(), path, u"some details",
      ash::AppListSearchResultType::kZeroStateFile,
      ash::SearchResultDisplayType::kList, 0.2f, std::u16string(),
      FileResult::Type::kFile, profile_.get(), nullptr);
  EXPECT_TRUE(result.chip_icon().isNull());
  EXPECT_EQ(result.icon().dimension, kThumbnailDimension);

  const gfx::ImageSkia expected_placeholder =
      gfx::ImageSkiaOperations::CreateSuperimposedImage(
          ash::image_util::CreateEmptyImage(
              gfx::Size(kThumbnailDimension, kThumbnailDimension)),
          chromeos::GetIconFromType(chromeos::IconType::kImage, true));
  EXPECT_TRUE(
      gfx::BitmapsAreEqual(*result.icon().icon.Rasterize(nullptr).bitmap(),
                           *expected_placeholder.bitmap()));
}

TEST_F(FileResultTest, FileMetadataPopulatedForDisplay) {
  // Register temp dir as local filesystem, for pretty file name metadata.
  auto* volume_manager = file_manager::VolumeManager::Get(profile_.get());
  base::FilePath local_directory(base::FilePath(temp_dir_.GetPath()));
  volume_manager->RegisterDownloadsDirectoryForTesting(local_directory);

  base::FilePath path(local_directory.Append("test.jpg"));
  ASSERT_TRUE(base::WriteFile(path, kJpegData));
  ASSERT_TRUE(base::TouchFile(path, base::Time::FromSecondsSinceUnixEpoch(1),
                              base::Time::FromSecondsSinceUnixEpoch(2)));
  FileResult result(
      /*id=*/"file://" + path.value(), path, std::nullopt,
      ash::AppListSearchResultType::kImageSearch,
      ash::SearchResultDisplayType::kImage, 0.2f, std::u16string(),
      FileResult::Type::kFile, profile_.get(), nullptr);

  base::File::Info info;
  base::RunLoop file_info_load_waiter;
  result.file_metadata_loader()->RequestFileInfo(base::BindLambdaForTesting(
      [&info, &file_info_load_waiter](base::File::Info returned_info) {
        info = returned_info;
        file_info_load_waiter.Quit();
      }));

  // The file info, when requested, gets loaded on a worker thread.
  // Wait for the file info request to get handled, and then run main loop to
  // make sure load response posted on the main thread runs.
  file_info_load_waiter.Run();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(info.size, kJpegDataSize);
  EXPECT_EQ(info.last_modified, base::Time::FromSecondsSinceUnixEpoch(2));
  EXPECT_EQ(result.displayable_file_path().value(), "My files/test.jpg");

  storage::ExternalMountPoints::GetSystemInstance()->RevokeAllFileSystems();
}

}  // namespace app_list::test