1// Copyright 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "ios/chrome/browser/snapshots/snapshot_cache.h"
6
7#import <UIKit/UIKit.h>
8
9#include "base/files/file_path.h"
10#include "base/files/file_util.h"
11#include "base/files/scoped_temp_dir.h"
12#include "base/format_macros.h"
13#include "base/location.h"
14#include "base/mac/scoped_cftyperef.h"
15#include "base/run_loop.h"
16#include "base/strings/sys_string_conversions.h"
17#include "base/task/thread_pool/thread_pool_instance.h"
18#include "base/time/time.h"
19#import "ios/chrome/browser/snapshots/snapshot_cache_internal.h"
20#import "ios/chrome/browser/snapshots/snapshot_cache_observer.h"
21#include "ios/web/public/test/web_task_environment.h"
22#include "ios/web/public/thread/web_thread.h"
23#include "testing/gtest/include/gtest/gtest.h"
24#include "testing/gtest_mac.h"
25#include "testing/platform_test.h"
26
27#if !defined(__has_feature) || !__has_feature(objc_arc)
28#error "This file requires ARC support."
29#endif
30
31static const NSUInteger kSnapshotCount = 10;
32static const NSUInteger kSnapshotPixelSize = 8;
33
34@interface FakeSnapshotCacheObserver : NSObject<SnapshotCacheObserver>
35@property(nonatomic, copy) NSString* lastUpdatedIdentifier;
36@end
37
38@implementation FakeSnapshotCacheObserver
39@synthesize lastUpdatedIdentifier = _lastUpdatedIdentifier;
40- (void)snapshotCache:(SnapshotCache*)snapshotCache
41    didUpdateSnapshotForIdentifier:(NSString*)identifier {
42  self.lastUpdatedIdentifier = identifier;
43}
44@end
45
46namespace {
47
48class SnapshotCacheTest : public PlatformTest {
49 protected:
50  // Build an array of snapshot IDs and an array of UIImages filled with
51  // random colors.
52  void SetUp() override {
53    PlatformTest::SetUp();
54    ASSERT_TRUE(scoped_temp_directory_.CreateUniqueTempDir());
55    snapshotCache_ = [[SnapshotCache alloc]
56        initWithStoragePath:scoped_temp_directory_.GetPath()];
57    testImages_ = [[NSMutableArray alloc] initWithCapacity:kSnapshotCount];
58    snapshotIDs_ = [[NSMutableArray alloc] initWithCapacity:kSnapshotCount];
59
60    CGFloat scale = [snapshotCache_ snapshotScaleForDevice];
61    UIGraphicsBeginImageContextWithOptions(
62        CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, scale);
63    CGContextRef context = UIGraphicsGetCurrentContext();
64    srand(1);
65
66    for (NSUInteger i = 0; i < kSnapshotCount; ++i) {
67      UIImage* image = GenerateRandomImage(context);
68      [testImages_ addObject:image];
69      [snapshotIDs_
70          addObject:[NSString stringWithFormat:@"SnapshotID-%" PRIuNS, i]];
71    }
72
73    UIGraphicsEndImageContext();
74
75    ClearDumpedImages();
76  }
77
78  void TearDown() override {
79    ClearDumpedImages();
80    [snapshotCache_ shutdown];
81    snapshotCache_ = nil;
82    PlatformTest::TearDown();
83  }
84
85  SnapshotCache* GetSnapshotCache() { return snapshotCache_; }
86
87  // Adds a fake snapshot file into |directory| using |snapshot_id| in the
88  // filename.
89  base::FilePath AddSnapshotFileToDirectory(const base::FilePath directory,
90                                            NSString* snapshot_id) {
91    // Use the same filename as designated by SnapshotCache.
92    base::FilePath cache_image_path =
93        [GetSnapshotCache() imagePathForSnapshotID:snapshot_id];
94    base::FilePath image_filename = cache_image_path.BaseName();
95    base::FilePath image_path = directory.Append(image_filename);
96
97    EXPECT_TRUE(WriteFile(image_path, ""));
98    EXPECT_TRUE(base::PathExists(image_path));
99    EXPECT_FALSE(base::PathExists(cache_image_path));
100    return image_path;
101  }
102
103  // Generates an image filled with a random color.
104  UIImage* GenerateRandomImage(CGContextRef context) {
105    CGFloat r = rand() / CGFloat(RAND_MAX);
106    CGFloat g = rand() / CGFloat(RAND_MAX);
107    CGFloat b = rand() / CGFloat(RAND_MAX);
108    CGContextSetRGBStrokeColor(context, r, g, b, 1.0);
109    CGContextSetRGBFillColor(context, r, g, b, 1.0);
110    CGContextFillRect(
111        context, CGRectMake(0.0, 0.0, kSnapshotPixelSize, kSnapshotPixelSize));
112    return UIGraphicsGetImageFromCurrentImageContext();
113  }
114
115  // Generates an image of |size|, filled with a random color.
116  UIImage* GenerateRandomImage(CGSize size) {
117    UIGraphicsBeginImageContextWithOptions(size, /*opaque=*/NO,
118                                           UIScreen.mainScreen.scale);
119    CGContextRef context = UIGraphicsGetCurrentContext();
120    UIImage* image = GenerateRandomImage(context);
121    UIGraphicsEndImageContext();
122    return image;
123  }
124
125  // Flushes all the runloops internally used by the snapshot cache.
126  void FlushRunLoops() {
127    base::ThreadPoolInstance::Get()->FlushForTesting();
128    base::RunLoop().RunUntilIdle();
129  }
130
131  // This function removes the snapshots both from dictionary and from disk.
132  void ClearDumpedImages() {
133    SnapshotCache* cache = GetSnapshotCache();
134
135    NSString* snapshotID;
136    for (snapshotID in snapshotIDs_)
137      [cache removeImageWithSnapshotID:snapshotID];
138
139    FlushRunLoops();
140    // The above calls to -removeImageWithSnapshotID remove both the color
141    // and grey snapshots for each snapshotID, if they are on disk.  However,
142    // ensure we also get rid of the grey snapshots in memory.
143    [cache removeGreyCache];
144
145    __block BOOL foundImage = NO;
146    __block NSUInteger numCallbacks = 0;
147    for (snapshotID in snapshotIDs_) {
148      base::FilePath path([cache imagePathForSnapshotID:snapshotID]);
149
150      // Checks that the snapshot is not on disk.
151      EXPECT_FALSE(base::PathExists(path));
152
153      // Check that the snapshot is not in the dictionary.
154      [cache retrieveImageForSnapshotID:snapshotID
155                               callback:^(UIImage* image) {
156                                 ++numCallbacks;
157                                 if (image)
158                                   foundImage = YES;
159                               }];
160    }
161
162    // Expect that all the callbacks ran and that none retrieved an image.
163    FlushRunLoops();
164    EXPECT_EQ([snapshotIDs_ count], numCallbacks);
165    EXPECT_FALSE(foundImage);
166  }
167
168  // Loads kSnapshotCount color images into the cache.  If |waitForFilesOnDisk|
169  // is YES, will not return until the images have been written to disk.
170  void LoadAllColorImagesIntoCache(bool waitForFilesOnDisk) {
171    LoadColorImagesIntoCache(kSnapshotCount, waitForFilesOnDisk);
172  }
173
174  // Loads |count| color images into the cache.  If |waitForFilesOnDisk|
175  // is YES, will not return until the images have been written to disk.
176  void LoadColorImagesIntoCache(NSUInteger count, bool waitForFilesOnDisk) {
177    SnapshotCache* cache = GetSnapshotCache();
178    // Put color images in the cache.
179    for (NSUInteger i = 0; i < count; ++i) {
180      @autoreleasepool {
181        UIImage* image = [testImages_ objectAtIndex:i];
182        NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
183        [cache setImage:image withSnapshotID:snapshotID];
184      }
185    }
186    if (waitForFilesOnDisk) {
187      FlushRunLoops();
188      for (NSUInteger i = 0; i < count; ++i) {
189        // Check that images are on the disk.
190        NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
191        base::FilePath path([cache imagePathForSnapshotID:snapshotID]);
192        EXPECT_TRUE(base::PathExists(path));
193      }
194    }
195  }
196
197  // Waits for the first |count| grey images for |snapshotIDs_| to be placed in
198  // the cache.
199  void WaitForGreyImagesInCache(NSUInteger count) {
200    SnapshotCache* cache = GetSnapshotCache();
201    FlushRunLoops();
202    for (NSUInteger i = 0; i < count; i++)
203      EXPECT_TRUE([cache hasGreyImageInMemory:snapshotIDs_[i]]);
204  }
205
206  // Guesses the order of the color channels in the image.
207  // Supports RGB, BGR, RGBA, BGRA, ARGB, ABGR.
208  // Returns the position of each channel between 0 and 3.
209  void ComputeColorComponents(CGImageRef cgImage,
210                              int* red,
211                              int* green,
212                              int* blue) {
213    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
214    CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
215    int byteOrder = bitmapInfo & kCGBitmapByteOrderMask;
216
217    *red = 0;
218    *green = 1;
219    *blue = 2;
220
221    if (alphaInfo == kCGImageAlphaLast ||
222        alphaInfo == kCGImageAlphaPremultipliedLast ||
223        alphaInfo == kCGImageAlphaNoneSkipLast) {
224      *red = 1;
225      *green = 2;
226      *blue = 3;
227    }
228
229    if (byteOrder != kCGBitmapByteOrder32Host) {
230      int lastChannel = (CGImageGetBitsPerPixel(cgImage) == 24) ? 2 : 3;
231      *red = lastChannel - *red;
232      *green = lastChannel - *green;
233      *blue = lastChannel - *blue;
234    }
235  }
236
237  void TriggerMemoryWarning() {
238    // _performMemoryWarning is a private API and must not be compiled into
239    // official builds.
240#pragma clang diagnostic push
241#pragma clang diagnostic ignored "-Wundeclared-selector"
242    [[UIApplication sharedApplication]
243        performSelector:@selector(_performMemoryWarning)];
244#pragma clang diagnostic pop
245  }
246
247  web::WebTaskEnvironment task_environment_;
248  base::ScopedTempDir scoped_temp_directory_;
249  SnapshotCache* snapshotCache_;
250  NSMutableArray<NSString*>* snapshotIDs_;
251  NSMutableArray<UIImage*>* testImages_;
252};
253
254// This test simply put all the snapshots in the cache and then gets them back
255// As the snapshots are kept in memory, the same pointer can be retrieved.
256// This test also checks that images are correctly removed from the disk.
257TEST_F(SnapshotCacheTest, Cache) {
258  SnapshotCache* cache = GetSnapshotCache();
259
260  NSUInteger expectedCacheSize = MIN(kSnapshotCount, [cache lruCacheMaxSize]);
261
262  // Put all images in the cache.
263  for (NSUInteger i = 0; i < expectedCacheSize; ++i) {
264    UIImage* image = [testImages_ objectAtIndex:i];
265    NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
266    [cache setImage:image withSnapshotID:snapshotID];
267  }
268
269  // Get images back.
270  __block NSUInteger numberOfCallbacks = 0;
271  for (NSUInteger i = 0; i < expectedCacheSize; ++i) {
272    NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
273    UIImage* expectedImage = [testImages_ objectAtIndex:i];
274    EXPECT_TRUE(expectedImage != nil);
275    [cache retrieveImageForSnapshotID:snapshotID
276                             callback:^(UIImage* image) {
277                               // Images have not been removed from the
278                               // dictionnary. We expect the same pointer.
279                               EXPECT_EQ(expectedImage, image);
280                               ++numberOfCallbacks;
281                             }];
282  }
283  EXPECT_EQ(expectedCacheSize, numberOfCallbacks);
284}
285
286// This test puts all the snapshots in the cache and flushes them to disk.
287// The snapshots are then reloaded from the disk, and the colors are compared.
288TEST_F(SnapshotCacheTest, SaveToDisk) {
289  SnapshotCache* cache = GetSnapshotCache();
290
291  // Put all images in the cache.
292  for (NSUInteger i = 0; i < kSnapshotCount; ++i) {
293    UIImage* image = [testImages_ objectAtIndex:i];
294    NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
295    [cache setImage:image withSnapshotID:snapshotID];
296  }
297  FlushRunLoops();
298
299  for (NSUInteger i = 0; i < kSnapshotCount; ++i) {
300    // Check that images are on the disk.
301    NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
302
303    base::FilePath path([cache imagePathForSnapshotID:snapshotID]);
304    EXPECT_TRUE(base::PathExists(path));
305
306    // Check image colors by comparing the first pixel against the reference
307    // image.
308    UIImage* image =
309        [UIImage imageWithContentsOfFile:base::SysUTF8ToNSString(path.value())];
310    CGImageRef cgImage = [image CGImage];
311    ASSERT_TRUE(cgImage != nullptr);
312
313    base::ScopedCFTypeRef<CFDataRef> pixelData(
314        CGDataProviderCopyData(CGImageGetDataProvider(cgImage)));
315    const char* pixels =
316        reinterpret_cast<const char*>(CFDataGetBytePtr(pixelData));
317    EXPECT_TRUE(pixels);
318
319    UIImage* referenceImage = [testImages_ objectAtIndex:i];
320    CGImageRef referenceCgImage = [referenceImage CGImage];
321    base::ScopedCFTypeRef<CFDataRef> referenceData(
322        CGDataProviderCopyData(CGImageGetDataProvider(referenceCgImage)));
323    const char* referencePixels =
324        reinterpret_cast<const char*>(CFDataGetBytePtr(referenceData));
325    EXPECT_TRUE(referencePixels);
326
327    if (pixels != nil && referencePixels != nil) {
328      // Color components may not be in the same order,
329      // because of writing to disk and reloading.
330      int red, green, blue;
331      ComputeColorComponents(cgImage, &red, &green, &blue);
332
333      int referenceRed, referenceGreen, referenceBlue;
334      ComputeColorComponents(referenceCgImage, &referenceRed, &referenceGreen,
335                             &referenceBlue);
336
337      // Colors may not be exactly the same (compression or rounding errors)
338      // thus a small difference is allowed.
339      EXPECT_NEAR(referencePixels[referenceRed], pixels[red], 1);
340      EXPECT_NEAR(referencePixels[referenceGreen], pixels[green], 1);
341      EXPECT_NEAR(referencePixels[referenceBlue], pixels[blue], 1);
342    }
343  }
344}
345
346// Tests that migration moves only specified files to the current SnapshotCache
347// folder. Tests that the legacy folder and any remaining contents are deleted.
348TEST_F(SnapshotCacheTest, MigrationMovesFileAndDeletesSource) {
349  base::ScopedTempDir scoped_source_directory;
350  ASSERT_TRUE(scoped_source_directory.CreateUniqueTempDir());
351
352  SnapshotCache* cache = GetSnapshotCache();
353  base::FilePath source_folder = scoped_source_directory.GetPath();
354
355  // This snapshot will be included in migration.
356  NSString* image1_id = [[NSUUID UUID] UUIDString];
357  base::FilePath source_image1_path =
358      AddSnapshotFileToDirectory(source_folder, image1_id);
359  base::FilePath destination_image1_path =
360      [cache imagePathForSnapshotID:image1_id];
361
362  // This snapshot will be excluded from migration.
363  NSString* image2_id = [[NSUUID UUID] UUIDString];
364  base::FilePath source_image2_path =
365      AddSnapshotFileToDirectory(source_folder, image2_id);
366  base::FilePath destination_image2_path =
367      [cache imagePathForSnapshotID:image2_id];
368
369  NSSet<NSString*>* snapshot_ids = [[NSSet alloc] initWithArray:@[ image1_id ]];
370  [cache migrateSnapshotsWithIDs:snapshot_ids fromSourcePath:source_folder];
371  FlushRunLoops();
372
373  // image1 should have been moved to the destination path.
374  EXPECT_TRUE(base::PathExists(destination_image1_path));
375
376  // image2 should not have been moved.
377  EXPECT_FALSE(base::PathExists(destination_image2_path));
378
379  // The legacy folder should have been deleted.
380  EXPECT_FALSE(base::PathExists(source_image1_path));
381  EXPECT_FALSE(base::PathExists(source_image1_path));
382  EXPECT_FALSE(base::PathExists(source_folder));
383}
384
385TEST_F(SnapshotCacheTest, Purge) {
386  SnapshotCache* cache = GetSnapshotCache();
387
388  // Put all images in the cache.
389  for (NSUInteger i = 0; i < kSnapshotCount; ++i) {
390    UIImage* image = [testImages_ objectAtIndex:i];
391    NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
392    [cache setImage:image withSnapshotID:snapshotID];
393  }
394
395  NSMutableSet* liveSnapshotIDs = [NSMutableSet setWithCapacity:1];
396  [liveSnapshotIDs addObject:[snapshotIDs_ objectAtIndex:0]];
397
398  // Purge the cache.
399  [cache purgeCacheOlderThan:(base::Time::Now() - base::TimeDelta::FromHours(1))
400                     keeping:liveSnapshotIDs];
401  FlushRunLoops();
402
403  // Check that nothing has been deleted.
404  for (NSUInteger i = 0; i < kSnapshotCount; ++i) {
405    // Check that images are on the disk.
406    NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
407
408    base::FilePath path([cache imagePathForSnapshotID:snapshotID]);
409    EXPECT_TRUE(base::PathExists(path));
410  }
411
412  // Purge the cache.
413  [cache purgeCacheOlderThan:base::Time::Now() keeping:liveSnapshotIDs];
414  FlushRunLoops();
415
416  // Check that the file have been deleted.
417  for (NSUInteger i = 0; i < kSnapshotCount; ++i) {
418    // Check that images are on the disk.
419    NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
420
421    base::FilePath path([cache imagePathForSnapshotID:snapshotID]);
422    if (i == 0)
423      EXPECT_TRUE(base::PathExists(path));
424    else
425      EXPECT_FALSE(base::PathExists(path));
426  }
427}
428
429// Loads the color images into the cache, and pins two of them.  Ensures that
430// only the two pinned IDs remain in memory after a memory warning.
431TEST_F(SnapshotCacheTest, HandleMemoryWarning) {
432  LoadAllColorImagesIntoCache(true);
433
434  SnapshotCache* cache = GetSnapshotCache();
435
436  NSString* firstPinnedID = [snapshotIDs_ objectAtIndex:4];
437  NSString* secondPinnedID = [snapshotIDs_ objectAtIndex:6];
438  NSMutableSet* set = [NSMutableSet set];
439  [set addObject:firstPinnedID];
440  [set addObject:secondPinnedID];
441  cache.pinnedIDs = set;
442
443  TriggerMemoryWarning();
444
445  EXPECT_EQ(YES, [cache hasImageInMemory:firstPinnedID]);
446  EXPECT_EQ(YES, [cache hasImageInMemory:secondPinnedID]);
447
448  NSString* notPinnedID = [snapshotIDs_ objectAtIndex:2];
449  EXPECT_FALSE([cache hasImageInMemory:notPinnedID]);
450
451  // Wait for the final image to be pulled off disk.
452  FlushRunLoops();
453}
454
455// Tests that createGreyCache creates the grey snapshots in the background,
456// from color images in the in-memory cache.  When the grey images are all
457// loaded into memory, tests that the request to retrieve the grey snapshot
458// calls the callback immediately.
459// Disabled on simulators because it sometimes crashes. crbug/421425
460#if !TARGET_IPHONE_SIMULATOR
461TEST_F(SnapshotCacheTest, CreateGreyCache) {
462  LoadAllColorImagesIntoCache(true);
463
464  // Request the creation of a grey image cache for all images.
465  SnapshotCache* cache = GetSnapshotCache();
466  [cache createGreyCache:snapshotIDs_];
467
468  // Wait for them to be put into the grey image cache.
469  WaitForGreyImagesInCache(kSnapshotCount);
470
471  __block NSUInteger numberOfCallbacks = 0;
472  for (NSUInteger i = 0; i < kSnapshotCount; ++i) {
473    NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
474    [cache retrieveGreyImageForSnapshotID:snapshotID
475                                 callback:^(UIImage* image) {
476                                   EXPECT_TRUE(image);
477                                   ++numberOfCallbacks;
478                                 }];
479  }
480
481  EXPECT_EQ(numberOfCallbacks, kSnapshotCount);
482}
483
484// Same as previous test, except that all the color images are on disk,
485// rather than in memory.
486// Disabled due to the greyImage crash.  b/8048597
487TEST_F(SnapshotCacheTest, CreateGreyCacheFromDisk) {
488  LoadAllColorImagesIntoCache(true);
489
490  // Remove color images from in-memory cache.
491  SnapshotCache* cache = GetSnapshotCache();
492
493  TriggerMemoryWarning();
494
495  // Request the creation of a grey image cache for all images.
496  [cache createGreyCache:snapshotIDs_];
497
498  // Wait for them to be put into the grey image cache.
499  WaitForGreyImagesInCache(kSnapshotCount);
500
501  __block NSUInteger numberOfCallbacks = 0;
502  for (NSUInteger i = 0; i < kSnapshotCount; ++i) {
503    NSString* snapshotID = [snapshotIDs_ objectAtIndex:i];
504    [cache retrieveGreyImageForSnapshotID:snapshotID
505                                 callback:^(UIImage* image) {
506                                   EXPECT_TRUE(image);
507                                   ++numberOfCallbacks;
508                                 }];
509  }
510
511  EXPECT_EQ(numberOfCallbacks, kSnapshotCount);
512}
513#endif  // !TARGET_IPHONE_SIMULATOR
514
515// Tests mostRecentGreyBlock, which is a block to be called when the most
516// recently requested grey image is finally loaded.
517// The test requests three images be cached as grey images.  Only the final
518// callback of the three requests should be called.
519// Disabled due to the greyImage crash.  b/8048597
520TEST_F(SnapshotCacheTest, MostRecentGreyBlock) {
521  const NSUInteger kNumImages = 3;
522  NSMutableArray* snapshotIDs =
523      [[NSMutableArray alloc] initWithCapacity:kNumImages];
524  [snapshotIDs addObject:[snapshotIDs_ objectAtIndex:0]];
525  [snapshotIDs addObject:[snapshotIDs_ objectAtIndex:1]];
526  [snapshotIDs addObject:[snapshotIDs_ objectAtIndex:2]];
527
528  SnapshotCache* cache = GetSnapshotCache();
529
530  // Put 3 images in the cache.
531  LoadColorImagesIntoCache(kNumImages, true);
532  // Make sure the color images are only on disk, to ensure the background
533  // thread is slow enough to queue up the requests.
534  TriggerMemoryWarning();
535
536  // Enable the grey image cache.
537  [cache createGreyCache:snapshotIDs];
538
539  // Request the grey versions
540  __block BOOL firstCallbackCalled = NO;
541  __block BOOL secondCallbackCalled = NO;
542  __block BOOL thirdCallbackCalled = NO;
543  [cache greyImageForSnapshotID:[snapshotIDs_ objectAtIndex:0]
544                       callback:^(UIImage*) {
545                         firstCallbackCalled = YES;
546                       }];
547  [cache greyImageForSnapshotID:[snapshotIDs_ objectAtIndex:1]
548                       callback:^(UIImage*) {
549                         secondCallbackCalled = YES;
550                       }];
551  [cache greyImageForSnapshotID:[snapshotIDs_ objectAtIndex:2]
552                       callback:^(UIImage*) {
553                         thirdCallbackCalled = YES;
554                       }];
555
556  // Wait for them to be loaded.
557  WaitForGreyImagesInCache(kNumImages);
558
559  EXPECT_FALSE(firstCallbackCalled);
560  EXPECT_FALSE(secondCallbackCalled);
561  EXPECT_TRUE(thirdCallbackCalled);
562}
563
564// Test the function used to save a grey copy of a color snapshot fully on a
565// background thread when the application is backgrounded.
566TEST_F(SnapshotCacheTest, GreyImageAllInBackground) {
567  LoadAllColorImagesIntoCache(true);
568
569  SnapshotCache* cache = GetSnapshotCache();
570
571  // Now convert every image into a grey image, on disk, in the background.
572  for (NSUInteger i = 0; i < kSnapshotCount; ++i) {
573    [cache saveGreyInBackgroundForSnapshotID:[snapshotIDs_ objectAtIndex:i]];
574  }
575
576  // Waits for the grey images for |snapshotIDs_| to be written to disk, which
577  // happens in a background thread.
578  FlushRunLoops();
579
580  for (NSString* snapshotID in snapshotIDs_) {
581    base::FilePath path([cache greyImagePathForSnapshotID:snapshotID]);
582    EXPECT_TRUE(base::PathExists(path));
583    base::DeleteFile(path);
584  }
585}
586
587// Verifies that image size and scale are preserved when writing and reading
588// from disk.
589TEST_F(SnapshotCacheTest, SizeAndScalePreservation) {
590  SnapshotCache* cache = GetSnapshotCache();
591
592  // Create an image with the expected snapshot scale.
593  CGFloat scale = [cache snapshotScaleForDevice];
594  UIGraphicsBeginImageContextWithOptions(
595      CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, scale);
596  CGContextRef context = UIGraphicsGetCurrentContext();
597  UIImage* image = GenerateRandomImage(context);
598  UIGraphicsEndImageContext();
599
600  // Add the image to the cache then call handle low memory to ensure the image
601  // is read from disk instead of the in-memory cache.
602  NSString* const kSnapshotID = @"foo";
603  [cache setImage:image withSnapshotID:kSnapshotID];
604  FlushRunLoops();  // ensure the file is written to disk.
605  TriggerMemoryWarning();
606
607  // Retrive the image and have the callback verify the size and scale.
608  __block BOOL callbackComplete = NO;
609  [cache
610      retrieveImageForSnapshotID:kSnapshotID
611                        callback:^(UIImage* imageFromDisk) {
612                          EXPECT_EQ(image.size.width, imageFromDisk.size.width);
613                          EXPECT_EQ(image.size.height,
614                                    imageFromDisk.size.height);
615                          EXPECT_EQ(image.scale, imageFromDisk.scale);
616                          callbackComplete = YES;
617                        }];
618  FlushRunLoops();
619  EXPECT_TRUE(callbackComplete);
620}
621
622// Verifies that retina-scale images are deleted properly.
623TEST_F(SnapshotCacheTest, DeleteRetinaImages) {
624  SnapshotCache* cache = GetSnapshotCache();
625  if ([cache snapshotScaleForDevice] != 2.0) {
626    return;
627  }
628
629  // Create an image with retina scale.
630  UIGraphicsBeginImageContextWithOptions(
631      CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, 2.0);
632  CGContextRef context = UIGraphicsGetCurrentContext();
633  UIImage* image = GenerateRandomImage(context);
634  UIGraphicsEndImageContext();
635
636  // Add the image to the cache then call handle low memory to ensure the image
637  // is read from disk instead of the in-memory cache.
638  NSString* const kSnapshotID = @"foo";
639  [cache setImage:image withSnapshotID:kSnapshotID];
640  FlushRunLoops();  // ensure the file is written to disk.
641  TriggerMemoryWarning();
642
643  // Verify the file was writted with @2x in the file name.
644  base::FilePath retinaFile = [cache imagePathForSnapshotID:kSnapshotID];
645  EXPECT_TRUE(base::PathExists(retinaFile));
646
647  // Delete the image.
648  [cache removeImageWithSnapshotID:kSnapshotID];
649  FlushRunLoops();  // ensure the file is removed.
650
651  EXPECT_FALSE(base::PathExists(retinaFile));
652}
653
654// Tests that image immediately deletes when calling
655// |-removeImageWithSnapshotID:|.
656TEST_F(SnapshotCacheTest, ImageDeleted) {
657  SnapshotCache* cache = GetSnapshotCache();
658  UIImage* image =
659      GenerateRandomImage(CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize));
660  [cache setImage:image withSnapshotID:@"snapshotID"];
661  base::FilePath image_path = [cache imagePathForSnapshotID:@"snapshotID"];
662  [cache removeImageWithSnapshotID:@"snapshotID"];
663  // Give enough time for deletion.
664  FlushRunLoops();
665  EXPECT_FALSE(base::PathExists(image_path));
666}
667
668// Tests that all images are deleted when calling |-removeAllImages|.
669TEST_F(SnapshotCacheTest, AllImagesDeleted) {
670  SnapshotCache* cache = GetSnapshotCache();
671  UIImage* image =
672      GenerateRandomImage(CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize));
673  [cache setImage:image withSnapshotID:@"snapshotID-1"];
674  [cache setImage:image withSnapshotID:@"snapshotID-2"];
675  base::FilePath image_1_path = [cache imagePathForSnapshotID:@"snapshotID-1"];
676  base::FilePath image_2_path = [cache imagePathForSnapshotID:@"snapshotID-2"];
677  [cache removeAllImages];
678  // Give enough time for deletion.
679  FlushRunLoops();
680  EXPECT_FALSE(base::PathExists(image_1_path));
681  EXPECT_FALSE(base::PathExists(image_2_path));
682}
683
684// Tests that observers are notified when a snapshot is cached and removed.
685TEST_F(SnapshotCacheTest, ObserversNotifiedOnSetAndRemoveImage) {
686  SnapshotCache* cache = GetSnapshotCache();
687  FakeSnapshotCacheObserver* observer =
688      [[FakeSnapshotCacheObserver alloc] init];
689  [cache addObserver:observer];
690  EXPECT_NSEQ(nil, observer.lastUpdatedIdentifier);
691  UIImage* image = [testImages_ objectAtIndex:0];
692  NSString* snapshotID = [snapshotIDs_ objectAtIndex:0];
693  [cache setImage:image withSnapshotID:snapshotID];
694  EXPECT_NSEQ(snapshotID, observer.lastUpdatedIdentifier);
695  observer.lastUpdatedIdentifier = nil;
696  [cache removeImageWithSnapshotID:snapshotID];
697  EXPECT_NSEQ(snapshotID, observer.lastUpdatedIdentifier);
698  [cache removeObserver:observer];
699}
700}  // namespace
701