Give names to all utility processes.
[chromium-blink-merge.git] / ios / chrome / browser / snapshots / snapshot_cache_unittest.mm
blob728bbcc6ff61b6ee7a573a67b961d5b256325ace
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;
34 @end
36 @interface SnapshotCache (TestingAdditions)
37 - (BOOL)hasImageInMemory:(NSString*)sessionID;
38 - (BOOL)hasGreyImageInMemory:(NSString*)sessionID;
39 @end
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;
48 @end
50 namespace {
52 class SnapshotCacheTest : public PlatformTest {
53  protected:
54   // Build an array of session names and an array of UIImages filled with
55   // random colors.
56   void SetUp() override {
57     PlatformTest::SetUp();
58     testImages_.reset([[NSMutableArray alloc] initWithCapacity:kSessionCount]);
59     testSessions_.reset(
60         [[NSMutableArray alloc] initWithCapacity:kSessionCount]);
62     CGFloat scale = [SnapshotCache snapshotScaleForDevice];
63     UIGraphicsBeginImageContextWithOptions(
64         CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, scale);
65     CGContextRef context = UIGraphicsGetCurrentContext();
66     srand(1);
68     for (NSUInteger i = 0; i < kSessionCount; ++i) {
69       UIImage* image = GenerateRandomImage(context);
70       [testImages_ addObject:image];
71       [testSessions_
72           addObject:[NSString stringWithFormat:@"SessionId-%" PRIuNS, i]];
73     }
75     UIGraphicsEndImageContext();
77     ClearDumpedImages();
78   }
80   void TearDown() override {
81     ClearDumpedImages();
82     PlatformTest::TearDown();
83   }
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);
92     CGContextFillRect(
93         context, CGRectMake(0.0, 0.0, kSnapshotPixelSize, kSnapshotPixelSize));
94     return UIGraphicsGetImageFromCurrentImageContext();
95   }
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();
102   }
104   // This function removes the snapshots both from dictionary and from disk.
105   void ClearDumpedImages() {
106     SnapshotCache* cache = [SnapshotCache sharedInstance];
108     NSString* sessionID;
109     for (sessionID in testSessions_.get())
110       [cache removeImageWithSessionID:sessionID];
112     FlushRunLoops();
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) {
129                                 ++numCallbacks;
130                                 if (image)
131                                   foundImage = YES;
132                               }];
133     }
135     // Expect that all the callbacks ran and that none retrieved an image.
136     FlushRunLoops();
137     EXPECT_EQ([testSessions_ count], numCallbacks);
138     EXPECT_FALSE(foundImage);
139   }
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);
145   }
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];
157     }
158     if (waitForFilesOnDisk) {
159       FlushRunLoops();
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));
165       }
166     }
167   }
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];
173     FlushRunLoops();
174     for (NSUInteger i = 0; i < count; i++)
175       EXPECT_TRUE([cache hasGreyImageInMemory:testSessions_[i]]);
176   }
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,
182                               int* red,
183                               int* green,
184                               int* blue) {
185     CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
186     CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
187     int byteOrder = bitmapInfo & kCGBitmapByteOrderMask;
189     *red = 0;
190     *green = 1;
191     *blue = 2;
193     if (alphaInfo == kCGImageAlphaLast ||
194         alphaInfo == kCGImageAlphaPremultipliedLast ||
195         alphaInfo == kCGImageAlphaNoneSkipLast) {
196       *red = 1;
197       *green = 2;
198       *blue = 3;
199     }
201     if (byteOrder != kCGBitmapByteOrder32Host) {
202       int lastChannel = (CGImageGetBitsPerPixel(cgImage) == 24) ? 2 : 3;
203       *red = lastChannel - *red;
204       *green = lastChannel - *green;
205       *blue = lastChannel - *blue;
206     }
207   }
209   const char* GetPixelData(CGImageRef cgImage) {
210     CFDataRef data = CGDataProviderCopyData(CGImageGetDataProvider(cgImage));
211     return reinterpret_cast<const char*>(CFDataGetBytePtr(data));
212   }
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.
225   if (IsIPadIdiom()) {
226     return;
227   }
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];
236   }
238   // Get images back.
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);
249                               ++numberOfCallbacks;
250                             }];
251   }
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];
265   }
266   FlushRunLoops();
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
276     // image.
277     UIImage* image =
278         [UIImage imageWithContentsOfFile:base::SysUTF8ToNSString(path.value())];
279     CGImageRef cgImage = [image CGImage];
280     const char* pixels = GetPixelData(cgImage);
281     EXPECT_TRUE(pixels);
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,
296                              &referenceBlue);
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);
303     }
304   }
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];
315   }
317   NSMutableSet* liveSessions = [NSMutableSet setWithCapacity:1];
318   [liveSessions addObject:[testSessions_ objectAtIndex:0]];
320   // Purge the cache.
321   [cache purgeCacheOlderThan:(base::Time::Now() - base::TimeDelta::FromHours(1))
322                      keeping:liveSessions];
323   FlushRunLoops();
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));
332   }
334   // Purge the cache.
335   [cache purgeCacheOlderThan:base::Time::Now() keeping:liveSessions];
336   FlushRunLoops();
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]);
344     if (i == 0)
345       EXPECT_TRUE(base::PathExists(path));
346     else
347       EXPECT_FALSE(base::PathExists(path));
348   }
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.";
358     return;
359   }
360 #endif
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;
376   if (IsIPadIdiom()) {
377     expectedValue = NO;
378   }
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.
386   FlushRunLoops();
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) {
410                                   EXPECT_TRUE(image);
411                                   ++numberOfCallbacks;
412                                 }];
413   }
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) {
439                                   EXPECT_TRUE(image);
440                                   ++numberOfCallbacks;
441                                 }];
442   }
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;
479                       }];
480   [cache greyImageForSessionID:[testSessions_ objectAtIndex:1]
481                       callback:^(UIImage*) {
482                         secondCallbackCalled = YES;
483                       }];
484   [cache greyImageForSessionID:[testSessions_ objectAtIndex:2]
485                       callback:^(UIImage*) {
486                         thirdCallbackCalled = YES;
487                       }];
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]];
508   }
510   // Waits for the grey images for the sessions in |testSessions_| to be written
511   // to disk, which happens in a background thread.
512   FlushRunLoops();
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);
518   }
521 // Verifies that image size and scale are preserved when writing and reading
522 // from disk.
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;
550                           }];
551   FlushRunLoops();
552   EXPECT_TRUE(callbackComplete);
555 // Verifies that retina-scale images are deleted properly.
556 TEST_F(SnapshotCacheTest, DeleteRetinaImages) {
557   if ([SnapshotCache snapshotScaleForDevice] != 2.0) {
558     return;
559   }
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));
580   // Delete the image.
581   [cache removeImageWithSessionID:kSession];
582   FlushRunLoops();  // ensure the file is removed.
584   EXPECT_FALSE(base::PathExists(retinaFile));
587 }  // namespace