chromium/ios/chrome/browser/snapshots/model/snapshot_storage_unittest.mm

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

#import <UIKit/UIKit.h>

#import "base/apple/foundation_util.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/files/scoped_temp_dir.h"
#import "base/run_loop.h"
#import "components/sessions/core/session_id.h"
#import "ios/chrome/browser/snapshots/model/features.h"
#import "ios/chrome/browser/snapshots/model/legacy_snapshot_storage+Testing.h"
#import "ios/chrome/browser/snapshots/model/legacy_snapshot_storage.h"
#import "ios/chrome/browser/snapshots/model/model_swift.h"
#import "ios/chrome/browser/snapshots/model/snapshot_id.h"
#import "ios/chrome/browser/snapshots/model/snapshot_id_wrapper.h"
#import "ios/chrome/browser/snapshots/model/snapshot_scale.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/platform_test.h"

@interface FakeSnapshotStorageObserver : NSObject <SnapshotStorageObserver>
@property(nonatomic, strong) SnapshotIDWrapper* lastUpdatedID;
@end

@implementation FakeSnapshotStorageObserver
- (void)didUpdateSnapshotStorageWithSnapshotID:(SnapshotIDWrapper*)snapshotID {
  self.lastUpdatedID = snapshotID;
}
@end

namespace {

const NSInteger kSnapshotCount = 10;
const NSUInteger kSnapshotPixelSize = 8;

// Constants used to construct path to test the storage migration.
const base::FilePath::CharType kSnapshots[] = FILE_PATH_LITERAL("Snapshots");
const base::FilePath::CharType kSessions[] = FILE_PATH_LITERAL("Sessions");
const base::FilePath::CharType kIdentifier[] = FILE_PATH_LITERAL("Identifier");
const base::FilePath::CharType kFilename[] = FILE_PATH_LITERAL("Filename.txt");

// TODO(crbug.com/40943236): Remove LegacySnapshotStorageTest once the new
// implementation is enabled by default.
class LegacySnapshotStorageTest : public PlatformTest {
 protected:
  void TearDown() override {
    ClearAllImages();
    [snapshot_storage_ shutdown];
    snapshot_storage_ = nil;
    PlatformTest::TearDown();
  }

  // Builds an array of snapshot IDs and an array of UIImages filled with
  // random colors.
  [[nodiscard]] bool CreateSnapshotStorage() {
    DCHECK(!snapshot_storage_);
    if (!scoped_temp_directory_.CreateUniqueTempDir()) {
      return false;
    }

    snapshot_storage_ = [[LegacySnapshotStorage alloc]
        initWithStoragePath:scoped_temp_directory_.GetPath()];

    CGFloat scale = [SnapshotImageScale floatImageScaleForDevice];

    srand(1);

    for (auto i = 0; i < kSnapshotCount; ++i) {
      test_images_.insert(std::make_pair(
          SnapshotID(SessionID::NewUnique().id()), GenerateRandomImage(scale)));
    }

    return true;
  }

  LegacySnapshotStorage* GetSnapshotStorage() {
    DCHECK(snapshot_storage_);
    return snapshot_storage_;
  }

  // Generates an image of `scale`, filled with a random color.
  UIImage* GenerateRandomImage(CGFloat scale) {
    CGSize size = CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize);
    UIGraphicsImageRendererFormat* format =
        [UIGraphicsImageRendererFormat preferredFormat];
    format.scale = scale;
    format.opaque = NO;

    UIGraphicsImageRenderer* renderer =
        [[UIGraphicsImageRenderer alloc] initWithSize:size format:format];

    return [renderer
        imageWithActions:^(UIGraphicsImageRendererContext* UIContext) {
          CGContextRef context = UIContext.CGContext;
          CGFloat r = rand() / CGFloat(RAND_MAX);
          CGFloat g = rand() / CGFloat(RAND_MAX);
          CGFloat b = rand() / CGFloat(RAND_MAX);
          CGContextSetRGBStrokeColor(context, r, g, b, 1.0);
          CGContextSetRGBFillColor(context, r, g, b, 1.0);
          CGContextFillRect(context, CGRectMake(0.0, 0.0, kSnapshotPixelSize,
                                                kSnapshotPixelSize));
        }];
  }

  // Flushes all the runloops internally used by the snapshot storage. This is
  // done by asking to retrieve a non-existent image from disk and blocking
  // until the callback is invoked.
  void FlushRunLoops(LegacySnapshotStorage* storage) {
    base::RunLoop run_loop;
    [storage retrieveImageForSnapshotID:SnapshotID(SessionID::NewUnique().id())
                               callback:base::CallbackToBlock(
                                            base::IgnoreArgs<UIImage*>(
                                                run_loop.QuitClosure()))];
    run_loop.Run();
  }

  // This function removes the snapshots both from dictionary and from disk.
  void ClearAllImages() {
    if (!snapshot_storage_) {
      return;
    }

    for (auto [snapshot_id, _] : test_images_) {
      [snapshot_storage_ removeImageWithSnapshotID:snapshot_id];
    }

    FlushRunLoops(snapshot_storage_);

    __block BOOL foundImage = NO;
    __block NSUInteger numCallbacks = 0;
    for (auto [snapshot_id, _] : test_images_) {
      const base::FilePath path =
          [snapshot_storage_ imagePathForSnapshotID:snapshot_id];

      // Checks that the snapshot is not on disk.
      EXPECT_FALSE(base::PathExists(path));

      // Check that the snapshot is not in the dictionary.
      [snapshot_storage_ retrieveImageForSnapshotID:snapshot_id
                                           callback:^(UIImage* image) {
                                             ++numCallbacks;
                                             if (image) {
                                               foundImage = YES;
                                             }
                                           }];
    }

    // Expect that all the callbacks ran and that none retrieved an image.
    FlushRunLoops(snapshot_storage_);

    EXPECT_EQ(test_images_.size(), numCallbacks);
    EXPECT_FALSE(foundImage);
  }

  // Loads kSnapshotCount color images into the storage.  If
  // `waitForFilesOnDisk` is YES, will not return until the images have been
  // written to disk.
  void LoadAllColorImagesIntoCache(bool waitForFilesOnDisk) {
    // Put color images in the storage.
    for (auto [snapshot_id, image] : test_images_) {
      @autoreleasepool {
        [snapshot_storage_ setImage:image withSnapshotID:snapshot_id];
      }
    }
    if (waitForFilesOnDisk) {
      FlushRunLoops(snapshot_storage_);
      for (auto [snapshot_id, _] : test_images_) {
        // Check that images are on the disk.
        const base::FilePath path =
            [snapshot_storage_ imagePathForSnapshotID:snapshot_id];
        EXPECT_TRUE(base::PathExists(path));
      }
    }
  }

  web::WebTaskEnvironment task_environment_;
  base::ScopedTempDir scoped_temp_directory_;
  LegacySnapshotStorage* snapshot_storage_;
  std::map<SnapshotID, UIImage*> test_images_;
};

// This test simply put all snapshots in the storage and then gets them back.
// As the snapshots are kept in memory, the same pointer can be retrieved.
// This test also checks that images are correctly removed from the disk.
TEST_F(LegacySnapshotStorageTest, ReadAndWriteCache) {
  ASSERT_TRUE(CreateSnapshotStorage());
  LegacySnapshotStorage* storage = GetSnapshotStorage();

  NSUInteger expectedCacheSize = MIN(kSnapshotCount, [storage lruCacheMaxSize]);

  // Put all images to the cache.
  {
    NSUInteger inserted_images = 0;
    for (auto [snapshot_id, image] : test_images_) {
      [storage setImage:image withSnapshotID:snapshot_id];
      if (++inserted_images == expectedCacheSize) {
        break;
      }
    }
  }

  // Get images back.
  __block NSUInteger numberOfCallbacks = 0;
  {
    NSUInteger inserted_images = 0;
    for (auto [snapshot_id, image] : test_images_) {
      UIImage* expected_image = image;
      [storage retrieveImageForSnapshotID:snapshot_id
                                 callback:^(UIImage* retrieved_image) {
                                   // Images have not been removed from the
                                   // dictionnary. We expect the same pointer.
                                   EXPECT_EQ(retrieved_image, expected_image);
                                   ++numberOfCallbacks;
                                 }];
      if (++inserted_images == expectedCacheSize) {
        break;
      }
    }
  }

  EXPECT_EQ(expectedCacheSize, numberOfCallbacks);
}

// This test puts all snapshots in the storage, clears the LRU cache and checks
// if the image can be retrieved via disk.
TEST_F(LegacySnapshotStorageTest, ReadAndWriteWithoutCache) {
  ASSERT_TRUE(CreateSnapshotStorage());
  LegacySnapshotStorage* storage = GetSnapshotStorage();

  LoadAllColorImagesIntoCache(true);

  // Remove color images from LRU cache.
  [storage clearCache];

  __block NSInteger numberOfCallbacks = 0;

  for (auto [snapshot_id, _] : test_images_) {
    // Check that images are on the disk.
    const base::FilePath path = [storage imagePathForSnapshotID:snapshot_id];
    EXPECT_TRUE(base::PathExists(path));

    [storage retrieveImageForSnapshotID:snapshot_id
                               callback:^(UIImage* image) {
                                 EXPECT_TRUE(image);
                                 ++numberOfCallbacks;
                               }];
  }

  // Wait until all callbacks are called.
  FlushRunLoops(storage);

  EXPECT_EQ(numberOfCallbacks, kSnapshotCount);
}

// Tests that an image is immediately deleted when calling
// `-removeImageWithSnapshotID:`.
TEST_F(LegacySnapshotStorageTest, ImageDeleted) {
  ASSERT_TRUE(CreateSnapshotStorage());
  LegacySnapshotStorage* storage = GetSnapshotStorage();

  UIImage* image = GenerateRandomImage(0);
  const SnapshotID kSnapshotID(SessionID::NewUnique().id());
  [storage setImage:image withSnapshotID:kSnapshotID];

  base::FilePath image_path = [storage imagePathForSnapshotID:kSnapshotID];

  // Remove the image and ensure the file is removed.
  [storage removeImageWithSnapshotID:kSnapshotID];
  FlushRunLoops(storage);

  EXPECT_FALSE(base::PathExists(image_path));
  [storage retrieveImageForSnapshotID:kSnapshotID
                             callback:^(UIImage* retrievedImage) {
                               EXPECT_FALSE(retrievedImage);
                             }];
}

// Tests that all images are deleted when calling `-removeAllImages`.
TEST_F(LegacySnapshotStorageTest, AllImagesDeleted) {
  ASSERT_TRUE(CreateSnapshotStorage());
  LegacySnapshotStorage* storage = GetSnapshotStorage();

  UIImage* image = GenerateRandomImage(0);
  const SnapshotID kSnapshotID1(SessionID::NewUnique().id());
  const SnapshotID kSnapshotID2(SessionID::NewUnique().id());
  [storage setImage:image withSnapshotID:kSnapshotID1];
  [storage setImage:image withSnapshotID:kSnapshotID2];
  base::FilePath image_1_path = [storage imagePathForSnapshotID:kSnapshotID1];
  base::FilePath image_2_path = [storage imagePathForSnapshotID:kSnapshotID2];

  // Remove all images and ensure the files are removed.
  [storage removeAllImages];
  FlushRunLoops(storage);

  EXPECT_FALSE(base::PathExists(image_1_path));
  EXPECT_FALSE(base::PathExists(image_2_path));
  [storage retrieveImageForSnapshotID:kSnapshotID1
                             callback:^(UIImage* retrievedImage1) {
                               EXPECT_FALSE(retrievedImage1);
                             }];
  [storage retrieveImageForSnapshotID:kSnapshotID2
                             callback:^(UIImage* retrievedImage2) {
                               EXPECT_FALSE(retrievedImage2);
                             }];
}

// Tests that observers are notified when a snapshot is storaged and removed.
TEST_F(LegacySnapshotStorageTest, ObserversNotifiedOnSetAndRemoveImage) {
  ASSERT_TRUE(CreateSnapshotStorage());
  LegacySnapshotStorage* storage = GetSnapshotStorage();

  FakeSnapshotStorageObserver* observer =
      [[FakeSnapshotStorageObserver alloc] init];
  [storage addObserver:observer];
  EXPECT_FALSE(observer.lastUpdatedID.snapshot_id.valid());
  ASSERT_TRUE(!test_images_.empty());

  // Check if setting a new image is notified to the observer.
  std::pair<SnapshotID, UIImage*> pair = *test_images_.begin();
  [storage setImage:pair.second withSnapshotID:pair.first];
  EXPECT_EQ(pair.first, observer.lastUpdatedID.snapshot_id);

  // Check if removing an image is notified to the observer.
  observer.lastUpdatedID =
      [[SnapshotIDWrapper alloc] initWithSnapshotID:SnapshotID()];
  [storage removeImageWithSnapshotID:pair.first];
  EXPECT_EQ(pair.first, observer.lastUpdatedID.snapshot_id);
  [storage removeObserver:observer];
}

// Tests that creating the SnapshotStorage migrates an existing legacy storage.
TEST_F(LegacySnapshotStorageTest, MigrateCache) {
  ASSERT_TRUE(scoped_temp_directory_.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_directory_.GetPath();

  const base::FilePath storage_path =
      root.Append(kSnapshots).Append(kIdentifier);

  const base::FilePath legacy_path =
      root.Append(kSessions).Append(kIdentifier).Append(kSnapshots);

  ASSERT_TRUE(base::CreateDirectory(legacy_path));
  ASSERT_TRUE(base::WriteFile(legacy_path.Append(kFilename), ""));

  LegacySnapshotStorage* storage =
      [[LegacySnapshotStorage alloc] initWithStoragePath:storage_path
                                              legacyPath:legacy_path];

  FlushRunLoops(storage);

  EXPECT_TRUE(base::DirectoryExists(storage_path));
  EXPECT_FALSE(base::DirectoryExists(legacy_path));

  // Check that the legacy directory content has been moved.
  EXPECT_TRUE(base::PathExists(storage_path.Append(kFilename)));

  [storage shutdown];
}

// Tests that creating the SnapshotStorage simply create the storage directory
// if the legacy path is not specified.
TEST_F(LegacySnapshotStorageTest, MigrateCache_EmptyLegacyPath) {
  ASSERT_TRUE(scoped_temp_directory_.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_directory_.GetPath();

  const base::FilePath storage_path =
      root.Append(kSnapshots).Append(kIdentifier);

  LegacySnapshotStorage* storage =
      [[LegacySnapshotStorage alloc] initWithStoragePath:storage_path
                                              legacyPath:base::FilePath()];

  FlushRunLoops(storage);

  EXPECT_TRUE(base::DirectoryExists(storage_path));

  [storage shutdown];
}

// Tests that creating the SnapshotStorage simply create the storage directory
// if the legacy path does not exists.
TEST_F(LegacySnapshotStorageTest, MigrateCache_NoLegacyStorage) {
  ASSERT_TRUE(scoped_temp_directory_.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_directory_.GetPath();

  const base::FilePath storage_path =
      root.Append(kSnapshots).Append(kIdentifier);

  const base::FilePath legacy_path =
      root.Append(kSessions).Append(kIdentifier).Append(kSnapshots);

  ASSERT_FALSE(base::DirectoryExists(legacy_path));

  LegacySnapshotStorage* storage =
      [[LegacySnapshotStorage alloc] initWithStoragePath:storage_path
                                              legacyPath:legacy_path];

  FlushRunLoops(storage);

  EXPECT_TRUE(base::DirectoryExists(storage_path));
  EXPECT_FALSE(base::DirectoryExists(legacy_path));

  [storage shutdown];
}

// Tests that creating the SnapshotStorage can fail to create the storage
// directory and that the legacy directory is left untouch in that case.
TEST_F(LegacySnapshotStorageTest, MigrateCache_FailCreatingCache) {
  ASSERT_TRUE(scoped_temp_directory_.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_directory_.GetPath();

  const base::FilePath storage_path =
      root.Append(kSnapshots).Append(kIdentifier);

  const base::FilePath legacy_path =
      root.Append(kSessions).Append(kIdentifier).Append(kSnapshots);

  ASSERT_TRUE(base::CreateDirectory(legacy_path));
  ASSERT_TRUE(base::WriteFile(legacy_path.Append(kFilename), ""));

  // Create a file with the same name as the storage directory to
  // simulate a failure (in real world the failure would be caused
  // by a disk that is full).
  ASSERT_TRUE(base::CreateDirectory(storage_path.DirName()));
  ASSERT_TRUE(base::WriteFile(storage_path, ""));

  LegacySnapshotStorage* storage =
      [[LegacySnapshotStorage alloc] initWithStoragePath:storage_path
                                              legacyPath:base::FilePath()];

  FlushRunLoops(storage);

  EXPECT_FALSE(base::DirectoryExists(storage_path));
  EXPECT_TRUE(base::DirectoryExists(legacy_path));
  EXPECT_TRUE(base::PathExists(legacy_path.Append(kFilename)));

  [storage shutdown];
}

// Tests that retrieving grey snapshot images generated by color images stored
// in cache.
TEST_F(LegacySnapshotStorageTest, RetrieveGreyImageFromColorImageInMemory) {
  ASSERT_TRUE(CreateSnapshotStorage());
  LoadAllColorImagesIntoCache(true);

  LegacySnapshotStorage* storage = GetSnapshotStorage();
  __block NSInteger numberOfCallbacks = 0;
  for (auto [snapshot_id, _] : test_images_) {
    [storage retrieveGreyImageForSnapshotID:snapshot_id
                                   callback:^(UIImage* image) {
                                     EXPECT_TRUE(image);
                                     ++numberOfCallbacks;
                                   }];
  }

  // Wait until all callbacks are called.
  FlushRunLoops(storage);

  EXPECT_EQ(numberOfCallbacks, kSnapshotCount);
}

// Tests that retrieving grey snapshot images generated by color images stored
// in disk.
TEST_F(LegacySnapshotStorageTest, RetrieveGreyImageFromColorImageInDisk) {
  ASSERT_TRUE(CreateSnapshotStorage());
  LoadAllColorImagesIntoCache(true);

  LegacySnapshotStorage* storage = GetSnapshotStorage();

  // Remove color images from in-memory storage.
  [storage clearCache];

  __block NSInteger numberOfCallbacks = 0;
  for (auto [snapshot_id, _] : test_images_) {
    [storage retrieveGreyImageForSnapshotID:snapshot_id
                                   callback:^(UIImage* image) {
                                     EXPECT_TRUE(image);
                                     ++numberOfCallbacks;
                                   }];
  }

  // Wait until all callbacks are called.
  FlushRunLoops(storage);

  EXPECT_EQ(numberOfCallbacks, kSnapshotCount);
}

class SnapshotStorageTest : public PlatformTest {
 protected:
  void TearDown() override {
    ClearAllImages();
    snapshot_storage_ = nil;
    PlatformTest::TearDown();
  }

  // Builds an array of snapshot IDs and an array of UIImages filled with
  // random colors.
  [[nodiscard]] bool CreateSnapshotStorage() {
    DCHECK(!snapshot_storage_);
    if (!scoped_temp_directory_.CreateUniqueTempDir()) {
      return false;
    }

    NSURL* storage_url =
        base::apple::FilePathToNSURL(scoped_temp_directory_.GetPath());
    NSURL* legacy_url = base::apple::FilePathToNSURL(base::FilePath());
    snapshot_storage_ =
        [[SnapshotStorage alloc] initWithStorageDirectoryUrl:storage_url
                                          legacyDirectoryUrl:legacy_url];

    CGFloat scale = [SnapshotImageScale floatImageScaleForDevice];

    srand(1);

    for (auto i = 0; i < kSnapshotCount; ++i) {
      test_images_.insert(std::make_pair(
          [[SnapshotIDWrapper alloc]
              initWithSnapshotID:SnapshotID(SessionID::NewUnique().id())],
          GenerateRandomImage(scale)));
    }

    return true;
  }

  SnapshotStorage* GetSnapshotStorage() {
    DCHECK(snapshot_storage_);
    return snapshot_storage_;
  }

  // Generates an image of `scale`, filled with a random color.
  UIImage* GenerateRandomImage(CGFloat scale) {
    CGSize size = CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize);
    UIGraphicsImageRendererFormat* format =
        [UIGraphicsImageRendererFormat preferredFormat];
    format.scale = scale;
    format.opaque = NO;

    UIGraphicsImageRenderer* renderer =
        [[UIGraphicsImageRenderer alloc] initWithSize:size format:format];

    return [renderer
        imageWithActions:^(UIGraphicsImageRendererContext* UIContext) {
          CGContextRef context = UIContext.CGContext;
          CGFloat r = rand() / CGFloat(RAND_MAX);
          CGFloat g = rand() / CGFloat(RAND_MAX);
          CGFloat b = rand() / CGFloat(RAND_MAX);
          CGContextSetRGBStrokeColor(context, r, g, b, 1.0);
          CGContextSetRGBFillColor(context, r, g, b, 1.0);
          CGContextFillRect(context, CGRectMake(0.0, 0.0, kSnapshotPixelSize,
                                                kSnapshotPixelSize));
        }];
  }

  // Flushes all the runloops internally used by the snapshot storage. This is
  // done by asking to retrieve a non-existent image from disk and blocking
  // until the callback is invoked.
  void FlushRunLoops(SnapshotStorage* storage) {
    base::RunLoop run_loop;
    [storage retrieveImageWithSnapshotID:
                 [[SnapshotIDWrapper alloc]
                     initWithSnapshotID:SnapshotID(SessionID::NewUnique().id())]
                              completion:base::CallbackToBlock(
                                             base::IgnoreArgs<UIImage*>(
                                                 run_loop.QuitClosure()))];
    run_loop.Run();
  }

  // This function removes the snapshots both from the cache and from the disk.
  void ClearAllImages() {
    if (!snapshot_storage_) {
      return;
    }

    for (auto [snapshot_id, _] : test_images_) {
      [snapshot_storage_ removeImageWithSnapshotID:snapshot_id];
    }

    FlushRunLoops(snapshot_storage_);

    __block BOOL foundImage = NO;
    __block NSUInteger numCallbacks = 0;
    for (auto [snapshot_id, _] : test_images_) {
      const NSURL* url =
          [snapshot_storage_ imagePathWithSnapshotID:snapshot_id];

      // Checks that the snapshot is not on disk.
      EXPECT_FALSE(
          [[NSFileManager defaultManager] fileExistsAtPath:[url path]]);

      // Check that the snapshot is not in the dictionary.
      [snapshot_storage_ retrieveImageWithSnapshotID:snapshot_id
                                          completion:^(UIImage* image) {
                                            ++numCallbacks;
                                            if (image) {
                                              foundImage = YES;
                                            }
                                          }];
    }

    // Expect that all the callbacks ran and that none retrieved an image.
    FlushRunLoops(snapshot_storage_);

    EXPECT_EQ(test_images_.size(), numCallbacks);
    EXPECT_FALSE(foundImage);
  }

  // Loads kSnapshotCount color images into the storage.  If
  // `waitForFilesOnDisk` is YES, will not return until the images have been
  // written to disk.
  void LoadAllColorImagesIntoCache(bool waitForFilesOnDisk) {
    // Put color images in the storage.
    for (auto [snapshot_id, image] : test_images_) {
      @autoreleasepool {
        [snapshot_storage_ setImage:image snapshotID:snapshot_id];
      }
    }
    if (waitForFilesOnDisk) {
      FlushRunLoops(snapshot_storage_);
      for (auto [snapshot_id, _] : test_images_) {
        // Check that images are on the disk.
        const NSURL* url =
            [snapshot_storage_ imagePathWithSnapshotID:snapshot_id];
        EXPECT_TRUE(
            [[NSFileManager defaultManager] fileExistsAtPath:[url path]]);
      }
    }
  }

  web::WebTaskEnvironment task_environment_;
  base::ScopedTempDir scoped_temp_directory_;
  SnapshotStorage* snapshot_storage_;
  std::map<SnapshotIDWrapper*, UIImage*> test_images_;
};

// This test simply put all snapshots in the storage and then gets them back.
// As the snapshots are kept in memory, the same pointer can be retrieved.
TEST_F(SnapshotStorageTest, ReadAndWriteCache) {
  ASSERT_TRUE(CreateSnapshotStorage());
  SnapshotStorage* storage = GetSnapshotStorage();

  NSUInteger expectedCacheSize = MIN(kSnapshotCount, [storage lruCacheMaxSize]);

  // Put all images to the cache.
  {
    NSUInteger inserted_images = 0;
    for (auto [snapshot_id, image] : test_images_) {
      [storage setImage:image snapshotID:snapshot_id];
      if (++inserted_images == expectedCacheSize) {
        break;
      }
    }
  }

  // Get images back.
  __block NSUInteger numberOfCallbacks = 0;
  {
    NSUInteger inserted_images = 0;
    for (auto [snapshot_id, image] : test_images_) {
      UIImage* expected_image = image;
      [storage retrieveImageWithSnapshotID:snapshot_id
                                completion:^(UIImage* retrieved_image) {
                                  // Images have not been removed from the
                                  // cahce. We expect the same pointer.
                                  EXPECT_EQ(retrieved_image, expected_image);
                                  ++numberOfCallbacks;
                                }];
      if (++inserted_images == expectedCacheSize) {
        break;
      }
    }
  }

  // Wait until all callbacks are called.
  FlushRunLoops(storage);

  EXPECT_EQ(expectedCacheSize, numberOfCallbacks);
}

// This test puts all snapshots in the storage, clears the LRU cache and checks
// if the images can be retrieved from the disk.
TEST_F(SnapshotStorageTest, ReadAndWriteWithoutCache) {
  ASSERT_TRUE(CreateSnapshotStorage());
  SnapshotStorage* storage = GetSnapshotStorage();

  LoadAllColorImagesIntoCache(true);

  // Remove color images from LRU cache.
  [storage clearCache];

  __block NSInteger numberOfCallbacks = 0;

  for (auto [snapshot_id, _] : test_images_) {
    // Check that images are on the disk.
    const NSURL* url = [storage imagePathWithSnapshotID:snapshot_id];
    EXPECT_TRUE([[NSFileManager defaultManager] fileExistsAtPath:[url path]]);

    [storage retrieveImageWithSnapshotID:snapshot_id
                              completion:^(UIImage* image) {
                                EXPECT_TRUE(image);
                                ++numberOfCallbacks;
                              }];
  }

  // Wait until all callbacks are called.
  FlushRunLoops(storage);

  EXPECT_EQ(numberOfCallbacks, kSnapshotCount);
}

// Tests that an image is immediately deleted when calling
// `-removeImageWithSnapshotID:`.
TEST_F(SnapshotStorageTest, ImageDeleted) {
  ASSERT_TRUE(CreateSnapshotStorage());
  SnapshotStorage* storage = GetSnapshotStorage();

  UIImage* image = GenerateRandomImage(0);
  SnapshotIDWrapper* kSnapshotID = [[SnapshotIDWrapper alloc]
      initWithSnapshotID:SnapshotID(SessionID::NewUnique().id())];
  [storage setImage:image snapshotID:kSnapshotID];

  NSURL* url = [storage imagePathWithSnapshotID:kSnapshotID];

  // Remove the image and ensure the file is removed.
  [storage removeImageWithSnapshotID:kSnapshotID];
  FlushRunLoops(storage);

  EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:[url path]]);
  [storage retrieveImageWithSnapshotID:kSnapshotID
                            completion:^(UIImage* retrievedImage) {
                              EXPECT_FALSE(retrievedImage);
                            }];
}

// Tests that all images are deleted when calling `-removeAllImages`.
TEST_F(SnapshotStorageTest, AllImagesDeleted) {
  ASSERT_TRUE(CreateSnapshotStorage());
  SnapshotStorage* storage = GetSnapshotStorage();

  UIImage* image = GenerateRandomImage(0);
  SnapshotIDWrapper* kSnapshotID1 = [[SnapshotIDWrapper alloc]
      initWithSnapshotID:SnapshotID(SessionID::NewUnique().id())];
  SnapshotIDWrapper* kSnapshotID2 = [[SnapshotIDWrapper alloc]
      initWithSnapshotID:SnapshotID(SessionID::NewUnique().id())];
  [storage setImage:image snapshotID:kSnapshotID1];
  [storage setImage:image snapshotID:kSnapshotID2];
  NSURL* url1 = [storage imagePathWithSnapshotID:kSnapshotID1];
  NSURL* url2 = [storage imagePathWithSnapshotID:kSnapshotID2];

  // Remove all images and ensure the files are removed.
  [storage removeAllImages];
  FlushRunLoops(storage);

  EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:[url1 path]]);
  EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:[url2 path]]);
  [storage retrieveImageWithSnapshotID:kSnapshotID1
                            completion:^(UIImage* retrievedImage1) {
                              EXPECT_FALSE(retrievedImage1);
                            }];
  [storage retrieveImageWithSnapshotID:kSnapshotID2
                            completion:^(UIImage* retrievedImage2) {
                              EXPECT_FALSE(retrievedImage2);
                            }];
}

// Tests that observers are notified when a snapshot is storaged and removed.
TEST_F(SnapshotStorageTest, ObserversNotifiedOnSetAndRemoveImage) {
  ASSERT_TRUE(CreateSnapshotStorage());
  SnapshotStorage* storage = GetSnapshotStorage();

  FakeSnapshotStorageObserver* observer =
      [[FakeSnapshotStorageObserver alloc] init];
  [storage addObserver:observer];
  EXPECT_FALSE(observer.lastUpdatedID.snapshot_id.valid());
  ASSERT_TRUE(!test_images_.empty());

  // Check if setting a new image is notified to the observer.
  std::pair<SnapshotIDWrapper*, UIImage*> pair = *test_images_.begin();
  [storage setImage:pair.second snapshotID:pair.first];
  EXPECT_EQ(pair.first, observer.lastUpdatedID);

  // Check if removing an image is notified to the observer.
  observer.lastUpdatedID =
      [[SnapshotIDWrapper alloc] initWithSnapshotID:SnapshotID()];
  [storage removeImageWithSnapshotID:pair.first];
  EXPECT_EQ(pair.first, observer.lastUpdatedID);
  [storage removeObserver:observer];
}

// Tests that creating the SnapshotStorage migrates an existing legacy storage.
TEST_F(SnapshotStorageTest, MigrateCache) {
  ASSERT_TRUE(scoped_temp_directory_.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_directory_.GetPath();

  const base::FilePath storage_path =
      root.Append(kSnapshots).Append(kIdentifier);

  const base::FilePath legacy_path =
      root.Append(kSessions).Append(kIdentifier).Append(kSnapshots);

  ASSERT_TRUE(base::CreateDirectory(legacy_path));
  ASSERT_TRUE(base::WriteFile(legacy_path.Append(kFilename), ""));

  NSURL* storage_url = base::apple::FilePathToNSURL(storage_path);
  NSURL* legacy_url = base::apple::FilePathToNSURL(legacy_path);
  SnapshotStorage* storage =
      [[SnapshotStorage alloc] initWithStorageDirectoryUrl:storage_url
                                        legacyDirectoryUrl:legacy_url];

  FlushRunLoops(storage);

  EXPECT_TRUE(base::DirectoryExists(storage_path));
  EXPECT_FALSE(base::DirectoryExists(legacy_path));

  // Check that the legacy directory content has been moved.
  EXPECT_TRUE(base::PathExists(storage_path.Append(kFilename)));
}

// Tests that creating the SnapshotStorage simply create the storage directory
// if the legacy path is not specified.
TEST_F(SnapshotStorageTest, MigrateCache_EmptyLegacyPath) {
  ASSERT_TRUE(scoped_temp_directory_.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_directory_.GetPath();

  const base::FilePath storage_path =
      root.Append(kSnapshots).Append(kIdentifier);

  NSURL* storage_url = base::apple::FilePathToNSURL(storage_path);
  NSURL* legacy_url = base::apple::FilePathToNSURL(base::FilePath());
  SnapshotStorage* storage =
      [[SnapshotStorage alloc] initWithStorageDirectoryUrl:storage_url
                                        legacyDirectoryUrl:legacy_url];

  FlushRunLoops(storage);

  EXPECT_TRUE(base::DirectoryExists(storage_path));
}

// Tests that creating the SnapshotStorage simply create the storage directory
// if the legacy path does not exists.
TEST_F(SnapshotStorageTest, MigrateCache_NoLegacyStorage) {
  ASSERT_TRUE(scoped_temp_directory_.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_directory_.GetPath();

  const base::FilePath storage_path =
      root.Append(kSnapshots).Append(kIdentifier);

  const base::FilePath legacy_path =
      root.Append(kSessions).Append(kIdentifier).Append(kSnapshots);

  ASSERT_FALSE(base::DirectoryExists(legacy_path));

  NSURL* storage_url = base::apple::FilePathToNSURL(storage_path);
  NSURL* legacy_url = base::apple::FilePathToNSURL(legacy_path);
  SnapshotStorage* storage =
      [[SnapshotStorage alloc] initWithStorageDirectoryUrl:storage_url
                                        legacyDirectoryUrl:legacy_url];

  FlushRunLoops(storage);

  EXPECT_TRUE(base::DirectoryExists(storage_path));
  EXPECT_FALSE(base::DirectoryExists(legacy_path));
}

// Tests that creating the SnapshotStorage can fail to create the storage
// directory and that the legacy directory is left untouch in that case.
TEST_F(SnapshotStorageTest, MigrateCache_FailCreatingCache) {
  ASSERT_TRUE(scoped_temp_directory_.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_directory_.GetPath();

  const base::FilePath storage_path =
      root.Append(kSnapshots).Append(kIdentifier);

  const base::FilePath legacy_path =
      root.Append(kSessions).Append(kIdentifier).Append(kSnapshots);

  ASSERT_TRUE(base::CreateDirectory(legacy_path));
  ASSERT_TRUE(base::WriteFile(legacy_path.Append(kFilename), ""));

  // Create a file with the same name as the storage directory to
  // simulate a failure (in real world the failure would be caused
  // by a disk that is full).
  ASSERT_TRUE(base::CreateDirectory(storage_path.DirName()));
  ASSERT_TRUE(base::WriteFile(storage_path, ""));

  NSURL* storage_url = base::apple::FilePathToNSURL(storage_path);
  NSURL* legacy_url = base::apple::FilePathToNSURL(base::FilePath());
  SnapshotStorage* storage =
      [[SnapshotStorage alloc] initWithStorageDirectoryUrl:storage_url
                                        legacyDirectoryUrl:legacy_url];

  FlushRunLoops(storage);

  EXPECT_FALSE(base::DirectoryExists(storage_path));
  EXPECT_TRUE(base::DirectoryExists(legacy_path));
  EXPECT_TRUE(base::PathExists(legacy_path.Append(kFilename)));
}

// Tests that retrieving grey snapshot images generated by color images stored
// in cache.
TEST_F(SnapshotStorageTest, RetrieveGreyImageFromColorImageInMemory) {
  ASSERT_TRUE(CreateSnapshotStorage());
  LoadAllColorImagesIntoCache(true);

  SnapshotStorage* storage = GetSnapshotStorage();
  __block NSInteger numberOfCallbacks = 0;
  for (auto [snapshot_id, _] : test_images_) {
    [storage retrieveGreyImageWithSnapshotID:snapshot_id
                                  completion:^(UIImage* image) {
                                    EXPECT_TRUE(image);
                                    ++numberOfCallbacks;
                                  }];
  }

  // Wait until all callbacks are called.
  FlushRunLoops(storage);

  EXPECT_EQ(numberOfCallbacks, kSnapshotCount);
}

// Tests that retrieving grey snapshot images generated by color images stored
// in disk.
TEST_F(SnapshotStorageTest, RetrieveGreyImageFromColorImageInDisk) {
  ASSERT_TRUE(CreateSnapshotStorage());
  LoadAllColorImagesIntoCache(true);

  SnapshotStorage* storage = GetSnapshotStorage();

  // Remove color images from in-memory storage.
  [storage clearCache];

  __block NSInteger numberOfCallbacks = 0;
  for (auto [snapshot_id, _] : test_images_) {
    [storage retrieveGreyImageWithSnapshotID:snapshot_id
                                  completion:^(UIImage* image) {
                                    EXPECT_TRUE(image);
                                    ++numberOfCallbacks;
                                  }];
  }

  // Wait until all callbacks are called.
  FlushRunLoops(storage);

  EXPECT_EQ(numberOfCallbacks, kSnapshotCount);
}

}  // namespace