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/location.h"
13 #include "base/mac/bind_objc_block.h"
14 #include "base/mac/scoped_nsautorelease_pool.h"
15 #include "base/ios/ios_util.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 testImages_.reset([[NSMutableArray alloc] initWithCapacity:kSessionCount]);
60 [[NSMutableArray alloc] initWithCapacity:kSessionCount]);
62 CGFloat scale = [SnapshotCache snapshotScaleForDevice];
63 UIGraphicsBeginImageContextWithOptions(
64 CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, scale);
65 CGContextRef context = UIGraphicsGetCurrentContext();
68 for (NSUInteger i = 0; i < kSessionCount; ++i) {
69 UIImage* image = GenerateRandomImage(context);
70 [testImages_ addObject:image];
72 addObject:[NSString stringWithFormat:@"SessionId-%" PRIuNS, i]];
75 UIGraphicsEndImageContext();
80 void TearDown() override {
82 PlatformTest::TearDown();
85 // Generates an image filled with a random color.
86 UIImage* GenerateRandomImage(CGContextRef context) {
87 CGFloat r = rand() / CGFloat(RAND_MAX);
88 CGFloat g = rand() / CGFloat(RAND_MAX);
89 CGFloat b = rand() / CGFloat(RAND_MAX);
90 CGContextSetRGBStrokeColor(context, r, g, b, 1.0);
91 CGContextSetRGBFillColor(context, r, g, b, 1.0);
93 context, CGRectMake(0.0, 0.0, kSnapshotPixelSize, kSnapshotPixelSize));
94 return UIGraphicsGetImageFromCurrentImageContext();
97 // Flushes all the runloops internally used by the snapshot cache.
98 void FlushRunLoops() {
99 base::RunLoop().RunUntilIdle();
100 web::WebThread::GetBlockingPool()->FlushForTesting();
101 base::RunLoop().RunUntilIdle();
104 // This function removes the snapshots both from dictionary and from disk.
105 void ClearDumpedImages() {
106 SnapshotCache* cache = [SnapshotCache sharedInstance];
109 for (sessionID in testSessions_.get())
110 [cache removeImageWithSessionID:sessionID];
113 // The above calls to -removeImageWithSessionID remove both the color
114 // and grey snapshots for each sessionID, if they are on disk. However,
115 // ensure we also get rid of the grey snapshots in memory.
116 [cache removeGreyCache];
118 __block BOOL foundImage = NO;
119 __block NSUInteger numCallbacks = 0;
120 for (sessionID in testSessions_.get()) {
121 base::FilePath path([SnapshotCache imagePathForSessionID:sessionID]);
123 // Checks that the snapshot is not on disk.
124 EXPECT_FALSE(base::PathExists(path));
126 // Check that the snapshot is not in the dictionary.
127 [cache retrieveImageForSessionID:sessionID
128 callback:^(UIImage* image) {
135 // Expect that all the callbacks ran and that none retrieved an image.
137 EXPECT_EQ([testSessions_ count], numCallbacks);
138 EXPECT_FALSE(foundImage);
141 // Loads kSessionCount color images into the cache. If |waitForFilesOnDisk|
142 // is YES, will not return until the images have been written to disk.
143 void LoadAllColorImagesIntoCache(bool waitForFilesOnDisk) {
144 LoadColorImagesIntoCache(kSessionCount, waitForFilesOnDisk);
147 // Loads |count| color images into the cache. If |waitForFilesOnDisk|
148 // is YES, will not return until the images have been written to disk.
149 void LoadColorImagesIntoCache(NSUInteger count, bool waitForFilesOnDisk) {
150 SnapshotCache* cache = [SnapshotCache sharedInstance];
151 // Put color images in the cache.
152 for (NSUInteger i = 0; i < count; ++i) {
153 base::mac::ScopedNSAutoreleasePool pool;
154 UIImage* image = [testImages_ objectAtIndex:i];
155 NSString* sessionID = [testSessions_ objectAtIndex:i];
156 [cache setImage:image withSessionID:sessionID];
158 if (waitForFilesOnDisk) {
160 for (NSUInteger i = 0; i < count; ++i) {
161 // Check that images are on the disk.
162 NSString* sessionID = [testSessions_ objectAtIndex:i];
163 base::FilePath path([SnapshotCache imagePathForSessionID:sessionID]);
164 EXPECT_TRUE(base::PathExists(path));
169 // Waits for the first |count| grey images for sessions in |testSessions_|
170 // to be placed in the cache.
171 void WaitForGreyImagesInCache(NSUInteger count) {
172 SnapshotCache* cache = [SnapshotCache sharedInstance];
174 for (NSUInteger i = 0; i < count; i++)
175 EXPECT_TRUE([cache hasGreyImageInMemory:testSessions_[i]]);
178 // Guesses the order of the color channels in the image.
179 // Supports RGB, BGR, RGBA, BGRA, ARGB, ABGR.
180 // Returns the position of each channel between 0 and 3.
181 void ComputeColorComponents(CGImageRef cgImage,
185 CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
186 CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
187 int byteOrder = bitmapInfo & kCGBitmapByteOrderMask;
193 if (alphaInfo == kCGImageAlphaLast ||
194 alphaInfo == kCGImageAlphaPremultipliedLast ||
195 alphaInfo == kCGImageAlphaNoneSkipLast) {
201 if (byteOrder != kCGBitmapByteOrder32Host) {
202 int lastChannel = (CGImageGetBitsPerPixel(cgImage) == 24) ? 2 : 3;
203 *red = lastChannel - *red;
204 *green = lastChannel - *green;
205 *blue = lastChannel - *blue;
209 const char* GetPixelData(CGImageRef cgImage) {
210 CFDataRef data = CGDataProviderCopyData(CGImageGetDataProvider(cgImage));
211 return reinterpret_cast<const char*>(CFDataGetBytePtr(data));
214 web::TestWebThreadBundle thread_bundle_;
215 base::scoped_nsobject<NSMutableArray> testSessions_;
216 base::scoped_nsobject<NSMutableArray> testImages_;
219 // This test simply put all the snapshots in the cache and then gets them back
220 // As the snapshots are kept in memory, the same pointer can be retrieved.
221 // This test also checks that images are correctly removed from the disk.
222 TEST_F(SnapshotCacheTest, Cache) {
223 // Don't run on tablets because color snapshots are not cached so this test
224 // can't compare the UIImage pointers directly.
229 SnapshotCache* cache = [SnapshotCache sharedInstance];
231 // Put all images in the cache.
232 for (NSUInteger i = 0; i < kSessionCount; ++i) {
233 UIImage* image = [testImages_ objectAtIndex:i];
234 NSString* sessionID = [testSessions_ objectAtIndex:i];
235 [cache setImage:image withSessionID:sessionID];
239 __block NSUInteger numberOfCallbacks = 0;
240 for (NSUInteger i = 0; i < kSessionCount; ++i) {
241 NSString* sessionID = [testSessions_ objectAtIndex:i];
242 UIImage* expectedImage = [testImages_ objectAtIndex:i];
243 EXPECT_TRUE(expectedImage != nil);
244 [cache retrieveImageForSessionID:sessionID
245 callback:^(UIImage* image) {
246 // Images have not been removed from the
247 // dictionnary. We expect the same pointer.
248 EXPECT_EQ(expectedImage, image);
252 EXPECT_EQ(kSessionCount, numberOfCallbacks);
255 // This test puts all the snapshots in the cache and flushes them to disk.
256 // The snapshots are then reloaded from the disk, and the colors are compared.
257 TEST_F(SnapshotCacheTest, SaveToDisk) {
258 SnapshotCache* cache = [SnapshotCache sharedInstance];
260 // Put all images in the cache.
261 for (NSUInteger i = 0; i < kSessionCount; ++i) {
262 UIImage* image = [testImages_ objectAtIndex:i];
263 NSString* sessionID = [testSessions_ objectAtIndex:i];
264 [cache setImage:image withSessionID:sessionID];
268 for (NSUInteger i = 0; i < kSessionCount; ++i) {
269 // Check that images are on the disk.
270 NSString* sessionID = [testSessions_ objectAtIndex:i];
272 base::FilePath path([SnapshotCache imagePathForSessionID:sessionID]);
273 EXPECT_TRUE(base::PathExists(path));
275 // Check image colors by comparing the first pixel against the reference
278 [UIImage imageWithContentsOfFile:base::SysUTF8ToNSString(path.value())];
279 CGImageRef cgImage = [image CGImage];
280 const char* pixels = GetPixelData(cgImage);
283 UIImage* referenceImage = [testImages_ objectAtIndex:i];
284 CGImageRef referenceCgImage = [referenceImage CGImage];
285 const char* referencePixels = GetPixelData(referenceCgImage);
286 EXPECT_TRUE(referencePixels);
288 if (pixels != nil && referencePixels != nil) {
289 // Color components may not be in the same order,
290 // because of writing to disk and reloading.
291 int red, green, blue;
292 ComputeColorComponents(cgImage, &red, &green, &blue);
294 int referenceRed, referenceGreen, referenceBlue;
295 ComputeColorComponents(referenceCgImage, &referenceRed, &referenceGreen,
298 // Colors may not be exactly the same (compression or rounding errors)
299 // thus a small difference is allowed.
300 EXPECT_NEAR(referencePixels[referenceRed], pixels[red], 1);
301 EXPECT_NEAR(referencePixels[referenceGreen], pixels[green], 1);
302 EXPECT_NEAR(referencePixels[referenceBlue], pixels[blue], 1);
307 TEST_F(SnapshotCacheTest, Purge) {
308 SnapshotCache* cache = [SnapshotCache sharedInstance];
310 // Put all images in the cache.
311 for (NSUInteger i = 0; i < kSessionCount; ++i) {
312 UIImage* image = [testImages_ objectAtIndex:i];
313 NSString* sessionID = [testSessions_ objectAtIndex:i];
314 [cache setImage:image withSessionID:sessionID];
317 NSMutableSet* liveSessions = [NSMutableSet setWithCapacity:1];
318 [liveSessions addObject:[testSessions_ objectAtIndex:0]];
321 [cache purgeCacheOlderThan:(base::Time::Now() - base::TimeDelta::FromHours(1))
322 keeping:liveSessions];
325 // Check that nothing has been deleted.
326 for (NSUInteger i = 0; i < kSessionCount; ++i) {
327 // Check that images are on the disk.
328 NSString* sessionID = [testSessions_ objectAtIndex:i];
330 base::FilePath path([SnapshotCache imagePathForSessionID:sessionID]);
331 EXPECT_TRUE(base::PathExists(path));
335 [cache purgeCacheOlderThan:base::Time::Now() keeping:liveSessions];
338 // Check that the file have been deleted.
339 for (NSUInteger i = 0; i < kSessionCount; ++i) {
340 // Check that images are on the disk.
341 NSString* sessionID = [testSessions_ objectAtIndex:i];
343 base::FilePath path([SnapshotCache imagePathForSessionID:sessionID]);
345 EXPECT_TRUE(base::PathExists(path));
347 EXPECT_FALSE(base::PathExists(path));
351 // Loads the color images into the cache, and pins two of them. Ensures that
352 // only the two pinned IDs remain in memory after a call to -handleLowMemory.
353 TEST_F(SnapshotCacheTest, HandleLowMemory) {
354 // TODO(droger): This test fails on iPad iOS8 device: http://crbug.com/455209
355 #if !TARGET_IPHONE_SIMULATOR
356 if (IsIPadIdiom() && base::ios::IsRunningOnIOS8OrLater()) {
357 LOG(WARNING) << "Test disabled on iPad iOS8 device.";
362 LoadAllColorImagesIntoCache(true);
364 SnapshotCache* cache = [SnapshotCache sharedInstance];
366 NSString* firstPinnedID = [testSessions_ objectAtIndex:4];
367 NSString* secondPinnedID = [testSessions_ objectAtIndex:6];
368 NSMutableSet* set = [NSMutableSet set];
369 [set addObject:firstPinnedID];
370 [set addObject:secondPinnedID];
371 cache.pinnedIDs = set;
373 [cache handleLowMemory];
375 BOOL expectedValue = YES;
379 EXPECT_EQ(expectedValue, [cache hasImageInMemory:firstPinnedID]);
380 EXPECT_EQ(expectedValue, [cache hasImageInMemory:secondPinnedID]);
382 NSString* notPinnedID = [testSessions_ objectAtIndex:2];
383 EXPECT_FALSE([cache hasImageInMemory:notPinnedID]);
385 // Wait for the final image to be pulled off disk.
389 // Tests that createGreyCache creates the grey snapshots in the background,
390 // from color images in the in-memory cache. When the grey images are all
391 // loaded into memory, tests that the request to retrieve the grey snapshot
392 // calls the callback immediately.
393 // Disabled on simulators because it sometimes crashes. crbug/421425
394 #if !TARGET_IPHONE_SIMULATOR
395 TEST_F(SnapshotCacheTest, CreateGreyCache) {
396 LoadAllColorImagesIntoCache(true);
398 // Request the creation of a grey image cache for all images.
399 SnapshotCache* cache = [SnapshotCache sharedInstance];
400 [cache createGreyCache:testSessions_];
402 // Wait for them to be put into the grey image cache.
403 WaitForGreyImagesInCache(kSessionCount);
405 __block NSUInteger numberOfCallbacks = 0;
406 for (NSUInteger i = 0; i < kSessionCount; ++i) {
407 NSString* sessionID = [testSessions_ objectAtIndex:i];
408 [cache retrieveGreyImageForSessionID:sessionID
409 callback:^(UIImage* image) {
415 EXPECT_EQ(numberOfCallbacks, kSessionCount);
418 // Same as previous test, except that all the color images are on disk,
419 // rather than in memory.
420 // Disabled due to the greyImage crash. b/8048597
421 TEST_F(SnapshotCacheTest, CreateGreyCacheFromDisk) {
422 LoadAllColorImagesIntoCache(true);
424 // Remove color images from in-memory cache.
425 SnapshotCache* cache = [SnapshotCache sharedInstance];
426 [cache handleLowMemory];
428 // Request the creation of a grey image cache for all images.
429 [cache createGreyCache:testSessions_];
431 // Wait for them to be put into the grey image cache.
432 WaitForGreyImagesInCache(kSessionCount);
434 __block NSUInteger numberOfCallbacks = 0;
435 for (NSUInteger i = 0; i < kSessionCount; ++i) {
436 NSString* sessionID = [testSessions_ objectAtIndex:i];
437 [cache retrieveGreyImageForSessionID:sessionID
438 callback:^(UIImage* image) {
444 EXPECT_EQ(numberOfCallbacks, kSessionCount);
446 #endif // !TARGET_IPHONE_SIMULATOR
448 // Tests mostRecentGreyBlock, which is a block to be called when the most
449 // recently requested grey image is finally loaded.
450 // The test requests three images be cached as grey images. Only the final
451 // callback of the three requests should be called.
452 // Disabled due to the greyImage crash. b/8048597
453 TEST_F(SnapshotCacheTest, MostRecentGreyBlock) {
454 const NSUInteger kNumImages = 3;
455 base::scoped_nsobject<NSMutableArray> sessionIDs(
456 [[NSMutableArray alloc] initWithCapacity:kNumImages]);
457 [sessionIDs addObject:[testSessions_ objectAtIndex:0]];
458 [sessionIDs addObject:[testSessions_ objectAtIndex:1]];
459 [sessionIDs addObject:[testSessions_ objectAtIndex:2]];
461 SnapshotCache* cache = [SnapshotCache sharedInstance];
463 // Put 3 images in the cache.
464 LoadColorImagesIntoCache(kNumImages, true);
465 // Make sure the color images are only on disk, to ensure the background
466 // thread is slow enough to queue up the requests.
467 [cache handleLowMemory];
469 // Enable the grey image cache.
470 [cache createGreyCache:sessionIDs];
472 // Request the grey versions
473 __block BOOL firstCallbackCalled = NO;
474 __block BOOL secondCallbackCalled = NO;
475 __block BOOL thirdCallbackCalled = NO;
476 [cache greyImageForSessionID:[testSessions_ objectAtIndex:0]
477 callback:^(UIImage*) {
478 firstCallbackCalled = YES;
480 [cache greyImageForSessionID:[testSessions_ objectAtIndex:1]
481 callback:^(UIImage*) {
482 secondCallbackCalled = YES;
484 [cache greyImageForSessionID:[testSessions_ objectAtIndex:2]
485 callback:^(UIImage*) {
486 thirdCallbackCalled = YES;
489 // Wait for them to be loaded.
490 WaitForGreyImagesInCache(kNumImages);
492 EXPECT_FALSE(firstCallbackCalled);
493 EXPECT_FALSE(secondCallbackCalled);
494 EXPECT_TRUE(thirdCallbackCalled);
497 // Test the function used to save a grey copy of a color snapshot fully on a
498 // background thread when the application is backgrounded.
499 // Disabled due to the greyImage crash. b/8048597
500 TEST_F(SnapshotCacheTest, GreyImageAllInBackground) {
501 LoadAllColorImagesIntoCache(true);
503 SnapshotCache* cache = [SnapshotCache sharedInstance];
505 // Now convert every image into a grey image, on disk, in the background.
506 for (NSUInteger i = 0; i < kSessionCount; ++i) {
507 [cache saveGreyInBackgroundForSessionID:[testSessions_ objectAtIndex:i]];
510 // Waits for the grey images for the sessions in |testSessions_| to be written
511 // to disk, which happens in a background thread.
514 for (NSString* sessionID in testSessions_.get()) {
515 base::FilePath path([SnapshotCache greyImagePathForSessionID:sessionID]);
516 EXPECT_TRUE(base::PathExists(path));
517 base::DeleteFile(path, false);
521 // Verifies that image size and scale are preserved when writing and reading
523 TEST_F(SnapshotCacheTest, SizeAndScalePreservation) {
524 // Create an image with the expected snapshot scale.
525 CGFloat scale = [SnapshotCache snapshotScaleForDevice];
526 UIGraphicsBeginImageContextWithOptions(
527 CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, scale);
528 CGContextRef context = UIGraphicsGetCurrentContext();
529 UIImage* image = GenerateRandomImage(context);
530 UIGraphicsEndImageContext();
532 // Add the image to the cache then call handle low memory to ensure the image
533 // is read from disk instead of the in-memory cache.
534 SnapshotCache* cache = [SnapshotCache sharedInstance];
535 NSString* const kSession = @"foo";
536 [cache setImage:image withSessionID:kSession];
537 FlushRunLoops(); // ensure the file is written to disk.
538 [cache handleLowMemory];
540 // Retrive the image and have the callback verify the size and scale.
541 __block BOOL callbackComplete = NO;
542 [cache retrieveImageForSessionID:kSession
543 callback:^(UIImage* imageFromDisk) {
544 EXPECT_EQ(image.size.width,
545 imageFromDisk.size.width);
546 EXPECT_EQ(image.size.height,
547 imageFromDisk.size.height);
548 EXPECT_EQ(image.scale, imageFromDisk.scale);
549 callbackComplete = YES;
552 EXPECT_TRUE(callbackComplete);
555 // Verifies that retina-scale images are deleted properly.
556 TEST_F(SnapshotCacheTest, DeleteRetinaImages) {
557 if ([SnapshotCache snapshotScaleForDevice] != 2.0) {
561 // Create an image with retina scale.
562 UIGraphicsBeginImageContextWithOptions(
563 CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, 2.0);
564 CGContextRef context = UIGraphicsGetCurrentContext();
565 UIImage* image = GenerateRandomImage(context);
566 UIGraphicsEndImageContext();
568 // Add the image to the cache then call handle low memory to ensure the image
569 // is read from disk instead of the in-memory cache.
570 SnapshotCache* cache = [SnapshotCache sharedInstance];
571 NSString* const kSession = @"foo";
572 [cache setImage:image withSessionID:kSession];
573 FlushRunLoops(); // ensure the file is written to disk.
574 [cache handleLowMemory];
576 // Verify the file was writted with @2x in the file name.
577 base::FilePath retinaFile = [SnapshotCache imagePathForSessionID:kSession];
578 EXPECT_TRUE(base::PathExists(retinaFile));
581 [cache removeImageWithSessionID:kSession];
582 FlushRunLoops(); // ensure the file is removed.
584 EXPECT_FALSE(base::PathExists(retinaFile));