ComponentKitTests/StatefulViews/CKStatefulViewReusePoolTests.mm (407 lines of code) (raw):
/*
* 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 <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import <ComponentKitTestHelpers/CKTestRunLoopRunning.h>
#import <ComponentKit/CKStatefulViewReusePool.h>
#import "CKTestStatefulViewComponent.h"
@interface CKStatefulViewReusePoolTests : XCTestCase
@end
@interface CKOtherStatefulViewComponentController : CKStatefulViewComponentController
@end
@interface CKStatefulViewComponentWithMaximumController : CKStatefulViewComponentController
@end
@implementation CKStatefulViewComponentWithMaximumController
+ (NSInteger)maximumPoolSize:(id)context
{
return 1;
}
@end
@interface CKStatefulViewComponentInvalidatableContext: NSObject
@property (assign, nonatomic) BOOL isValid;
@end
@implementation CKStatefulViewComponentInvalidatableContext
@end
@interface CKStatefulViewComponentWithInvalidContextController : CKStatefulViewComponentController
@end
@implementation CKStatefulViewComponentWithInvalidContextController
+ (BOOL)isContextValid:(CKStatefulViewComponentInvalidatableContext *)context {
return context.isValid;
}
@end
@interface EnqueueOnDealloc: NSObject
+ (instancetype)newWithPool:(CKStatefulViewReusePool *)pool;
@end
@implementation CKStatefulViewReusePoolTests
- (void)testDequeueingFromEmptyPoolReturnsNil
{
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
UIView *container = [[UIView alloc] init];
XCTAssertNil([pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container
context:nil], @"Didn't expect to vend view from empty pool");
}
- (void)testEnqueueingViewThenDequeueingReturnsSameView
{
__block BOOL calledBlock = NO;
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
CKTestStatefulView *view = [[CKTestStatefulView alloc] init];
[pool enqueueStatefulView:view
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlock = YES;
return YES;
}];
CKRunRunLoopUntilBlockIsTrue(^BOOL{
return calledBlock;
});
UIView *container = [[UIView alloc] init];
UIView *dequeuedView = [pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container
context:nil];
XCTAssertEqualObjects(dequeuedView, view, @"Expected enqueued view to be returned");
}
- (void)testEnqueueingViewThenDequeueingWhileRefusingToRelinquishReturnsNil
{
__block BOOL calledBlock = NO;
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
CKTestStatefulView *view = [[CKTestStatefulView alloc] init];
[pool enqueueStatefulView:view
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlock = YES;
return NO;
}];
CKRunRunLoopUntilBlockIsTrue(^BOOL{
return calledBlock;
});
UIView *container = [[UIView alloc] init];
UIView *dequeuedView = [pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container
context:nil];
XCTAssertTrue(calledBlock && dequeuedView == nil, @"Expected dequeued view to be nil");
}
- (void)testEnqueueingViewThenDequeueingWithDifferentControllerClassReturnsNil
{
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
CKTestStatefulView *view = [[CKTestStatefulView alloc] init];
[pool enqueueStatefulView:view
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
return YES;
}];
UIView *container = [[UIView alloc] init];
XCTAssertNil([pool dequeueStatefulViewForControllerClass:[CKOtherStatefulViewComponentController class]
preferredSuperview:container
context:nil], @"Didn't expect to vend view, controller mismatch");
}
- (void)testEnqueueingTwoViewsThenDequeueingWithPreferredSuperviewReturnsViewWithMatchingSuperview
{
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
__block int blockCallCount = 0;
UIView *container1 = [[UIView alloc] init];
CKTestStatefulView *view1 = [[CKTestStatefulView alloc] init];
[container1 addSubview:view1];
[pool enqueueStatefulView:view1
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
blockCallCount++;
return YES;
}];
UIView *container2 = [[UIView alloc] init];
CKTestStatefulView *view2 = [[CKTestStatefulView alloc] init];
[container2 addSubview:view2];
[pool enqueueStatefulView:view2
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
blockCallCount++;
return YES;
}];
CKRunRunLoopUntilBlockIsTrue(^BOOL{
return blockCallCount == 2;
});
UIView *dequeuedView = [pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container1
context:nil];
XCTAssertTrue(dequeuedView == view1, @"Expected view in container1 to be returned");
}
- (void)testDequeueingViewDoesNotLaterDequeueTheSameViewForTheOriginalSuperview
{
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
CKTestStatefulView *view = [[CKTestStatefulView alloc] init];
UIView *container1 = [[UIView alloc] init];
[container1 addSubview:view];
[pool enqueueStatefulView:view
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
return YES;
}];
UIView *container2 = [[UIView alloc] init];
[pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container2
context:nil];
XCTAssertNil([pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container1
context:nil], @"Didn't expect to vend view.");
}
- (void)testEnqueueingViewThenDequeueingWithDifferentContextReturnsNewView
{
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
UIView *containerView = [[UIView alloc] init];
UIView *firstView =[[UIView alloc] init];
[pool enqueueStatefulView:firstView
forControllerClass:[CKTestStatefulViewComponentController class]
context:@"context1"
mayRelinquishBlock:^BOOL{
return YES;
}];
UIView *dequeuedView = [pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:containerView
context:@"context2"];
XCTAssertTrue(firstView != dequeuedView, @"Expected different view to be vended.");
}
- (void)testEnqueueingOneViewThatLostItSuperviewThenDequeueingWithDifferentPreferredSuperviews
{
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
__block BOOL calledBlock = NO;
UIView *container1 = [[UIView alloc] init];
CKTestStatefulView *view = [[CKTestStatefulView alloc] init];
[container1 addSubview:view];
[pool enqueueStatefulView:view
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlock = YES;
return YES;
}];
CKRunRunLoopUntilBlockIsTrue(^BOOL{
return calledBlock;
});
// remove the statefull view from the container
[view removeFromSuperview];
UIView *container2 = [[UIView alloc] init];
UIView *dequeuedView = [pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container2
context:nil];
XCTAssertTrue(dequeuedView == view, @"Expected view in container1 to be returned");
XCTAssertNil([pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container1
context:nil], @"Didn't expect to vend view from empty pool");
XCTAssertNil([pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container2
context:nil], @"Didn't expect to vend view from empty pool");
}
- (void)testMaximumPoolSizeOfOneByEnqueueingTwoViewsThenDequeueingTwoViewsReturnsNewView
{
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
__block int calledBlockCount = 0;
UIView *container1 = [[UIView alloc] init];
CKTestStatefulView *view1 = [[CKTestStatefulView alloc] init];
[container1 addSubview:view1];
[pool enqueueStatefulView:view1
forControllerClass:[CKStatefulViewComponentWithMaximumController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlockCount++;
return YES;
}];
UIView *container2 = [[UIView alloc] init];
CKTestStatefulView *view2 = [[CKTestStatefulView alloc] init];
[container2 addSubview:view2];
[pool enqueueStatefulView:view2
forControllerClass:[CKStatefulViewComponentWithMaximumController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlockCount++;
return YES;
}];
CKRunRunLoopUntilBlockIsTrue(^BOOL{
return calledBlockCount == 2;
});
UIView *dequeuedView1 = [pool dequeueStatefulViewForControllerClass:[CKStatefulViewComponentWithMaximumController class]
preferredSuperview:container1
context:nil];
XCTAssertTrue(dequeuedView1 == view1, @"Expected view in container1 to be returned");
UIView *dequeuedView2 = [pool dequeueStatefulViewForControllerClass:[CKStatefulViewComponentWithMaximumController class]
preferredSuperview:container2
context:nil];
XCTAssertTrue(dequeuedView2 != view2, @"Didn't expect view in container2 to be returned");
}
- (void)testInvalidContextEnqueue
{
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
CKStatefulViewComponentInvalidatableContext *context = [CKStatefulViewComponentInvalidatableContext new];
__block int calledBlockCount = 0;
UIView *container1 = [[UIView alloc] init];
CKTestStatefulView *view1 = [[CKTestStatefulView alloc] init];
[container1 addSubview:view1];
[pool enqueueStatefulView:view1
forControllerClass:[CKStatefulViewComponentWithInvalidContextController class]
context:context
mayRelinquishBlock:^BOOL{
return YES;
}];
UIView *container2 = [[UIView alloc] init];
CKTestStatefulView *view2 = [[CKTestStatefulView alloc] init];
[container2 addSubview:view2];
[pool enqueueStatefulView:view2
forControllerClass:[CKOtherStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlockCount++;
return YES;
}];
CKRunRunLoopUntilBlockIsTrue(^BOOL{
return calledBlockCount == 1;
});
UIView *dequeuedView1 = [pool dequeueStatefulViewForControllerClass:[CKStatefulViewComponentWithInvalidContextController class]
preferredSuperview:container1
context:nil];
XCTAssertTrue(dequeuedView1 == nil, @"Expected nil to be returned, since context is invalid");
UIView *dequeuedView2 = [pool dequeueStatefulViewForControllerClass:[CKOtherStatefulViewComponentController class]
preferredSuperview:container2
context:nil];
XCTAssertTrue(dequeuedView2 == view2, @"View with valid context should be returned");
}
- (void)testInvalidContextAfterEnqueue
{
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
CKStatefulViewComponentInvalidatableContext *context = [CKStatefulViewComponentInvalidatableContext new];
context.isValid = YES;
__block int calledBlockCount = 0;
UIView *container1 = [[UIView alloc] init];
CKTestStatefulView *view1 = [[CKTestStatefulView alloc] init];
[container1 addSubview:view1];
[pool enqueueStatefulView:view1
forControllerClass:[CKStatefulViewComponentWithInvalidContextController class]
context:context
mayRelinquishBlock:^BOOL{
return YES;
}];
context.isValid = NO;
UIView *container2 = [[UIView alloc] init];
CKTestStatefulView *view2 = [[CKTestStatefulView alloc] init];
[container2 addSubview:view2];
[pool enqueueStatefulView:view2
forControllerClass:[CKOtherStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlockCount++;
return YES;
}];
CKRunRunLoopUntilBlockIsTrue(^BOOL{
return calledBlockCount == 1;
});
UIView *dequeuedView1 = [pool dequeueStatefulViewForControllerClass:[CKStatefulViewComponentWithInvalidContextController class]
preferredSuperview:container1
context:nil];
XCTAssertTrue(dequeuedView1 == nil, @"Expected nil to be returned, since context is invalid");
UIView *dequeuedView2 = [pool dequeueStatefulViewForControllerClass:[CKOtherStatefulViewComponentController class]
preferredSuperview:container2
context:nil];
XCTAssertTrue(dequeuedView2 == view2, @"View with valid context should be returned");
}
#pragma mark - Pending pool tests
- (void)testEnqueueingViewThenDequeueingWithPendingEnabledReturnsSameViewImmediately
{
__block BOOL calledBlock = NO;
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
// Warm up the pool so that pending reuse will occur
[pool enqueueStatefulView:[[CKTestStatefulView alloc] init]
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlock = YES;
return YES;
}];
CKRunRunLoopUntilBlockIsTrue(^BOOL{
return calledBlock;
});
[pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:nil
context:nil];
calledBlock = NO;
CKTestStatefulView *view = [[CKTestStatefulView alloc] init];
[pool enqueueStatefulView:view
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlock = YES;
return YES;
}];
UIView *container = [[UIView alloc] init];
UIView *dequeuedView = [pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container
context:nil];
XCTAssertTrue(calledBlock);
XCTAssertEqualObjects(dequeuedView, view, @"Expected enqueued view to be returned");
}
- (void)testEnqueueingViewThenDequeueingWhileRefusingToRelinquishWithPendingEnabledReturnsNilImmediately
{
__block BOOL calledBlock = NO;
CKStatefulViewReusePool *pool = [[CKStatefulViewReusePool alloc] init];
// Warm up the pool so that pending reuse will occur
[pool enqueueStatefulView:[[CKTestStatefulView alloc] init]
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlock = YES;
return YES;
}];
CKRunRunLoopUntilBlockIsTrue(^BOOL{
return calledBlock;
});
[pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:nil
context:nil];
calledBlock = NO;
CKTestStatefulView *view = [[CKTestStatefulView alloc] init];
[pool enqueueStatefulView:view
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^BOOL{
calledBlock = YES;
return NO;
}];
UIView *container = [[UIView alloc] init];
UIView *dequeuedView = [pool dequeueStatefulViewForControllerClass:[CKTestStatefulViewComponentController class]
preferredSuperview:container
context:nil];
XCTAssertTrue(calledBlock);
XCTAssertTrue(dequeuedView == nil, @"Expected dequeued view to be nil");
}
- (void)test_WhenPurgingPendingPoolLeadsToEnqueueing_DoesNotCrash {
for (auto i = 0; i < 10000; i++) {
auto pool = [CKStatefulViewReusePool new];
auto eod = [EnqueueOnDealloc newWithPool:pool];
[pool enqueueStatefulView:[CKTestStatefulView new]
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^{
// Capturing what would be the only strong reference to eod when the block runs.
// This will cause eod to be released after the block returns.
(void)eod.class;
return YES;
}];
eod = nil;
}
}
@end
@implementation CKOtherStatefulViewComponentController
@end
@implementation EnqueueOnDealloc {
CKStatefulViewReusePool *_pool;
}
+ (instancetype)newWithPool:(CKStatefulViewReusePool *)pool {
auto obj = [super new];
obj->_pool = pool;
return obj;
}
- (void)dealloc {
[_pool enqueueStatefulView:[CKTestStatefulView new]
forControllerClass:[CKTestStatefulViewComponentController class]
context:nil
mayRelinquishBlock:^{
return YES;
}];
}
@end