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