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.
5 #import "ios/chrome/browser/snapshots/snapshot_cache.h"
7 #import <Foundation/Foundation.h>
9 #include "base/files/file_path.h"
10 #include "base/files/file_util.h"
11 #include "base/format_macros.h"
12 #include "base/ios/ios_util.h"
13 #include "base/location.h"
14 #include "base/mac/bind_objc_block.h"
15 #include "base/mac/scoped_nsautorelease_pool.h"
16 #include "base/run_loop.h"
17 #include "base/strings/sys_string_conversions.h"
18 #include "base/time/time.h"
19 #include "ios/chrome/browser/ui/ui_util.h"
20 #include "ios/web/public/test/test_web_thread_bundle.h"
21 #include "ios/web/public/web_thread.h"
22 #include "testing/gtest/include/gtest/gtest.h"
23 #include "testing/gtest_mac.h"
24 #include "testing/platform_test.h"
26 static const NSUInteger kSessionCount = 10;
27 static const NSUInteger kSnapshotPixelSize = 8;
29 // Promote some implementation methods to public.
30 @interface SnapshotCache (Testing)
31 + (base::FilePath)imagePathForSessionID:(NSString*)sessionID;
32 + (base::FilePath)greyImagePathForSessionID:(NSString*)sessionID;
33 - (void)handleLowMemory;
36 @interface SnapshotCache (TestingAdditions)
37 - (BOOL)hasImageInMemory:(NSString*)sessionID;
38 - (BOOL)hasGreyImageInMemory:(NSString*)sessionID;
41 @implementation SnapshotCache (TestingAdditions)
42 - (BOOL)hasImageInMemory:(NSString*)sessionID {
43 return [imageDictionary_ objectForKey:sessionID] != nil;
45 - (BOOL)hasGreyImageInMemory:(NSString*)sessionID {
46 return [greyImageDictionary_ objectForKey:sessionID] != nil;
52 class SnapshotCacheTest : public PlatformTest {
54 // Build an array of session names and an array of UIImages filled with
56 void SetUp() override {
57 PlatformTest::SetUp();
58 snapshotCache_.reset([[SnapshotCache alloc] init]);
59 testImages_.reset([[NSMutableArray alloc] initWithCapacity:kSessionCount]);
61 [[NSMutableArray alloc] initWithCapacity:kSessionCount]);
63 CGFloat scale = [SnapshotCache snapshotScaleForDevice];
64 UIGraphicsBeginImageContextWithOptions(
65 CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, scale);
66 CGContextRef context = UIGraphicsGetCurrentContext();
69 for (NSUInteger i = 0; i < kSessionCount; ++i) {
70 UIImage* image = GenerateRandomImage(context);
71 [testImages_ addObject:image];
73 addObject:[NSString stringWithFormat:@"SessionId-%" PRIuNS, i]];
76 UIGraphicsEndImageContext();
81 void TearDown() override {
83 snapshotCache_.reset();
84 PlatformTest::TearDown();
87 SnapshotCache* GetSnapshotCache() {
88 return snapshotCache_.get();
91 // Generates an image filled with a random color.
92 UIImage* GenerateRandomImage(CGContextRef context) {
93 CGFloat r = rand() / CGFloat(RAND_MAX);
94 CGFloat g = rand() / CGFloat(RAND_MAX);
95 CGFloat b = rand() / CGFloat(RAND_MAX);
96 CGContextSetRGBStrokeColor(context, r, g, b, 1.0);
97 CGContextSetRGBFillColor(context, r, g, b, 1.0);
99 context, CGRectMake(0.0, 0.0, kSnapshotPixelSize, kSnapshotPixelSize));
100 return UIGraphicsGetImageFromCurrentImageContext();
103 // Flushes all the runloops internally used by the snapshot cache.
104 void FlushRunLoops() {
105 base::RunLoop().RunUntilIdle();
106 web::WebThread::GetBlockingPool()->FlushForTesting();
107 base::RunLoop().RunUntilIdle();
110 // This function removes the snapshots both from dictionary and from disk.
111 void ClearDumpedImages() {
112 SnapshotCache* cache = GetSnapshotCache();
115 for (sessionID in testSessions_.get())
116 [cache removeImageWithSessionID:sessionID];
119 // The above calls to -removeImageWithSessionID remove both the color
120 // and grey snapshots for each sessionID, if they are on disk. However,
121 // ensure we also get rid of the grey snapshots in memory.
122 [cache removeGreyCache];
124 __block BOOL foundImage = NO;
125 __block NSUInteger numCallbacks = 0;
126 for (sessionID in testSessions_.get()) {
127 base::FilePath path([SnapshotCache imagePathForSessionID:sessionID]);
129 // Checks that the snapshot is not on disk.
130 EXPECT_FALSE(base::PathExists(path));
132 // Check that the snapshot is not in the dictionary.
133 [cache retrieveImageForSessionID:sessionID
134 callback:^(UIImage* image) {
141 // Expect that all the callbacks ran and that none retrieved an image.
143 EXPECT_EQ([testSessions_ count], numCallbacks);
144 EXPECT_FALSE(foundImage);
147 // Loads kSessionCount color images into the cache. If |waitForFilesOnDisk|
148 // is YES, will not return until the images have been written to disk.
149 void LoadAllColorImagesIntoCache(bool waitForFilesOnDisk) {
150 LoadColorImagesIntoCache(kSessionCount, waitForFilesOnDisk);
153 // Loads |count| color images into the cache. If |waitForFilesOnDisk|
154 // is YES, will not return until the images have been written to disk.
155 void LoadColorImagesIntoCache(NSUInteger count, bool waitForFilesOnDisk) {
156 SnapshotCache* cache = GetSnapshotCache();
157 // Put color images in the cache.
158 for (NSUInteger i = 0; i < count; ++i) {
159 base::mac::ScopedNSAutoreleasePool pool;
160 UIImage* image = [testImages_ objectAtIndex:i];
161 NSString* sessionID = [testSessions_ objectAtIndex:i];
162 [cache setImage:image withSessionID:sessionID];
164 if (waitForFilesOnDisk) {
166 for (NSUInteger i = 0; i < count; ++i) {
167 // Check that images are on the disk.
168 NSString* sessionID = [testSessions_ objectAtIndex:i];
169 base::FilePath path([SnapshotCache imagePathForSessionID:sessionID]);
170 EXPECT_TRUE(base::PathExists(path));
175 // Waits for the first |count| grey images for sessions in |testSessions_|
176 // to be placed in the cache.
177 void WaitForGreyImagesInCache(NSUInteger count) {
178 SnapshotCache* cache = GetSnapshotCache();
180 for (NSUInteger i = 0; i < count; i++)
181 EXPECT_TRUE([cache hasGreyImageInMemory:testSessions_[i]]);
184 // Guesses the order of the color channels in the image.
185 // Supports RGB, BGR, RGBA, BGRA, ARGB, ABGR.
186 // Returns the position of each channel between 0 and 3.
187 void ComputeColorComponents(CGImageRef cgImage,
191 CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
192 CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
193 int byteOrder = bitmapInfo & kCGBitmapByteOrderMask;
199 if (alphaInfo == kCGImageAlphaLast ||
200 alphaInfo == kCGImageAlphaPremultipliedLast ||
201 alphaInfo == kCGImageAlphaNoneSkipLast) {
207 if (byteOrder != kCGBitmapByteOrder32Host) {
208 int lastChannel = (CGImageGetBitsPerPixel(cgImage) == 24) ? 2 : 3;
209 *red = lastChannel - *red;
210 *green = lastChannel - *green;
211 *blue = lastChannel - *blue;
215 const char* GetPixelData(CGImageRef cgImage) {
216 CFDataRef data = CGDataProviderCopyData(CGImageGetDataProvider(cgImage));
217 return reinterpret_cast<const char*>(CFDataGetBytePtr(data));
220 web::TestWebThreadBundle thread_bundle_;
221 base::scoped_nsobject<SnapshotCache> snapshotCache_;
222 base::scoped_nsobject<NSMutableArray> testSessions_;
223 base::scoped_nsobject<NSMutableArray> testImages_;
226 // This test simply put all the snapshots in the cache and then gets them back
227 // As the snapshots are kept in memory, the same pointer can be retrieved.
228 // This test also checks that images are correctly removed from the disk.
229 TEST_F(SnapshotCacheTest, Cache) {
230 // Don't run on tablets because color snapshots are not cached so this test
231 // can't compare the UIImage pointers directly.
236 SnapshotCache* cache = GetSnapshotCache();
238 // Put all images in the cache.
239 for (NSUInteger i = 0; i < kSessionCount; ++i) {
240 UIImage* image = [testImages_ objectAtIndex:i];
241 NSString* sessionID = [testSessions_ objectAtIndex:i];
242 [cache setImage:image withSessionID:sessionID];
246 __block NSUInteger numberOfCallbacks = 0;
247 for (NSUInteger i = 0; i < kSessionCount; ++i) {
248 NSString* sessionID = [testSessions_ objectAtIndex:i];
249 UIImage* expectedImage = [testImages_ objectAtIndex:i];
250 EXPECT_TRUE(expectedImage != nil);
251 [cache retrieveImageForSessionID:sessionID
252 callback:^(UIImage* image) {
253 // Images have not been removed from the
254 // dictionnary. We expect the same pointer.
255 EXPECT_EQ(expectedImage, image);
259 EXPECT_EQ(kSessionCount, numberOfCallbacks);
262 // This test puts all the snapshots in the cache and flushes them to disk.
263 // The snapshots are then reloaded from the disk, and the colors are compared.
264 TEST_F(SnapshotCacheTest, SaveToDisk) {
265 SnapshotCache* cache = GetSnapshotCache();
267 // Put all images in the cache.
268 for (NSUInteger i = 0; i < kSessionCount; ++i) {
269 UIImage* image = [testImages_ objectAtIndex:i];
270 NSString* sessionID = [testSessions_ objectAtIndex:i];
271 [cache setImage:image withSessionID:sessionID];
275 for (NSUInteger i = 0; i < kSessionCount; ++i) {
276 // Check that images are on the disk.
277 NSString* sessionID = [testSessions_ objectAtIndex:i];
279 base::FilePath path([SnapshotCache imagePathForSessionID:sessionID]);
280 EXPECT_TRUE(base::PathExists(path));
282 // Check image colors by comparing the first pixel against the reference
285 [UIImage imageWithContentsOfFile:base::SysUTF8ToNSString(path.value())];
286 CGImageRef cgImage = [image CGImage];
287 const char* pixels = GetPixelData(cgImage);
290 UIImage* referenceImage = [testImages_ objectAtIndex:i];
291 CGImageRef referenceCgImage = [referenceImage CGImage];
292 const char* referencePixels = GetPixelData(referenceCgImage);
293 EXPECT_TRUE(referencePixels);
295 if (pixels != nil && referencePixels != nil) {
296 // Color components may not be in the same order,
297 // because of writing to disk and reloading.
298 int red, green, blue;
299 ComputeColorComponents(cgImage, &red, &green, &blue);
301 int referenceRed, referenceGreen, referenceBlue;
302 ComputeColorComponents(referenceCgImage, &referenceRed, &referenceGreen,
305 // Colors may not be exactly the same (compression or rounding errors)
306 // thus a small difference is allowed.
307 EXPECT_NEAR(referencePixels[referenceRed], pixels[red], 1);
308 EXPECT_NEAR(referencePixels[referenceGreen], pixels[green], 1);
309 EXPECT_NEAR(referencePixels[referenceBlue], pixels[blue], 1);
314 TEST_F(SnapshotCacheTest, Purge) {
315 SnapshotCache* cache = GetSnapshotCache();
317 // Put all images in the cache.
318 for (NSUInteger i = 0; i < kSessionCount; ++i) {
319 UIImage* image = [testImages_ objectAtIndex:i];
320 NSString* sessionID = [testSessions_ objectAtIndex:i];
321 [cache setImage:image withSessionID:sessionID];
324 NSMutableSet* liveSessions = [NSMutableSet setWithCapacity:1];
325 [liveSessions addObject:[testSessions_ objectAtIndex:0]];
328 [cache purgeCacheOlderThan:(base::Time::Now() - base::TimeDelta::FromHours(1))
329 keeping:liveSessions];
332 // Check that nothing has been deleted.
333 for (NSUInteger i = 0; i < kSessionCount; ++i) {
334 // Check that images are on the disk.
335 NSString* sessionID = [testSessions_ objectAtIndex:i];
337 base::FilePath path([SnapshotCache imagePathForSessionID:sessionID]);
338 EXPECT_TRUE(base::PathExists(path));
342 [cache purgeCacheOlderThan:base::Time::Now() keeping:liveSessions];
345 // Check that the file have been deleted.
346 for (NSUInteger i = 0; i < kSessionCount; ++i) {
347 // Check that images are on the disk.
348 NSString* sessionID = [testSessions_ objectAtIndex:i];
350 base::FilePath path([SnapshotCache imagePathForSessionID:sessionID]);
352 EXPECT_TRUE(base::PathExists(path));
354 EXPECT_FALSE(base::PathExists(path));
358 // Loads the color images into the cache, and pins two of them. Ensures that
359 // only the two pinned IDs remain in memory after a call to -handleLowMemory.
360 TEST_F(SnapshotCacheTest, HandleLowMemory) {
361 // TODO(droger): This test fails on iPad iOS8 device: http://crbug.com/455209
362 #if !TARGET_IPHONE_SIMULATOR
363 if (IsIPadIdiom() && base::ios::IsRunningOnIOS8OrLater()) {
364 LOG(WARNING) << "Test disabled on iPad iOS8 device.";
369 LoadAllColorImagesIntoCache(true);
371 SnapshotCache* cache = GetSnapshotCache();
373 NSString* firstPinnedID = [testSessions_ objectAtIndex:4];
374 NSString* secondPinnedID = [testSessions_ objectAtIndex:6];
375 NSMutableSet* set = [NSMutableSet set];
376 [set addObject:firstPinnedID];
377 [set addObject:secondPinnedID];
378 cache.pinnedIDs = set;
381 [cache handleLowMemory];
383 BOOL expectedValue = YES;
387 EXPECT_EQ(expectedValue, [cache hasImageInMemory:firstPinnedID]);
388 EXPECT_EQ(expectedValue, [cache hasImageInMemory:secondPinnedID]);
390 NSString* notPinnedID = [testSessions_ objectAtIndex:2];
391 EXPECT_FALSE([cache hasImageInMemory:notPinnedID]);
393 // Wait for the final image to be pulled off disk.
397 // Tests that createGreyCache creates the grey snapshots in the background,
398 // from color images in the in-memory cache. When the grey images are all
399 // loaded into memory, tests that the request to retrieve the grey snapshot
400 // calls the callback immediately.
401 // Disabled on simulators because it sometimes crashes. crbug/421425
402 #if !TARGET_IPHONE_SIMULATOR
403 TEST_F(SnapshotCacheTest, CreateGreyCache) {
404 LoadAllColorImagesIntoCache(true);
406 // Request the creation of a grey image cache for all images.
407 SnapshotCache* cache = GetSnapshotCache();
408 [cache createGreyCache:testSessions_];
410 // Wait for them to be put into the grey image cache.
411 WaitForGreyImagesInCache(kSessionCount);
413 __block NSUInteger numberOfCallbacks = 0;
414 for (NSUInteger i = 0; i < kSessionCount; ++i) {
415 NSString* sessionID = [testSessions_ objectAtIndex:i];
416 [cache retrieveGreyImageForSessionID:sessionID
417 callback:^(UIImage* image) {
423 EXPECT_EQ(numberOfCallbacks, kSessionCount);
426 // Same as previous test, except that all the color images are on disk,
427 // rather than in memory.
428 // Disabled due to the greyImage crash. b/8048597
429 TEST_F(SnapshotCacheTest, CreateGreyCacheFromDisk) {
430 LoadAllColorImagesIntoCache(true);
432 // Remove color images from in-memory cache.
433 SnapshotCache* cache = GetSnapshotCache();
436 [cache handleLowMemory];
438 // Request the creation of a grey image cache for all images.
439 [cache createGreyCache:testSessions_];
441 // Wait for them to be put into the grey image cache.
442 WaitForGreyImagesInCache(kSessionCount);
444 __block NSUInteger numberOfCallbacks = 0;
445 for (NSUInteger i = 0; i < kSessionCount; ++i) {
446 NSString* sessionID = [testSessions_ objectAtIndex:i];
447 [cache retrieveGreyImageForSessionID:sessionID
448 callback:^(UIImage* image) {
454 EXPECT_EQ(numberOfCallbacks, kSessionCount);
456 #endif // !TARGET_IPHONE_SIMULATOR
458 // Tests mostRecentGreyBlock, which is a block to be called when the most
459 // recently requested grey image is finally loaded.
460 // The test requests three images be cached as grey images. Only the final
461 // callback of the three requests should be called.
462 // Disabled due to the greyImage crash. b/8048597
463 TEST_F(SnapshotCacheTest, MostRecentGreyBlock) {
464 const NSUInteger kNumImages = 3;
465 base::scoped_nsobject<NSMutableArray> sessionIDs(
466 [[NSMutableArray alloc] initWithCapacity:kNumImages]);
467 [sessionIDs addObject:[testSessions_ objectAtIndex:0]];
468 [sessionIDs addObject:[testSessions_ objectAtIndex:1]];
469 [sessionIDs addObject:[testSessions_ objectAtIndex:2]];
471 SnapshotCache* cache = GetSnapshotCache();
473 // Put 3 images in the cache.
474 LoadColorImagesIntoCache(kNumImages, true);
475 // Make sure the color images are only on disk, to ensure the background
476 // thread is slow enough to queue up the requests.
478 [cache handleLowMemory];
480 // Enable the grey image cache.
481 [cache createGreyCache:sessionIDs];
483 // Request the grey versions
484 __block BOOL firstCallbackCalled = NO;
485 __block BOOL secondCallbackCalled = NO;
486 __block BOOL thirdCallbackCalled = NO;
487 [cache greyImageForSessionID:[testSessions_ objectAtIndex:0]
488 callback:^(UIImage*) {
489 firstCallbackCalled = YES;
491 [cache greyImageForSessionID:[testSessions_ objectAtIndex:1]
492 callback:^(UIImage*) {
493 secondCallbackCalled = YES;
495 [cache greyImageForSessionID:[testSessions_ objectAtIndex:2]
496 callback:^(UIImage*) {
497 thirdCallbackCalled = YES;
500 // Wait for them to be loaded.
501 WaitForGreyImagesInCache(kNumImages);
503 EXPECT_FALSE(firstCallbackCalled);
504 EXPECT_FALSE(secondCallbackCalled);
505 EXPECT_TRUE(thirdCallbackCalled);
508 // Test the function used to save a grey copy of a color snapshot fully on a
509 // background thread when the application is backgrounded.
510 // Disabled due to the greyImage crash. b/8048597
511 TEST_F(SnapshotCacheTest, GreyImageAllInBackground) {
512 LoadAllColorImagesIntoCache(true);
514 SnapshotCache* cache = GetSnapshotCache();
516 // Now convert every image into a grey image, on disk, in the background.
517 for (NSUInteger i = 0; i < kSessionCount; ++i) {
518 [cache saveGreyInBackgroundForSessionID:[testSessions_ objectAtIndex:i]];
521 // Waits for the grey images for the sessions in |testSessions_| to be written
522 // to disk, which happens in a background thread.
525 for (NSString* sessionID in testSessions_.get()) {
526 base::FilePath path([SnapshotCache greyImagePathForSessionID:sessionID]);
527 EXPECT_TRUE(base::PathExists(path));
528 base::DeleteFile(path, false);
532 // Verifies that image size and scale are preserved when writing and reading
534 TEST_F(SnapshotCacheTest, SizeAndScalePreservation) {
535 // Create an image with the expected snapshot scale.
536 CGFloat scale = [SnapshotCache snapshotScaleForDevice];
537 UIGraphicsBeginImageContextWithOptions(
538 CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, scale);
539 CGContextRef context = UIGraphicsGetCurrentContext();
540 UIImage* image = GenerateRandomImage(context);
541 UIGraphicsEndImageContext();
543 // Add the image to the cache then call handle low memory to ensure the image
544 // is read from disk instead of the in-memory cache.
545 SnapshotCache* cache = GetSnapshotCache();
546 NSString* const kSession = @"foo";
547 [cache setImage:image withSessionID:kSession];
548 FlushRunLoops(); // ensure the file is written to disk.
550 [cache handleLowMemory];
552 // Retrive the image and have the callback verify the size and scale.
553 __block BOOL callbackComplete = NO;
554 [cache retrieveImageForSessionID:kSession
555 callback:^(UIImage* imageFromDisk) {
556 EXPECT_EQ(image.size.width,
557 imageFromDisk.size.width);
558 EXPECT_EQ(image.size.height,
559 imageFromDisk.size.height);
560 EXPECT_EQ(image.scale, imageFromDisk.scale);
561 callbackComplete = YES;
564 EXPECT_TRUE(callbackComplete);
567 // Verifies that retina-scale images are deleted properly.
568 TEST_F(SnapshotCacheTest, DeleteRetinaImages) {
569 if ([SnapshotCache snapshotScaleForDevice] != 2.0) {
573 // Create an image with retina scale.
574 UIGraphicsBeginImageContextWithOptions(
575 CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, 2.0);
576 CGContextRef context = UIGraphicsGetCurrentContext();
577 UIImage* image = GenerateRandomImage(context);
578 UIGraphicsEndImageContext();
580 // Add the image to the cache then call handle low memory to ensure the image
581 // is read from disk instead of the in-memory cache.
582 SnapshotCache* cache = GetSnapshotCache();
583 NSString* const kSession = @"foo";
584 [cache setImage:image withSessionID:kSession];
585 FlushRunLoops(); // ensure the file is written to disk.
587 [cache handleLowMemory];
589 // Verify the file was writted with @2x in the file name.
590 base::FilePath retinaFile = [SnapshotCache imagePathForSessionID:kSession];
591 EXPECT_TRUE(base::PathExists(retinaFile));
594 [cache removeImageWithSessionID:kSession];
595 FlushRunLoops(); // ensure the file is removed.
597 EXPECT_FALSE(base::PathExists(retinaFile));