chromium/chrome/browser/ash/app_list/search/local_image_search/image_annotation_worker_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 "chrome/browser/ash/app_list/search/local_image_search/image_annotation_worker.h"

#include <memory>
#include <vector>

#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/metrics/field_trial_params.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_list/search/local_image_search/annotation_storage.h"
#include "chrome/browser/ash/app_list/search/local_image_search/local_image_search_test_util.h"
#include "chrome/browser/ash/app_list/search/search_features.h"
#include "chromeos/dbus/machine_learning/machine_learning_client.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace app_list {
namespace {

constexpr uint8_t kBad_image[] = {
    0x40,
    0x22,
    0x23,
    0x25,
};

constexpr uint8_t kJpeg_image[] = {0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10,
                                   0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
                                   0x01, 0x00, 0x00, 0x01};

constexpr uint8_t kPng_image[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A,
                                  0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
                                  0x49, 0x48, 0x44, 0x52};

constexpr uint8_t kWebp_image[] = {
    0x52, 0x49, 0x46, 0x46, 0x6c, 0x32, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
    0x56, 0x50, 0x38, 0x58, 0x0a, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00};

constexpr uint8_t kWebp_image1[] = {
    0x52, 0x49, 0x46, 0x46, 0x74, 0x69, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
    0x56, 0x50, 0x38, 0x20, 0x68, 0x69, 0x00, 0x00, 0x90, 0x35, 0x03, 0x9d};

constexpr uint8_t kWebp_animation[] = {
    0x52, 0x49, 0x46, 0x46, 0xa6, 0x2f, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
    0x56, 0x50, 0x38, 0x58, 0x0a, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00};

class ImageAnnotationWorkerTest : public testing::Test {
 protected:
  // testing::Test overrides:
  void SetUp() override {
    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

    test_directory_ = temp_dir.GetPath();
    std::vector<base::FilePath> excluded_paths = {
        test_directory_.AppendASCII("TrashBin")};
    annotation_worker_ = std::make_unique<ImageAnnotationWorker>(
        test_directory_, std::move(excluded_paths),
        /*profile=*/nullptr,
        /*use_file_watchers=*/false,
        /*use_ocr=*/false,
        /*use_ica=*/false);
    annotation_worker_->set_image_processing_delay_for_testing(
        base::Seconds(0));
    bar_image_path_ = test_directory_.AppendASCII("bar.jpg");
    const base::FilePath test_db = test_directory_.AppendASCII("test.db");
    storage_ =
        std::make_unique<AnnotationStorage>(std::move(test_db),
                                            /*annotation_worker=*/nullptr);
  }

  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  std::unique_ptr<ImageAnnotationWorker> annotation_worker_;
  std::unique_ptr<AnnotationStorage> storage_;
  base::FilePath test_directory_;
  base::FilePath bar_image_path_;
};

TEST_F(ImageAnnotationWorkerTest, MustProcessTheFolderAtInitTest) {
  storage_->Initialize();
  task_environment_.RunUntilIdle();

  base::CreateDirectory(test_directory_.AppendASCII("Images"));
  base::CreateDirectory(test_directory_.AppendASCII("TrashBin"));

  auto jpg_path = test_directory_.AppendASCII("bar.jpg");
  base::WriteFile(jpg_path, kJpeg_image);
  auto jpeg_path =
      test_directory_.AppendASCII("Images").AppendASCII("bar1.jpeg");
  base::WriteFile(jpeg_path, kJpeg_image);
  auto png_path = test_directory_.AppendASCII("bar2.png");
  base::WriteFile(png_path, kPng_image);
  auto jng_path = test_directory_.AppendASCII("bar3.jng");
  base::WriteFile(jng_path, kJpeg_image);
  auto tjng_path = test_directory_.AppendASCII("bar4.tjng");
  base::WriteFile(tjng_path, kJpeg_image);
  auto JPG_path = test_directory_.AppendASCII("bar5.JPG");
  base::WriteFile(JPG_path, kJpeg_image);
  auto webp_path = test_directory_.AppendASCII("bar6.webp");
  base::WriteFile(webp_path, kWebp_image);
  auto WEBP_path = test_directory_.AppendASCII("bar7.WEBP");
  base::WriteFile(WEBP_path, kWebp_image1);
  auto bin_path =
      test_directory_.AppendASCII("TrashBin").AppendASCII("bar8.jpg");
  base::WriteFile(bin_path, kJpeg_image);

  auto image_time = base::Time::Now();
  for (const auto& path : {jpg_path, jpeg_path, png_path, jng_path, tjng_path,
                           JPG_path, webp_path, WEBP_path, bin_path}) {
    base::TouchFile(path, image_time, image_time);
  }

  annotation_worker_->Initialize(storage_.get());
  task_environment_.FastForwardBy(base::Seconds(1));
  task_environment_.RunUntilIdle();

  ImageInfo jpg_image({"bar"}, jpg_path, image_time, /*file_size=*/16);
  ImageInfo jpeg_image({"bar1"}, jpeg_path, image_time, 16);
  ImageInfo png_image({"bar2"}, png_path, image_time, 16);
  ImageInfo JPG_image({"bar5"}, JPG_path, image_time, 16);
  ImageInfo webp_image({"bar6"}, webp_path, image_time, 24);
  ImageInfo WEBP_image({"bar7"}, WEBP_path, image_time, 24);

  auto annotations = storage_->GetAllAnnotationsForTest();
  EXPECT_THAT(annotations, testing::UnorderedElementsAreArray(
                               {jpg_image, jpeg_image, png_image, JPG_image,
                                webp_image, WEBP_image}));

  task_environment_.RunUntilIdle();
}

TEST_F(ImageAnnotationWorkerTest, ImageProcessingTimeout) {
  // Overwrite processing delay to be greater than `kMaxImageProcessingTime`. It
  // simulate the case when the dlc processing is slow and callback is return
  // after timeout.
  annotation_worker_->set_image_processing_delay_for_testing(base::Minutes(3));

  storage_->Initialize();
  task_environment_.RunUntilIdle();

  base::CreateDirectory(test_directory_.AppendASCII("Images"));
  base::CreateDirectory(test_directory_.AppendASCII("TrashBin"));

  auto jpg_path = test_directory_.AppendASCII("bar.jpg");
  base::WriteFile(jpg_path, kJpeg_image);
  auto jpeg_path =
      test_directory_.AppendASCII("Images").AppendASCII("bar1.jpeg");
  base::WriteFile(jpeg_path, kJpeg_image);
  auto png_path = test_directory_.AppendASCII("bar2.png");
  base::WriteFile(png_path, kPng_image);
  auto jng_path = test_directory_.AppendASCII("bar3.jng");
  base::WriteFile(jng_path, kJpeg_image);
  auto tjng_path = test_directory_.AppendASCII("bar4.tjng");
  base::WriteFile(tjng_path, kJpeg_image);
  auto JPG_path = test_directory_.AppendASCII("bar5.JPG");
  base::WriteFile(JPG_path, kJpeg_image);
  auto webp_path = test_directory_.AppendASCII("bar6.webp");
  base::WriteFile(webp_path, kWebp_image);
  auto WEBP_path = test_directory_.AppendASCII("bar7.WEBP");
  base::WriteFile(WEBP_path, kWebp_image1);
  auto bin_path =
      test_directory_.AppendASCII("TrashBin").AppendASCII("bar8.jpg");
  base::WriteFile(bin_path, kJpeg_image);

  auto image_time = base::Time::Now();
  for (const auto& path : {jpg_path, jpeg_path, png_path, jng_path, tjng_path,
                           JPG_path, webp_path, WEBP_path, bin_path}) {
    base::TouchFile(path, image_time, image_time);
  }

  annotation_worker_->Initialize(storage_.get());
  // We need `FastForwardUntilNoTasksRemain` here as `RunUntilIdle` excludes
  // delayed tasks.
  task_environment_.FastForwardUntilNoTasksRemain();

  ImageInfo jpg_image({"bar"}, jpg_path, image_time, /*file_size=*/16);
  ImageInfo jpeg_image({"bar1"}, jpeg_path, image_time, 16);
  ImageInfo png_image({"bar2"}, png_path, image_time, 16);
  ImageInfo JPG_image({"bar5"}, JPG_path, image_time, 16);
  ImageInfo webp_image({"bar6"}, webp_path, image_time, 24);
  ImageInfo WEBP_image({"bar7"}, WEBP_path, image_time, 24);

  auto annotations = storage_->GetAllAnnotationsForTest();
  EXPECT_THAT(annotations, testing::UnorderedElementsAreArray(
                               {jpg_image, jpeg_image, png_image, JPG_image,
                                webp_image, WEBP_image}));

  task_environment_.RunUntilIdle();
}

TEST_F(ImageAnnotationWorkerTest, MustIgnoreBadFiles) {
  storage_->Initialize();
  annotation_worker_->Initialize(storage_.get());
  task_environment_.RunUntilIdle();

  auto image_time = base::Time::Now();

  auto webp_animation_path = test_directory_.AppendASCII("bar.webp");
  base::WriteFile(webp_animation_path, kWebp_animation);

  auto bad_image_path = test_directory_.AppendASCII("bar.png");
  base::WriteFile(bad_image_path, kBad_image);

  auto jng_path = test_directory_.AppendASCII("bar.jng");
  base::WriteFile(jng_path, kJpeg_image);

  auto txt_path = test_directory_.AppendASCII("bar.txt");
  base::WriteFile(jng_path, kJpeg_image);

  for (const auto& path :
       {webp_animation_path, bad_image_path, jng_path, txt_path}) {
    base::TouchFile(path, image_time, image_time);
    annotation_worker_->TriggerOnFileChangeForTests(path,
                                                    /*error=*/false);
  }

  task_environment_.RunUntilIdle();

  EXPECT_TRUE(storage_->GetAllAnnotationsForTest().empty());
  task_environment_.RunUntilIdle();
}

TEST_F(ImageAnnotationWorkerTest, MustProcessOnNewFileTest) {
  storage_->Initialize();
  annotation_worker_->Initialize(storage_.get());
  task_environment_.RunUntilIdle();

  base::WriteFile(bar_image_path_, kJpeg_image);
  auto bar_image_time = base::Time::Now();
  base::TouchFile(bar_image_path_, bar_image_time, bar_image_time);

  annotation_worker_->TriggerOnFileChangeForTests(bar_image_path_,
                                                  /*error=*/false);
  task_environment_.RunUntilIdle();

  ImageInfo bar_image({"bar"}, bar_image_path_, bar_image_time,
                      /*file_size=*/16);

  EXPECT_THAT(storage_->GetAllAnnotationsForTest(),
              testing::ElementsAreArray({bar_image}));

  task_environment_.RunUntilIdle();
}

TEST_F(ImageAnnotationWorkerTest, MustUpdateOnFileUpdateTest) {
  storage_->Initialize();
  annotation_worker_->Initialize(storage_.get());
  task_environment_.RunUntilIdle();

  base::WriteFile(bar_image_path_, kJpeg_image);

  annotation_worker_->TriggerOnFileChangeForTests(bar_image_path_,
                                                  /*error=*/false);
  task_environment_.RunUntilIdle();

  base::WriteFile(bar_image_path_, kJpeg_image);
  auto bar_image_time_updated = base::Time::Now();
  base::TouchFile(bar_image_path_, bar_image_time_updated,
                  bar_image_time_updated);

  annotation_worker_->TriggerOnFileChangeForTests(bar_image_path_,
                                                  /*error=*/false);
  task_environment_.RunUntilIdle();

  ImageInfo bar_image_updated({"bar"}, bar_image_path_, bar_image_time_updated,
                              /*file_size=*/16);
  EXPECT_THAT(storage_->GetAllAnnotationsForTest(),
              testing::ElementsAreArray({bar_image_updated}));

  task_environment_.RunUntilIdle();
}

TEST_F(ImageAnnotationWorkerTest, MustRemoveOnFileDeleteTest) {
  storage_->Initialize();
  annotation_worker_->Initialize(storage_.get());
  task_environment_.RunUntilIdle();

  base::WriteFile(bar_image_path_, kJpeg_image);

  annotation_worker_->TriggerOnFileChangeForTests(bar_image_path_,
                                                  /*error=*/false);
  task_environment_.RunUntilIdle();

  base::DeleteFile(bar_image_path_);
  annotation_worker_->TriggerOnFileChangeForTests(bar_image_path_,
                                                  /*error=*/false);
  task_environment_.RunUntilIdle();

  EXPECT_TRUE(storage_->GetAllAnnotationsForTest().empty());

  task_environment_.RunUntilIdle();
}

TEST_F(ImageAnnotationWorkerTest, GetAllFilesTest) {
  storage_->Initialize();
  annotation_worker_->Initialize(storage_.get());
  task_environment_.RunUntilIdle();

  EXPECT_TRUE(storage_->GetAllFiles().empty());

  base::WriteFile(bar_image_path_, kJpeg_image);
  annotation_worker_->TriggerOnFileChangeForTests(bar_image_path_,
                                                  /*error=*/false);
  task_environment_.RunUntilIdle();
  EXPECT_THAT(storage_->GetAllFiles(),
              testing::ElementsAreArray({bar_image_path_}));

  base::DeleteFile(bar_image_path_);
  annotation_worker_->TriggerOnFileChangeForTests(bar_image_path_,
                                                  /*error=*/false);
  task_environment_.RunUntilIdle();
  EXPECT_TRUE(storage_->GetAllFiles().empty());

  task_environment_.RunUntilIdle();
}

TEST_F(ImageAnnotationWorkerTest, ProcessDirectoryTest) {
  storage_->Initialize();
  annotation_worker_->Initialize(storage_.get());
  task_environment_.RunUntilIdle();

  auto test_images = test_directory_.AppendASCII("Test Images");
  auto test_images1 = test_directory_.AppendASCII("Test1Images");
  auto new_folder = test_images.AppendASCII("New Folder");
  auto new_folder1 = test_images1.AppendASCII("New Folder");
  base::CreateDirectory(test_images);
  base::CreateDirectory(new_folder);

  auto jpg_path = test_directory_.AppendASCII("bar.jpg");
  base::WriteFile(jpg_path, kJpeg_image);
  auto jpeg_path = test_images.AppendASCII("bar1.jpeg");
  auto jpeg_path1 = test_images1.AppendASCII("bar1.jpeg");
  base::WriteFile(jpeg_path, kJpeg_image);
  auto png_path = new_folder.AppendASCII("bar2.png");
  auto png_path1 = new_folder1.AppendASCII("bar2.png");
  base::WriteFile(png_path, kPng_image);

  auto image_time = base::Time::Now();
  for (const auto& path : {jpg_path, jpeg_path, png_path}) {
    base::TouchFile(path, image_time, image_time);
    annotation_worker_->TriggerOnFileChangeForTests(path,
                                                    /*error=*/false);
  }

  task_environment_.RunUntilIdle();

  ImageInfo jpg_image({"bar"}, jpg_path, image_time, /*file_size=*/16);
  ImageInfo jpeg_image({"bar1"}, jpeg_path, image_time, 16);
  ImageInfo jpeg_image1({"bar1"}, jpeg_path1, image_time, 16);
  ImageInfo png_image({"bar2"}, png_path, image_time, 16);
  ImageInfo png_image1({"bar2"}, png_path1, image_time, 16);

  EXPECT_THAT(
      storage_->GetAllAnnotationsForTest(),
      testing::UnorderedElementsAreArray({jpg_image, jpeg_image, png_image}));

  task_environment_.RunUntilIdle();

  base::Move(test_images, test_images1);
  annotation_worker_->TriggerOnFileChangeForTests(test_images,
                                                  /*error=*/false);
  annotation_worker_->TriggerOnFileChangeForTests(test_images1,
                                                  /*error=*/false);

  task_environment_.RunUntilIdle();

  EXPECT_THAT(
      storage_->GetAllAnnotationsForTest(),
      testing::UnorderedElementsAreArray({jpg_image, jpeg_image1, png_image1}));

  base::DeletePathRecursively(test_images1);
  annotation_worker_->TriggerOnFileChangeForTests(test_images1,
                                                  /*error=*/false);

  task_environment_.RunUntilIdle();

  EXPECT_THAT(storage_->GetAllAnnotationsForTest(),
              testing::UnorderedElementsAreArray({jpg_image}));
}

TEST_F(ImageAnnotationWorkerTest, IgnoreWhenLimitReachedTest) {
  // Overwrite the indexing limit to 0, so that no indexing is allowed.
  base::test::ScopedFeatureList scoped_feature_list;
  base::FieldTrialParams params;
  params["indexing_limit"] = "0";
  scoped_feature_list.InitAndEnableFeatureWithParameters(
      search_features::kLauncherImageSearchIndexingLimit, params);
  // Re-construct the annotation worker with the new param.
  std::vector<base::FilePath> excluded_paths = {
      test_directory_.AppendASCII("TrashBin")};
  annotation_worker_ = std::make_unique<ImageAnnotationWorker>(
      test_directory_, std::move(excluded_paths), /*profile=*/nullptr,
      /*use_file_watchers=*/false,
      /*use_ocr=*/false,
      /*use_ica=*/false);
  annotation_worker_->set_image_processing_delay_for_testing(base::Seconds(0));

  storage_->Initialize();
  annotation_worker_->Initialize(storage_.get());
  task_environment_.RunUntilIdle();

  base::WriteFile(bar_image_path_, kJpeg_image);
  auto bar_image_time = base::Time::Now();
  base::TouchFile(bar_image_path_, bar_image_time, bar_image_time);

  annotation_worker_->TriggerOnFileChangeForTests(bar_image_path_,
                                                  /*error=*/false);
  task_environment_.RunUntilIdle();

  ImageInfo bar_image({"bar"}, bar_image_path_, bar_image_time,
                      /*file_size=*/16);

  EXPECT_TRUE(storage_->GetAllAnnotationsForTest().empty());

  task_environment_.RunUntilIdle();
}

}  // namespace
}  // namespace app_list