diff --git a/Foundation/CPCache.j b/Foundation/CPCache.j new file mode 100644 index 000000000..c8efe2e2d --- /dev/null +++ b/Foundation/CPCache.j @@ -0,0 +1,371 @@ +/* + * CPCache.j + * Foundation + * + * Created by William Mura. + * Copyright 2015, William Mura. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +@import "CPObject.j" +@import "CPDictionary.j" +@import "CPString.j" + +@class _CPCacheItem; + + +/* + * Delegate CPCacheDelegate + * + * - cache:willEvictObject: is called when a object is going to be removed + * When the total cost or the count exceeds the total cost limit or the count limit + * And also when removeObjectForKey or removeAllObjects are called + */ +@protocol CPCacheDelegate + +@optional +- (void)cache:(CPCache)cache willEvictObject:(id)obj; + +@end + +var CPCacheDelegate_cache_willEvictObject_ = 1 << 1; + + +/*! + @class CPCache + @ingroup foundation + @brief A collection-like container with discardable objects + + https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSCache_Class/index.html#//apple_ref/occ/instp/NSCache/delegate + + A CPCache object is a collection-like container, or cache, that stores key-value pairs, + similar to the CPDictionary class. Developers often incorporate caches to temporarily + store objects with transient data that are expensive to create. + + Reusing these objects can provide performance benefits, because their values do not have to be recalculated. + However, the objects are not critical to the application and can be discarded if memory is tight. + If discarded, their values will have to be recomputed again when needed. + */ +@implementation CPCache : CPObject +{ + CPDictionary _items; + int _currentPosition; + BOOL _totalCostCache; + unsigned _implementedDelegateMethods; + + CPString _name @accessors(property=name); + int _countLimit @accessors(property=countLimit); + int _totalCostLimit @accessors(property=totalCostLimit); + id _delegate @accessors(property=delegate); +} + + +#pragma mark - +#pragma mark Initialization + +/*! + Initializes the cache with default values + @return the initialized cache +*/ +- (id)init +{ + if (self = [super init]) + { + _items = [[CPDictionary alloc] init]; + _currentPosition = 0; + _totalCostCache = -1; + _implementedDelegateMethods = 0; + + _name = @""; + _countLimit = 0; + _totalCostLimit = 0; + _delegate = nil; + } + + return self; +} + + +#pragma mark - +#pragma mark Managing cache + +/*! + Returns the object which correspond to the given key + @param aKey the key for the object's entry + @return the object for the entry +*/ +- (id)objectForKey:(id)aKey +{ + return [[_items objectForKey:aKey] object]; +} + +/*! + Adds an object with default cost into the cache. + @param anObject the object to add in the cache + @param aKey the object's key +*/ +- (void)setObject:(id)anObject forKey:(id)aKey +{ + [self setObject:anObject forKey:aKey cost:0]; +} + +/*! + Adds an object with a cost into the cache. + @param anObject the object to add in the cache + @param aKey the object's key + @param aCost the object's cost +*/ +- (void)setObject:(id)anObject forKey:(id)aKey cost:(int)aCost +{ + // Check if the key already exist + if ([_items objectForKey:aKey]) + [self removeObjectForKey:aKey]; + + // Add object + [_items setObject:[_CPCacheItem cacheItemWithObject:anObject cost:aCost position:++_currentPosition] forKey:aKey]; + + // Invalid cost cache + _totalCostCache = -1; + + // Clean cache to satisfy condition (< totalCostLimit & < countLimit) if necessary + [self _cleanCache]; +} + +/*! + Removes the object from the cache for the given key. + @param aKey the key of the object to be removed +*/ +- (void)removeObjectForKey:(id)aKey +{ + // Call delegate method to warn that the object is going to be removed + [self _sendDelegateWillEvictObjectForKey:aKey]; + + // Remove object + [_items removeObjectForKey:aKey]; + + // Invalid cost cache + _totalCostCache = -1; +} + +/*! + Removes all the objects from the cache. +*/ +- (void)removeAllObjects +{ + // Call delegate method to warn that the objects are going to be removed + var enumerator = [_items keyEnumerator], + key; + + while (key = [enumerator nextObject]) + [self _sendDelegateWillEvictObjectForKey:key] + + // Remove all objects + [_items removeAllObjects]; + + // Invalid cost cache and reset position counter + _totalCostCache = -1; + _currentPosition = 0; +} + + +#pragma mark - +#pragma mark Setters + +/*! + Sets the count limit of the cache. + Remove objects if not enough place to keep all of them + @param aCountLimit the new count limit +*/ +- (void)setCountLimit:(int)aCountLimit +{ + _countLimit = aCountLimit; + [self _cleanCache]; +} + +/*! + Sets the total cost limit of the cache. + Remove objects if not enough place to keep all of them + @param aTotalCostLimit the new total cost limit +*/ +- (void)setTotalCostLimit:(int)aTotalCostLimit +{ + _totalCostLimit = aTotalCostLimit; + [self _cleanCache]; +} + +/*! + Sets the cache's delegate. + @param aDelegate the new delegate +*/ +- (void)setDelegate:(id)aDelegate +{ + if (_delegate === aDelegate) + return; + + _delegate = aDelegate; + _implementedDelegateMethods = 0; + + if ([_delegate respondsToSelector:@selector(cache:willEvictObject:)]) + _implementedDelegateMethods |= CPCacheDelegate_cache_willEvictObject_ +} + + +#pragma mark - +#pragma mark Privates + +/* + * This method return the number of objects in the cache + */ +- (int)_count +{ + return [_items count]; +} + +/* + * This method return the total cost (addition of all object's cost in the cache) + */ +- (int)_totalCost +{ + if (_totalCostCache >= 0) + return _totalCostCache; + + var enumerator = [_items objectEnumerator], + value; + + _totalCostCache = 0; + + while (value = [enumerator nextObject]) + _totalCostCache += [value cost]; + + return _totalCostCache; +} + +/* + * This method resequence the position of objects + * Otherwise the position value could rise until to cause problem + */ +- (void)_resequencePosition +{ + _currentPosition = 1; + + // Sort keys by position + var sortedKeys = [[_items allKeys] sortedArrayUsingFunction: + function(k1, k2) { + return [[[_items objectForKey:k1] position] compare:[[_items objectForKey:k2] position]]; }]; + + // Affect new positions + for (var i = 0; i < sortedKeys.length; ++i) + [[_items objectForKey:sortedKeys[i]] setPosition:_currentPosition++]; +} + +/* + * Check if the totalCostLimit is exceeded + */ +- (BOOL)_isTotalCostLimitExceeded +{ + return ([self _totalCost] > _totalCostLimit && _totalCostLimit > 0); +} + +/* + * Check if the countLimit is exceeded + */ +- (BOOL)_isCountLimitExceeded +{ + return ([self _count] > _countLimit && _countLimit > 0); +} + +/* + * This method clean the cache if the totalCost or the count exceeds the totalCostLimit or the countLimit + * until to satisfy condition (totalCost < totalCostLimit and count < countLimit) + */ +- (void)_cleanCache +{ + // Check if the condition is satisfied + if (![self _isTotalCostLimitExceeded] && ![self _isCountLimitExceeded]) + return; + + // Sort keys by position + var sortedKeys = [[_items allKeys] sortedArrayUsingFunction: + function(k1, k2) { + return [[[_items objectForKey:k1] position] compare:[[_items objectForKey:k2] position]]; }]; + + // Remove oldest objects until to satisfy the break condition + for (var i = 0; i < sortedKeys.length; ++i) + { + if (![self _isTotalCostLimitExceeded] && ![self _isCountLimitExceeded]) + break; + + // Call delegate method to warn that the object is going to be removed + [self _sendDelegateWillEvictObjectForKey:sortedKeys[i]]; + + // Remove object + [_items removeObjectForKey: sortedKeys[i]]; + + // Invalid cost cache + _totalCostCache = -1; + } + + // Resequence position of all objects + [self _resequencePosition]; +} + +@end + + +@implementation CPCache (CPCacheDelegate) + +- (void)_sendDelegateWillEvictObjectForKey:(id)aKey +{ + if (_implementedDelegateMethods & CPCacheDelegate_cache_willEvictObject_) + [_delegate cache:self willEvictObject:[[_items objectForKey:aKey] object]]; +} + +@end + + +/* + * Class _CPCacheItem + * Represent an item of CPCache + * This class allow to associate a cost and a position to an object + * + * Attributes: + * - object: the stored object + * - cost: represent the cost (of memory) of the object + * - position: represent the insertion order to determine the oldest object + */ +@implementation _CPCacheItem : CPObject +{ + CPObject _object @accessors(property=object); + int _cost @accessors(property=cost); + int _position @accessors(property=position); +} + ++ (id)cacheItemWithObject:(CPObject)anObject cost:(int)aCost position:(int)aPosition +{ + var cacheItem = [[self alloc] init]; + + if (cacheItem) + { + [cacheItem setObject:anObject]; + [cacheItem setCost:aCost]; + [cacheItem setPosition:aPosition]; + } + + return cacheItem; +} + +@end \ No newline at end of file diff --git a/Foundation/Foundation.j b/Foundation/Foundation.j index 3fa485412..3fcf5481f 100755 --- a/Foundation/Foundation.j +++ b/Foundation/Foundation.j @@ -24,6 +24,7 @@ @import "CPArray.j" @import "CPBundle.j" @import "CPByteCountFormatter.j" +@import "CPCache.j" @import "CPCharacterSet.j" @import "CPCoder.j" @import "CPComparisonPredicate.j" diff --git a/Tests/Foundation/CPCacheItemTest.j b/Tests/Foundation/CPCacheItemTest.j new file mode 100644 index 000000000..89a982d10 --- /dev/null +++ b/Tests/Foundation/CPCacheItemTest.j @@ -0,0 +1,14 @@ +@import + + +@implementation CPCacheItemTest : OJTestCase + +- (void)testCacheItemWithObjectCostPosition +{ + var cacheItem = [_CPCacheItem cacheItemWithObject:"Hello Cappuccino!" cost:50 position:1]; + [self assert:[cacheItem object] equals:"Hello Cappuccino!"]; + [self assert:[cacheItem cost] equals:50]; + [self assert:[cacheItem position] equals:1]; +} + +@end diff --git a/Tests/Foundation/CPCacheTest.j b/Tests/Foundation/CPCacheTest.j new file mode 100644 index 000000000..ac50a0fc6 --- /dev/null +++ b/Tests/Foundation/CPCacheTest.j @@ -0,0 +1,411 @@ +@import + + +/* + * CPCacheTest tests all methods from CPCache + */ +@implementation CPCacheTest : OJTestCase +{ + // Parameters to check delegate + int _countDelegateExecuted; + CPArray _caches; + CPArray _objects; +} + +- (void)setUp +{ + // Initialize data to check delegate + _countDelegateExecuted = 0; + _caches = [[CPArray alloc] init]; + _objects = [[CPArray alloc] init]; +} + + +/* + * Delegate method, called when an object is evicted + */ +- (void)cache:(CPCache)cache willEvictObject:(id)obj +{ + _countDelegateExecuted++; + [_caches addObject:cache]; + [_objects addObject:obj]; +} + + +/* + * Tests methods of CPCache + */ +- (void)testInit +{ + var cache = [[CPCache alloc] init]; + + // Private attributes + [self assert:CPCache equals:[cache class]]; + [self assert:CPDictionary equals:[cache._items class]]; + [self assert:0 equals:cache._currentPosition]; + [self assert:-1 equals:cache._totalCostCache]; + [self assert:0 equals:cache._implementedDelegateMethods]; + + // Public attributes + [self assert:@"" equals:[cache name]]; + [self assert:0 equals:[cache countLimit]]; + [self assert:0 equals:[cache totalCostLimit]]; + [self assert:nil equals:[cache delegate]]; +} + +- (void)testObjectForKey +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1"]; + + // With valid key + [self assert:@"Object1" equals:[cache objectForKey:@"key1"]]; + + // With invalid key + [self assert:nil equals:[cache objectForKey:@"key666"]]; + + // With nil key + [self assert:nil equals:[cache objectForKey:nil]]; +} + +- (void)testSetObjectForKeyDefault +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + + // Check result when add key in empty cache + [cache setObject:@"Object1" forKey:@"key1"] + [self assert:@"Object1" equals:[cache objectForKey:@"key1"]]; + + // Check delegate + [self assert:0 equals:_countDelegateExecuted]; + [self assert:0 equals:[_caches count]]; + [self assert:0 equals:[_objects count]]; +} + +- (void)testSetObjectForKeyWithExistingKey +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1"]; + [cache setObject:@"Object2" forKey:@"key2"]; + + // Check result when add object with existing key in cache + [cache setObject:@"Object3" forKey:@"key1"]; + [self assert:@"Object3" equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; +} + +- (void)testSetObjectForKeyDiscardByCost +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setTotalCostLimit:100]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:50]; + + // Check result when add key in cache with total cost limit + [cache setObject:@"Object3" forKey:@"key3" cost:1]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + [self assert:@"Object3" equals:[cache objectForKey:@"key3"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; +} + +- (void)testSetObjectForKeyDiscardByCount +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setCountLimit:2]; + [cache setObject:@"Object1" forKey:@"key1"]; + [cache setObject:@"Object2" forKey:@"key2"]; + + // Check result when add key in cache with count limit + [cache setObject:@"Object3" forKey:@"key3"]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + [self assert:@"Object3" equals:[cache objectForKey:@"key3"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; +} + +- (void)testRemoveObjectForKey +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1"]; + + // Remove object + [cache removeObjectForKey:@"key1"]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; +} + +- (void)testRemoveAllObjects +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1"]; + [cache setObject:@"Object2" forKey:@"key2"]; + + // Remove all objects + [cache removeAllObjects]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:nil equals:[cache objectForKey:@"key2"]]; + + // Check delegate + [self assert:2 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; + [self assert:cache equals:[_caches objectAtIndex:1]]; + [self assert:@"Object2" equals:[_objects objectAtIndex:1]]; +} + + +/* + * Accessors and mutators + */ + +- (void)testSetCountLimit +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1"]; + [cache setObject:@"Object2" forKey:@"key2"]; + [cache setObject:@"Object3" forKey:@"key3"]; + + // Set count limit + [cache setCountLimit:2]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + [self assert:@"Object3" equals:[cache objectForKey:@"key3"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; + + // Check new count limit + [self assert:2 equals:[cache countLimit]]; +} + +- (void)testSetTotalCostLimit +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:10]; + [cache setObject:@"Object3" forKey:@"key3" cost:70]; + + // Set total cost limit + [cache setTotalCostLimit:100]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + [self assert:@"Object3" equals:[cache objectForKey:@"key3"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; + + // Check total cost limit + [self assert:100 equals:[cache totalCostLimit]]; +} + +- (void)testSetDelegate +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + + [self assert:self equals:[cache delegate]]; + [self assert:(1 << 1) equals:cache._implementedDelegateMethods] +} + +/* + * Test private methods + */ + +- (void)testCount +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:10]; + [cache setObject:@"Object3" forKey:@"key3" cost:70]; + + [self assert:3 equals:[cache _count]]; +} + +- (void)testTotalCost +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:10]; + [cache setObject:@"Object3" forKey:@"key3" cost:70]; + + [self assert:130 equals:[cache _totalCost]]; + + // Hack totalCost then invalidate it to be sure that it is recomputed + cache._totalCostCache = 650; + [self assert:650 equals:[cache _totalCost]]; + + cache._totalCostCache = -1; + [self assert:130 equals:[cache _totalCost]]; +} + +- (void)testResequencePosition +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; // 1 - 1 + [cache setObject:@"Object2" forKey:@"key2" cost:10]; + [cache setObject:@"Object3" forKey:@"key3" cost:70]; // 3 - 2 + [cache removeObjectForKey:@"key2"] + [cache setObject:@"Object4" forKey:@"key4" cost:70]; // 4 - 3 + + // Before resequence + [self assert:1 equals:[[cache._items objectForKey:@"key1"] position]]; + [self assert:3 equals:[[cache._items objectForKey:@"key3"] position]]; + [self assert:4 equals:[[cache._items objectForKey:@"key4"] position]]; + + [cache _resequencePosition]; + + // After resequence + [self assert:1 equals:[[cache._items objectForKey:@"key1"] position]]; + [self assert:2 equals:[[cache._items objectForKey:@"key3"] position]]; + [self assert:3 equals:[[cache._items objectForKey:@"key4"] position]]; +} + +- (void)testIsTotalCostLimitExceeded +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + + // Not exceed (because of not limit) + [self assert:NO equals:[cache _isTotalCostLimitExceeded]]; + + // Not exceed (with limit) + [cache setTotalCostLimit:100]; + [self assert:NO equals:[cache _isTotalCostLimitExceeded]]; + + // Exceed (by hacking the cost of object) + [[cache._items objectForKey:@"key1"] setCost: 150]; + cache._totalCostCache = -1; + [self assert:YES equals:[cache _isTotalCostLimitExceeded]]; +} + +- (void)testIsCountLimitExceeded +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1"]; + + // Not exceed (because of not limit) + [self assert:NO equals:[cache _isCountLimitExceeded]]; + + // Not exceed (with limit) + [cache setCountLimit:1]; + [self assert:NO equals:[cache _isCountLimitExceeded]]; + + // Exceed (by hacking the items) + [cache._items setObject:[_CPCacheItem cacheItemWithObject:@"Object2" cost:0 position:3] forKey:@"key2"]; + [self assert:YES equals:[cache _isCountLimitExceeded]]; +} + +- (void)testCleanCacheWithoutDiscarding +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setTotalCostLimit: 100]; + [cache setCountLimit: 2]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:40]; + + // Set total cost limit + [cache _cleanCache]; + [self assert:@"Object1" equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + + // Check delegate + [self assert:0 equals:_countDelegateExecuted]; +} + +- (void)testCleanCacheWithCostDiscarding +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setTotalCostLimit: 100]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:40]; + + // Hack cost of object + [[cache._items objectForKey:@"key2"] setCost:80]; + cache._totalCostCache = -1; + + // Set total cost limit + [cache _cleanCache]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; +} + +- (void)testCleanCacheWithCountDiscarding +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setCountLimit:2]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1"]; + [cache setObject:@"Object2" forKey:@"key2"]; + + // Hack items + [cache._items setObject:[_CPCacheItem cacheItemWithObject:@"Object3" cost:0 position:3] forKey:@"key3"]; + + // Set total cost limit + [cache _cleanCache]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + [self assert:@"Object3" equals:[cache objectForKey:@"key3"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; +} + + +@end