mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-17 19:09:56 +00:00
Initial open-source release of ASMultiplexImageNode. Documentation and example code forthcoming. Note: ASMultiplexImageNode requires Xcode 6 to compile. Tests are now compiled against the iOS 8 SDK and run on iOS 7.1 and iOS 8.
350 lines
14 KiB
Objective-C
350 lines
14 KiB
Objective-C
/* Copyright (c) 2014-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import <OCMock/OCMock.h>
|
|
|
|
#import <AsyncDisplayKit/ASImageProtocols.h>
|
|
#import <AsyncDisplayKit/ASMultiplexImageNode.h>
|
|
|
|
#import <libkern/OSAtomic.h>
|
|
|
|
#import <XCTest/XCTest.h>
|
|
|
|
@interface ASMultiplexImageNodeTests : XCTestCase
|
|
{
|
|
@private
|
|
id _mockCache;
|
|
id _mockDownloader;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation ASMultiplexImageNodeTests
|
|
|
|
#pragma mark -
|
|
#pragma mark Helpers.
|
|
|
|
- (NSURL *)_testImageURL
|
|
{
|
|
return [[NSBundle bundleForClass:[self class]] URLForResource:@"logo-square"
|
|
withExtension:@"png"
|
|
subdirectory:@"TestResources"];
|
|
}
|
|
|
|
- (UIImage *)_testImage
|
|
{
|
|
return [[[UIImage alloc] initWithContentsOfFile:[self _testImageURL].path] autorelease];
|
|
}
|
|
|
|
static BOOL ASInvokeConditionBlockWithBarriers(BOOL (^block)()) {
|
|
// In case the block does multiple comparisons, ensure it has a consistent view of memory by issuing read-write
|
|
// barriers on either side of the block.
|
|
OSMemoryBarrier();
|
|
BOOL result = block();
|
|
OSMemoryBarrier();
|
|
return result;
|
|
}
|
|
|
|
static BOOL ASRunRunLoopUntilBlockIsTrue(BOOL (^block)())
|
|
{
|
|
// Time out after 30 seconds.
|
|
CFTimeInterval timeoutDate = CACurrentMediaTime() + 30.0f;
|
|
BOOL passed = NO;
|
|
|
|
while (true) {
|
|
passed = ASInvokeConditionBlockWithBarriers(block);
|
|
|
|
if (passed) {
|
|
break;
|
|
}
|
|
|
|
CFTimeInterval now = CACurrentMediaTime();
|
|
if (now > timeoutDate) {
|
|
break;
|
|
}
|
|
|
|
// Run 1000 times a second until the poll timeout or until timeoutDate, whichever is first.
|
|
CFTimeInterval runLoopTimeout = MIN(0.001, timeoutDate - now);
|
|
CFRunLoopRunInMode(kCFRunLoopDefaultMode, runLoopTimeout, true);
|
|
}
|
|
|
|
return passed;
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Unit tests.
|
|
|
|
// TODO: add tests for delegate display notifications
|
|
|
|
- (void)setUp
|
|
{
|
|
[super setUp];
|
|
|
|
_mockCache = [OCMockObject mockForProtocol:@protocol(ASImageCacheProtocol)];
|
|
_mockDownloader = [OCMockObject mockForProtocol:@protocol(ASImageDownloaderProtocol)];
|
|
}
|
|
|
|
- (void)testDataSourceImageMethod
|
|
{
|
|
ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader];
|
|
|
|
// Mock the data source.
|
|
// Note that we're not using a niceMock because we want to assert if the URL data-source method gets hit, as the image
|
|
// method should be hit first and exclusively if it successfully returns an image.
|
|
id mockDataSource = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDataSource)];
|
|
imageNode.dataSource = mockDataSource;
|
|
|
|
NSNumber *imageIdentifier = @1;
|
|
|
|
// Expect the image method to be hit, and have it return our test image.
|
|
UIImage *testImage = [self _testImage];
|
|
[[[mockDataSource expect] andReturn:testImage] multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier];
|
|
|
|
imageNode.imageIdentifiers = @[imageIdentifier];
|
|
|
|
[mockDataSource verify];
|
|
|
|
// Also expect it to be loaded immediately.
|
|
XCTAssertEqualObjects(imageNode.loadedImageIdentifier, imageIdentifier, @"imageIdentifier was not loaded");
|
|
// And for the image to be equivalent to the image we provided.
|
|
XCTAssertEqualObjects(UIImagePNGRepresentation(imageNode.image),
|
|
UIImagePNGRepresentation(testImage),
|
|
@"Loaded image isn't the one we provided");
|
|
|
|
imageNode.delegate = nil;
|
|
imageNode.dataSource = nil;
|
|
[imageNode release];
|
|
}
|
|
|
|
- (void)testDataSourceURLMethod
|
|
{
|
|
ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader];
|
|
|
|
NSNumber *imageIdentifier = @1;
|
|
|
|
// Mock the data source such that we...
|
|
id mockDataSource = [OCMockObject niceMockForProtocol:@protocol(ASMultiplexImageNodeDataSource)];
|
|
imageNode.dataSource = mockDataSource;
|
|
// (a) first expect to be hit for the image directly, and fail to return it.
|
|
[mockDataSource setExpectationOrderMatters:YES];
|
|
[[[mockDataSource expect] andReturn:nil] multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier];
|
|
// (b) and then expect to be hit for the URL, which we'll return.
|
|
[[[mockDataSource expect] andReturn:[self _testImageURL]] multiplexImageNode:imageNode URLForImageIdentifier:imageIdentifier];
|
|
|
|
// Mock the cache to do a cache-hit for the test image URL.
|
|
[[[_mockCache stub] andDo:^(NSInvocation *inv) {
|
|
// Params are URL, callbackQueue, completion
|
|
NSArray *URL;
|
|
[inv getArgument:&URL atIndex:2];
|
|
|
|
void (^completionBlock)(CGImageRef);
|
|
[inv getArgument:&completionBlock atIndex:4];
|
|
|
|
// Call the completion block with our test image and URL.
|
|
NSURL *testImageURL = [self _testImageURL];
|
|
XCTAssertEqualObjects(URL, testImageURL, @"Fetching URL other than test image");
|
|
completionBlock([self _testImage].CGImage);
|
|
}] fetchCachedImageWithURL:[OCMArg any] callbackQueue:[OCMArg any] completion:[OCMArg any]];
|
|
|
|
// Kick off loading.
|
|
imageNode.imageIdentifiers = @[imageIdentifier];
|
|
|
|
// Verify the data source.
|
|
[mockDataSource verify];
|
|
// Also expect it to be loaded immediately.
|
|
XCTAssertEqualObjects(imageNode.loadedImageIdentifier, imageIdentifier, @"imageIdentifier was not loaded");
|
|
// And for the image to be equivalent to the image we provided.
|
|
XCTAssertEqualObjects(UIImagePNGRepresentation(imageNode.image),
|
|
UIImagePNGRepresentation([self _testImage]),
|
|
@"Loaded image isn't the one we provided");
|
|
|
|
imageNode.delegate = nil;
|
|
imageNode.dataSource = nil;
|
|
[imageNode release];
|
|
}
|
|
|
|
- (void)testAddLowerQualityImageIdentifier
|
|
{
|
|
// Adding a lower quality image identifier should not cause any loading.
|
|
ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader];
|
|
|
|
NSNumber *highResIdentifier = @2;
|
|
|
|
// Mock the data source such that we: (a) return the test image, and log whether we get hit for the lower-quality image.
|
|
id mockDataSource = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDataSource)];
|
|
imageNode.dataSource = mockDataSource;
|
|
__block int dataSourceHits = 0;
|
|
[[[mockDataSource stub] andDo:^(NSInvocation *inv) {
|
|
dataSourceHits++;
|
|
|
|
// Return the test image.
|
|
[inv setReturnValue:(void *)[self _testImage]];
|
|
}] multiplexImageNode:[OCMArg any] imageForImageIdentifier:[OCMArg any]];
|
|
|
|
imageNode.imageIdentifiers = @[highResIdentifier];
|
|
|
|
// At this point, we should have the high-res identifier loaded and the DS should have been hit once.
|
|
XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded.");
|
|
XCTAssertTrue(dataSourceHits == 1, @"Unexpected DS hit count");
|
|
|
|
// Add the low res identifier.
|
|
NSNumber *lowResIdentifier = @1;
|
|
imageNode.imageIdentifiers = @[highResIdentifier, lowResIdentifier];
|
|
|
|
// At this point the high-res should still be loaded, and the data source should not have been hit.
|
|
XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded.");
|
|
XCTAssertTrue(dataSourceHits == 1, @"Unexpected DS hit count");
|
|
|
|
imageNode.delegate = nil;
|
|
imageNode.dataSource = nil;
|
|
[imageNode release];
|
|
}
|
|
|
|
- (void)testAddHigherQualityImageIdentifier
|
|
{
|
|
// Adding a higher quality image identifier should cause loading.
|
|
ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader];
|
|
|
|
NSNumber *lowResIdentifier = @1;
|
|
|
|
// Mock the data source such that we: (a) return the test image, and log how many times the DS gets hit.
|
|
id mockDataSource = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDataSource)];
|
|
imageNode.dataSource = mockDataSource;
|
|
__block int dataSourceHits = 0;
|
|
[[[mockDataSource stub] andDo:^(NSInvocation *inv) {
|
|
dataSourceHits++;
|
|
|
|
// Return the test image.
|
|
[inv setReturnValue:(void *)[self _testImage]];
|
|
}] multiplexImageNode:[OCMArg any] imageForImageIdentifier:[OCMArg any]];
|
|
|
|
imageNode.imageIdentifiers = @[lowResIdentifier];
|
|
|
|
// At this point, we should have the low-res identifier loaded and the DS should have been hit once.
|
|
XCTAssertEqualObjects(imageNode.loadedImageIdentifier, lowResIdentifier, @"Low res identifier should be loaded.");
|
|
XCTAssertTrue(dataSourceHits == 1, @"Unexpected DS hit count");
|
|
|
|
// Add the low res identifier.
|
|
NSNumber *highResIdentifier = @2;
|
|
imageNode.imageIdentifiers = @[highResIdentifier, lowResIdentifier];
|
|
|
|
// At this point the high-res should be loaded, and the data source should been hit twice.
|
|
XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded.");
|
|
XCTAssertTrue(dataSourceHits == 2, @"Unexpected DS hit count");
|
|
|
|
imageNode.delegate = nil;
|
|
imageNode.dataSource = nil;
|
|
[imageNode release];
|
|
}
|
|
|
|
- (void)testProgressiveDownloading
|
|
{
|
|
ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader];
|
|
imageNode.downloadsIntermediateImages = YES;
|
|
|
|
// Set up a few identifiers to load.
|
|
NSInteger identifierCount = 5;
|
|
NSMutableArray *imageIdentifiers = [NSMutableArray array];
|
|
for (NSInteger identifierIndex = 0; identifierIndex < identifierCount; identifierIndex++)
|
|
[imageIdentifiers insertObject:@(identifierIndex + 1) atIndex:0];
|
|
|
|
// Mock the data source to only make the images available progressively.
|
|
// This is necessary because ASMultiplexImageNode will try to grab the best image immediately, regardless of
|
|
// `downloadsIntermediateImages`.
|
|
id mockDataSource = [OCMockObject niceMockForProtocol:@protocol(ASMultiplexImageNodeDataSource)];
|
|
imageNode.dataSource = mockDataSource;
|
|
__block NSUInteger loadedImageCount = 0;
|
|
[[[mockDataSource stub] andDo:^(NSInvocation *inv) {
|
|
id requestedIdentifier;
|
|
[inv getArgument:&requestedIdentifier atIndex:3];
|
|
|
|
NSInteger requestedIdentifierValue = [requestedIdentifier intValue];
|
|
|
|
// If no images are loaded, bail on trying to load anything but the worst image.
|
|
if (!imageNode.loadedImageIdentifier && requestedIdentifierValue != [[imageIdentifiers lastObject] integerValue])
|
|
return;
|
|
|
|
// Bail if it's trying to load an identifier that's more than one step than what's loaded.
|
|
NSInteger nextImageIdentifier = [imageNode.loadedImageIdentifier integerValue] + 1;
|
|
if (requestedIdentifierValue != nextImageIdentifier)
|
|
return;
|
|
|
|
// Return the test image.
|
|
loadedImageCount++;
|
|
[inv setReturnValue:(void *)[self _testImage]];
|
|
}] multiplexImageNode:[OCMArg any] imageForImageIdentifier:[OCMArg any]];
|
|
|
|
imageNode.imageIdentifiers = imageIdentifiers;
|
|
|
|
XCTAssertTrue(loadedImageCount == identifierCount, @"Expected to load the same number of identifiers we supplied");
|
|
|
|
imageNode.delegate = nil;
|
|
imageNode.dataSource = nil;
|
|
[imageNode release];
|
|
}
|
|
|
|
- (void)testUncachedDownload
|
|
{
|
|
// Mock a cache miss.
|
|
id mockCache = [OCMockObject mockForProtocol:@protocol(ASImageCacheProtocol)];
|
|
[[[mockCache stub] andDo:^(NSInvocation *inv) {
|
|
void (^completion)(CGImageRef imageFromCache);
|
|
[inv getArgument:&completion atIndex:4];
|
|
completion(nil);
|
|
}] fetchCachedImageWithURL:[OCMArg any] callbackQueue:[OCMArg any] completion:[OCMArg any]];
|
|
|
|
// Mock a 50%-progress URL download.
|
|
id mockDownloader = [OCMockObject mockForProtocol:@protocol(ASImageDownloaderProtocol)];
|
|
const CGFloat mockedProgress = 0.5;
|
|
[[[mockDownloader stub] andDo:^(NSInvocation *inv) {
|
|
// Simulate progress.
|
|
void (^progressBlock)(CGFloat progress);
|
|
[inv getArgument:&progressBlock atIndex:4];
|
|
progressBlock(mockedProgress);
|
|
|
|
// Simulate completion.
|
|
void (^completionBlock)(CGImageRef image, NSError *error);
|
|
[inv getArgument:&completionBlock atIndex:5];
|
|
completionBlock([self _testImage].CGImage, nil);
|
|
}] downloadImageWithURL:[OCMArg any] callbackQueue:[OCMArg any] downloadProgressBlock:[OCMArg any] completion:[OCMArg any]];
|
|
|
|
ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:mockCache downloader:mockDownloader];
|
|
NSNumber *imageIdentifier = @1;
|
|
|
|
// Mock the data source to return our test URL.
|
|
id mockDataSource = [OCMockObject niceMockForProtocol:@protocol(ASMultiplexImageNodeDataSource)];
|
|
[[[mockDataSource stub] andReturn:[self _testImageURL]] multiplexImageNode:imageNode URLForImageIdentifier:imageIdentifier];
|
|
imageNode.dataSource = mockDataSource;
|
|
|
|
// Mock the delegate to expect start, 50% progress, and completion invocations.
|
|
id mockDelegate = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDelegate)];
|
|
[[mockDelegate expect] multiplexImageNode:imageNode didStartDownloadOfImageWithIdentifier:imageIdentifier];
|
|
[[mockDelegate expect] multiplexImageNode:imageNode didUpdateDownloadProgress:mockedProgress forImageWithIdentifier:imageIdentifier];
|
|
[[mockDelegate expect] multiplexImageNode:imageNode didFinishDownloadingImageWithIdentifier:imageIdentifier error:nil];
|
|
[[mockDelegate expect] multiplexImageNode:imageNode didUpdateImage:[OCMArg any] withIdentifier:imageIdentifier fromImage:nil withIdentifier:nil];
|
|
imageNode.delegate = mockDelegate;
|
|
|
|
// Kick off loading.
|
|
imageNode.imageIdentifiers = @[imageIdentifier];
|
|
|
|
// Wait until the image is loaded.
|
|
ASRunRunLoopUntilBlockIsTrue(^BOOL{
|
|
return [imageNode.loadedImageIdentifier isEqual:imageIdentifier];
|
|
});
|
|
|
|
// Verify the delegation.
|
|
[mockDelegate verify];
|
|
// Also verify that it's acutally loaded (could be false if we timed out above).
|
|
XCTAssertEqualObjects(imageNode.loadedImageIdentifier, imageIdentifier, @"Failed to load image");
|
|
|
|
[imageNode release];
|
|
}
|
|
|
|
@end |