blob: 978bca28485fec196a89e69f9066d0cd5aa4545f [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/snapshots/snapshot_cache.h"
#import <Foundation/Foundation.h>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/format_macros.h"
#include "base/location.h"
#include "base/mac/bind_objc_block.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/run_loop.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task_scheduler/task_scheduler.h"
#include "base/threading/sequenced_worker_pool.h"
#include "base/time/time.h"
#import "ios/chrome/browser/snapshots/snapshot_cache_internal.h"
#include "ios/web/public/test/test_web_thread_bundle.h"
#include "ios/web/public/web_thread.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "testing/platform_test.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
static const NSUInteger kSessionCount = 10;
static const NSUInteger kSnapshotPixelSize = 8;
namespace {
class SnapshotCacheTest : public PlatformTest {
protected:
// Build an array of session names and an array of UIImages filled with
// random colors.
void SetUp() override {
PlatformTest::SetUp();
snapshotCache_ = [[SnapshotCache alloc] init];
testImages_ = [[NSMutableArray alloc] initWithCapacity:kSessionCount];
testSessions_ = [[NSMutableArray alloc] initWithCapacity:kSessionCount];
CGFloat scale = [snapshotCache_ snapshotScaleForDevice];
UIGraphicsBeginImageContextWithOptions(
CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
srand(1);
for (NSUInteger i = 0; i < kSessionCount; ++i) {
UIImage* image = GenerateRandomImage(context);
[testImages_ addObject:image];
[testSessions_
addObject:[NSString stringWithFormat:@"SessionId-%" PRIuNS, i]];
}
UIGraphicsEndImageContext();
ClearDumpedImages();
}
void TearDown() override {
ClearDumpedImages();
[snapshotCache_ shutdown];
snapshotCache_ = nil;
PlatformTest::TearDown();
}
SnapshotCache* GetSnapshotCache() { return snapshotCache_; }
// Generates an image filled with a random color.
UIImage* GenerateRandomImage(CGContextRef context) {
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));
return UIGraphicsGetImageFromCurrentImageContext();
}
// Flushes all the runloops internally used by the snapshot cache.
void FlushRunLoops() {
base::TaskScheduler::GetInstance()->FlushForTesting();
base::RunLoop().RunUntilIdle();
}
// This function removes the snapshots both from dictionary and from disk.
void ClearDumpedImages() {
SnapshotCache* cache = GetSnapshotCache();
NSString* sessionID;
for (sessionID in testSessions_)
[cache removeImageWithSessionID:sessionID];
FlushRunLoops();
// The above calls to -removeImageWithSessionID remove both the color
// and grey snapshots for each sessionID, if they are on disk. However,
// ensure we also get rid of the grey snapshots in memory.
[cache removeGreyCache];
__block BOOL foundImage = NO;
__block NSUInteger numCallbacks = 0;
for (sessionID in testSessions_) {
base::FilePath path([cache imagePathForSessionID:sessionID]);
// Checks that the snapshot is not on disk.
EXPECT_FALSE(base::PathExists(path));
// Check that the snapshot is not in the dictionary.
[cache retrieveImageForSessionID:sessionID
callback:^(UIImage* image) {
++numCallbacks;
if (image)
foundImage = YES;
}];
}
// Expect that all the callbacks ran and that none retrieved an image.
FlushRunLoops();
EXPECT_EQ([testSessions_ count], numCallbacks);
EXPECT_FALSE(foundImage);
}
// Loads kSessionCount color images into the cache. If |waitForFilesOnDisk|
// is YES, will not return until the images have been written to disk.
void LoadAllColorImagesIntoCache(bool waitForFilesOnDisk) {
LoadColorImagesIntoCache(kSessionCount, waitForFilesOnDisk);
}
// Loads |count| color images into the cache. If |waitForFilesOnDisk|
// is YES, will not return until the images have been written to disk.
void LoadColorImagesIntoCache(NSUInteger count, bool waitForFilesOnDisk) {
SnapshotCache* cache = GetSnapshotCache();
// Put color images in the cache.
for (NSUInteger i = 0; i < count; ++i) {
@autoreleasepool {
UIImage* image = [testImages_ objectAtIndex:i];
NSString* sessionID = [testSessions_ objectAtIndex:i];
[cache setImage:image withSessionID:sessionID];
}
}
if (waitForFilesOnDisk) {
FlushRunLoops();
for (NSUInteger i = 0; i < count; ++i) {
// Check that images are on the disk.
NSString* sessionID = [testSessions_ objectAtIndex:i];
base::FilePath path([cache imagePathForSessionID:sessionID]);
EXPECT_TRUE(base::PathExists(path));
}
}
}
// Waits for the first |count| grey images for sessions in |testSessions_|
// to be placed in the cache.
void WaitForGreyImagesInCache(NSUInteger count) {
SnapshotCache* cache = GetSnapshotCache();
FlushRunLoops();
for (NSUInteger i = 0; i < count; i++)
EXPECT_TRUE([cache hasGreyImageInMemory:testSessions_[i]]);
}
// Guesses the order of the color channels in the image.
// Supports RGB, BGR, RGBA, BGRA, ARGB, ABGR.
// Returns the position of each channel between 0 and 3.
void ComputeColorComponents(CGImageRef cgImage,
int* red,
int* green,
int* blue) {
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
int byteOrder = bitmapInfo & kCGBitmapByteOrderMask;
*red = 0;
*green = 1;
*blue = 2;
if (alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaNoneSkipLast) {
*red = 1;
*green = 2;
*blue = 3;
}
if (byteOrder != kCGBitmapByteOrder32Host) {
int lastChannel = (CGImageGetBitsPerPixel(cgImage) == 24) ? 2 : 3;
*red = lastChannel - *red;
*green = lastChannel - *green;
*blue = lastChannel - *blue;
}
}
void TriggerMemoryWarning() {
// _performMemoryWarning is a private API and must not be compiled into
// official builds.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
[[UIApplication sharedApplication]
performSelector:@selector(_performMemoryWarning)];
#pragma clang diagnostic pop
}
web::TestWebThreadBundle thread_bundle_;
SnapshotCache* snapshotCache_;
NSMutableArray* testSessions_;
NSMutableArray* testImages_;
};
// This test simply put all the snapshots in the cache 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(SnapshotCacheTest, Cache) {
SnapshotCache* cache = GetSnapshotCache();
NSUInteger expectedCacheSize = MIN(kSessionCount, [cache lruCacheMaxSize]);
// Put all images in the cache.
for (NSUInteger i = 0; i < expectedCacheSize; ++i) {
UIImage* image = [testImages_ objectAtIndex:i];
NSString* sessionID = [testSessions_ objectAtIndex:i];
[cache setImage:image withSessionID:sessionID];
}
// Get images back.
__block NSUInteger numberOfCallbacks = 0;
for (NSUInteger i = 0; i < expectedCacheSize; ++i) {
NSString* sessionID = [testSessions_ objectAtIndex:i];
UIImage* expectedImage = [testImages_ objectAtIndex:i];
EXPECT_TRUE(expectedImage != nil);
[cache retrieveImageForSessionID:sessionID
callback:^(UIImage* image) {
// Images have not been removed from the
// dictionnary. We expect the same pointer.
EXPECT_EQ(expectedImage, image);
++numberOfCallbacks;
}];
}
EXPECT_EQ(expectedCacheSize, numberOfCallbacks);
}
// This test puts all the snapshots in the cache and flushes them to disk.
// The snapshots are then reloaded from the disk, and the colors are compared.
TEST_F(SnapshotCacheTest, SaveToDisk) {
SnapshotCache* cache = GetSnapshotCache();
// Put all images in the cache.
for (NSUInteger i = 0; i < kSessionCount; ++i) {
UIImage* image = [testImages_ objectAtIndex:i];
NSString* sessionID = [testSessions_ objectAtIndex:i];
[cache setImage:image withSessionID:sessionID];
}
FlushRunLoops();
for (NSUInteger i = 0; i < kSessionCount; ++i) {
// Check that images are on the disk.
NSString* sessionID = [testSessions_ objectAtIndex:i];
base::FilePath path([cache imagePathForSessionID:sessionID]);
EXPECT_TRUE(base::PathExists(path));
// Check image colors by comparing the first pixel against the reference
// image.
UIImage* image =
[UIImage imageWithContentsOfFile:base::SysUTF8ToNSString(path.value())];
CGImageRef cgImage = [image CGImage];
ASSERT_TRUE(cgImage != nullptr);
base::ScopedCFTypeRef<CFDataRef> pixelData(
CGDataProviderCopyData(CGImageGetDataProvider(cgImage)));
const char* pixels =
reinterpret_cast<const char*>(CFDataGetBytePtr(pixelData));
EXPECT_TRUE(pixels);
UIImage* referenceImage = [testImages_ objectAtIndex:i];
CGImageRef referenceCgImage = [referenceImage CGImage];
base::ScopedCFTypeRef<CFDataRef> referenceData(
CGDataProviderCopyData(CGImageGetDataProvider(referenceCgImage)));
const char* referencePixels =
reinterpret_cast<const char*>(CFDataGetBytePtr(referenceData));
EXPECT_TRUE(referencePixels);
if (pixels != nil && referencePixels != nil) {
// Color components may not be in the same order,
// because of writing to disk and reloading.
int red, green, blue;
ComputeColorComponents(cgImage, &red, &green, &blue);
int referenceRed, referenceGreen, referenceBlue;
ComputeColorComponents(referenceCgImage, &referenceRed, &referenceGreen,
&referenceBlue);
// Colors may not be exactly the same (compression or rounding errors)
// thus a small difference is allowed.
EXPECT_NEAR(referencePixels[referenceRed], pixels[red], 1);
EXPECT_NEAR(referencePixels[referenceGreen], pixels[green], 1);
EXPECT_NEAR(referencePixels[referenceBlue], pixels[blue], 1);
}
}
}
TEST_F(SnapshotCacheTest, Purge) {
SnapshotCache* cache = GetSnapshotCache();
// Put all images in the cache.
for (NSUInteger i = 0; i < kSessionCount; ++i) {
UIImage* image = [testImages_ objectAtIndex:i];
NSString* sessionID = [testSessions_ objectAtIndex:i];
[cache setImage:image withSessionID:sessionID];
}
NSMutableSet* liveSessions = [NSMutableSet setWithCapacity:1];
[liveSessions addObject:[testSessions_ objectAtIndex:0]];
// Purge the cache.
[cache purgeCacheOlderThan:(base::Time::Now() - base::TimeDelta::FromHours(1))
keeping:liveSessions];
FlushRunLoops();
// Check that nothing has been deleted.
for (NSUInteger i = 0; i < kSessionCount; ++i) {
// Check that images are on the disk.
NSString* sessionID = [testSessions_ objectAtIndex:i];
base::FilePath path([cache imagePathForSessionID:sessionID]);
EXPECT_TRUE(base::PathExists(path));
}
// Purge the cache.
[cache purgeCacheOlderThan:base::Time::Now() keeping:liveSessions];
FlushRunLoops();
// Check that the file have been deleted.
for (NSUInteger i = 0; i < kSessionCount; ++i) {
// Check that images are on the disk.
NSString* sessionID = [testSessions_ objectAtIndex:i];
base::FilePath path([cache imagePathForSessionID:sessionID]);
if (i == 0)
EXPECT_TRUE(base::PathExists(path));
else
EXPECT_FALSE(base::PathExists(path));
}
}
// Loads the color images into the cache, and pins two of them. Ensures that
// only the two pinned IDs remain in memory after a memory warning.
TEST_F(SnapshotCacheTest, HandleMemoryWarning) {
LoadAllColorImagesIntoCache(true);
SnapshotCache* cache = GetSnapshotCache();
NSString* firstPinnedID = [testSessions_ objectAtIndex:4];
NSString* secondPinnedID = [testSessions_ objectAtIndex:6];
NSMutableSet* set = [NSMutableSet set];
[set addObject:firstPinnedID];
[set addObject:secondPinnedID];
cache.pinnedIDs = set;
TriggerMemoryWarning();
EXPECT_EQ(YES, [cache hasImageInMemory:firstPinnedID]);
EXPECT_EQ(YES, [cache hasImageInMemory:secondPinnedID]);
NSString* notPinnedID = [testSessions_ objectAtIndex:2];
EXPECT_FALSE([cache hasImageInMemory:notPinnedID]);
// Wait for the final image to be pulled off disk.
FlushRunLoops();
}
// Tests that createGreyCache creates the grey snapshots in the background,
// from color images in the in-memory cache. When the grey images are all
// loaded into memory, tests that the request to retrieve the grey snapshot
// calls the callback immediately.
// Disabled on simulators because it sometimes crashes. crbug/421425
#if !TARGET_IPHONE_SIMULATOR
TEST_F(SnapshotCacheTest, CreateGreyCache) {
LoadAllColorImagesIntoCache(true);
// Request the creation of a grey image cache for all images.
SnapshotCache* cache = GetSnapshotCache();
[cache createGreyCache:testSessions_];
// Wait for them to be put into the grey image cache.
WaitForGreyImagesInCache(kSessionCount);
__block NSUInteger numberOfCallbacks = 0;
for (NSUInteger i = 0; i < kSessionCount; ++i) {
NSString* sessionID = [testSessions_ objectAtIndex:i];
[cache retrieveGreyImageForSessionID:sessionID
callback:^(UIImage* image) {
EXPECT_TRUE(image);
++numberOfCallbacks;
}];
}
EXPECT_EQ(numberOfCallbacks, kSessionCount);
}
// Same as previous test, except that all the color images are on disk,
// rather than in memory.
// Disabled due to the greyImage crash. b/8048597
TEST_F(SnapshotCacheTest, CreateGreyCacheFromDisk) {
LoadAllColorImagesIntoCache(true);
// Remove color images from in-memory cache.
SnapshotCache* cache = GetSnapshotCache();
TriggerMemoryWarning();
// Request the creation of a grey image cache for all images.
[cache createGreyCache:testSessions_];
// Wait for them to be put into the grey image cache.
WaitForGreyImagesInCache(kSessionCount);
__block NSUInteger numberOfCallbacks = 0;
for (NSUInteger i = 0; i < kSessionCount; ++i) {
NSString* sessionID = [testSessions_ objectAtIndex:i];
[cache retrieveGreyImageForSessionID:sessionID
callback:^(UIImage* image) {
EXPECT_TRUE(image);
++numberOfCallbacks;
}];
}
EXPECT_EQ(numberOfCallbacks, kSessionCount);
}
#endif // !TARGET_IPHONE_SIMULATOR
// Tests mostRecentGreyBlock, which is a block to be called when the most
// recently requested grey image is finally loaded.
// The test requests three images be cached as grey images. Only the final
// callback of the three requests should be called.
// Disabled due to the greyImage crash. b/8048597
TEST_F(SnapshotCacheTest, MostRecentGreyBlock) {
const NSUInteger kNumImages = 3;
NSMutableArray* sessionIDs =
[[NSMutableArray alloc] initWithCapacity:kNumImages];
[sessionIDs addObject:[testSessions_ objectAtIndex:0]];
[sessionIDs addObject:[testSessions_ objectAtIndex:1]];
[sessionIDs addObject:[testSessions_ objectAtIndex:2]];
SnapshotCache* cache = GetSnapshotCache();
// Put 3 images in the cache.
LoadColorImagesIntoCache(kNumImages, true);
// Make sure the color images are only on disk, to ensure the background
// thread is slow enough to queue up the requests.
TriggerMemoryWarning();
// Enable the grey image cache.
[cache createGreyCache:sessionIDs];
// Request the grey versions
__block BOOL firstCallbackCalled = NO;
__block BOOL secondCallbackCalled = NO;
__block BOOL thirdCallbackCalled = NO;
[cache greyImageForSessionID:[testSessions_ objectAtIndex:0]
callback:^(UIImage*) {
firstCallbackCalled = YES;
}];
[cache greyImageForSessionID:[testSessions_ objectAtIndex:1]
callback:^(UIImage*) {
secondCallbackCalled = YES;
}];
[cache greyImageForSessionID:[testSessions_ objectAtIndex:2]
callback:^(UIImage*) {
thirdCallbackCalled = YES;
}];
// Wait for them to be loaded.
WaitForGreyImagesInCache(kNumImages);
EXPECT_FALSE(firstCallbackCalled);
EXPECT_FALSE(secondCallbackCalled);
EXPECT_TRUE(thirdCallbackCalled);
}
// Test the function used to save a grey copy of a color snapshot fully on a
// background thread when the application is backgrounded.
TEST_F(SnapshotCacheTest, GreyImageAllInBackground) {
LoadAllColorImagesIntoCache(true);
SnapshotCache* cache = GetSnapshotCache();
// Now convert every image into a grey image, on disk, in the background.
for (NSUInteger i = 0; i < kSessionCount; ++i) {
[cache saveGreyInBackgroundForSessionID:[testSessions_ objectAtIndex:i]];
}
// Waits for the grey images for the sessions in |testSessions_| to be written
// to disk, which happens in a background thread.
FlushRunLoops();
for (NSString* sessionID in testSessions_) {
base::FilePath path([cache greyImagePathForSessionID:sessionID]);
EXPECT_TRUE(base::PathExists(path));
base::DeleteFile(path, false);
}
}
// Verifies that image size and scale are preserved when writing and reading
// from disk.
TEST_F(SnapshotCacheTest, SizeAndScalePreservation) {
SnapshotCache* cache = GetSnapshotCache();
// Create an image with the expected snapshot scale.
CGFloat scale = [cache snapshotScaleForDevice];
UIGraphicsBeginImageContextWithOptions(
CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
UIImage* image = GenerateRandomImage(context);
UIGraphicsEndImageContext();
// Add the image to the cache then call handle low memory to ensure the image
// is read from disk instead of the in-memory cache.
NSString* const kSession = @"foo";
[cache setImage:image withSessionID:kSession];
FlushRunLoops(); // ensure the file is written to disk.
TriggerMemoryWarning();
// Retrive the image and have the callback verify the size and scale.
__block BOOL callbackComplete = NO;
[cache retrieveImageForSessionID:kSession
callback:^(UIImage* imageFromDisk) {
EXPECT_EQ(image.size.width,
imageFromDisk.size.width);
EXPECT_EQ(image.size.height,
imageFromDisk.size.height);
EXPECT_EQ(image.scale, imageFromDisk.scale);
callbackComplete = YES;
}];
FlushRunLoops();
EXPECT_TRUE(callbackComplete);
}
// Verifies that retina-scale images are deleted properly.
TEST_F(SnapshotCacheTest, DeleteRetinaImages) {
SnapshotCache* cache = GetSnapshotCache();
if ([cache snapshotScaleForDevice] != 2.0) {
return;
}
// Create an image with retina scale.
UIGraphicsBeginImageContextWithOptions(
CGSizeMake(kSnapshotPixelSize, kSnapshotPixelSize), NO, 2.0);
CGContextRef context = UIGraphicsGetCurrentContext();
UIImage* image = GenerateRandomImage(context);
UIGraphicsEndImageContext();
// Add the image to the cache then call handle low memory to ensure the image
// is read from disk instead of the in-memory cache.
NSString* const kSession = @"foo";
[cache setImage:image withSessionID:kSession];
FlushRunLoops(); // ensure the file is written to disk.
TriggerMemoryWarning();
// Verify the file was writted with @2x in the file name.
base::FilePath retinaFile = [cache imagePathForSessionID:kSession];
EXPECT_TRUE(base::PathExists(retinaFile));
// Delete the image.
[cache removeImageWithSessionID:kSession];
FlushRunLoops(); // ensure the file is removed.
EXPECT_FALSE(base::PathExists(retinaFile));
}
} // namespace