ComponentKitTests/TransactionalDataSource/CKDataSourceChangesetApplicatorTests.mm (418 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 <mutex> #import <XCTest/XCTest.h> #import <ComponentKit/CKAnalyticsListener.h> #import <ComponentKit/CKComponent.h> #import <ComponentKit/CKDataSourceChange.h> #import <ComponentKit/CKDataSourceChangeset.h> #import <ComponentKit/CKDataSourceConfigurationInternal.h> #import <ComponentKit/CKDataSourceInternal.h> #import <ComponentKit/CKDataSourceListener.h> #import <ComponentKit/CKDataSourceStateInternal.h> #import <ComponentKit/CKDataSourceChangesetApplicator.h> #import <ComponentKitTestHelpers/CKTestRunLoopRunning.h> @interface CKDataSourceMock : CKDataSource // Indicates how many times `verifyChange:` is called. @property (nonatomic, readonly, assign) NSUInteger verifyChangeCount; // Indicates how many times `applyChange:` is called if change is verified. @property (nonatomic, readonly, assign) NSUInteger applyChangeCount; @property (nonatomic, copy) void(^willVerifyChange)(void); @property (nonatomic, copy) void(^didApplyChange)(void); - (void)sendNewState; - (void)sendNewStateWithSizeRange:(CKSizeRange)sizeRange; @end @interface CKDataSourceChangesetApplicatorTests : XCTestCase <CKAnalyticsListener> @end @implementation CKDataSourceChangesetApplicatorTests { CKDataSourceMock *_dataSource; CKDataSourceChangesetApplicator *_changesetApplicator; dispatch_queue_t _queue; std::atomic<NSUInteger> _buildComponentCount; UITraitCollection *_currentTraitCollection; } - (void)setUp { const auto configuration = [[CKDataSourceConfiguration alloc] initWithComponentProviderFunc:componentProvider context:nil sizeRange:{{100, 100}, {100, 100}} options:{} componentPredicates:{} componentControllerPredicates:{} analyticsListener:self]; const auto dataSourceState = [[CKDataSourceState alloc] initWithConfiguration:configuration sections:@[]]; _dataSource = [[CKDataSourceMock alloc] initWithState:dataSourceState]; _queue = dispatch_queue_create("CKDataSourceChangesetApplicator.Tests", DISPATCH_QUEUE_SERIAL); _changesetApplicator = [[CKDataSourceChangesetApplicator alloc] initWithDataSource:_dataSource queue:_queue]; _buildComponentCount = 0; } - (void)tearDown { _dataSource = nil; _changesetApplicator = nil; } - (void)testChangeIsAppliedAfterApplyChangesetIsCalled { dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset:defaultChangeset() userInfo:@{} qos:CKDataSourceQOSDefault]; }); [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self assertNumberOfSuccessfulChanges:1 numberOfFailedChanges:0]; } - (void)testChangesAreAppliedSequentiallyAfterApplyChangesetIsCalledMultipleTimes { dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset:defaultChangeset() userInfo:@{} qos:CKDataSourceQOSDefault]; [self->_changesetApplicator applyChangeset:defaultChangeset() userInfo:@{} qos:CKDataSourceQOSDefault]; }); [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self assertNumberOfSuccessfulChanges:2 numberOfFailedChanges:0]; } - (void)testChangesetsAreReappliedIfDataSourceStateIsChangedWhenProcessingChangesets { dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset:defaultChangeset() userInfo:@{} qos:CKDataSourceQOSDefault]; [self->_changesetApplicator applyChangeset:defaultChangeset() userInfo:@{} qos:CKDataSourceQOSDefault]; }); [_dataSource sendNewState]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self waitUntilChangesetApplicatorQueueIsIdle]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self assertNumberOfSuccessfulChanges:2 numberOfFailedChanges:2]; } - (void)testSecondChangesetIsReappliedIfDataSourceStateIsChangedWhenProcessingSecondChangeset { dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset:defaultChangeset() userInfo:@{} qos:CKDataSourceQOSDefault]; }); [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset:defaultChangeset() userInfo:@{} qos:CKDataSourceQOSDefault]; }); [_dataSource sendNewState]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self waitUntilChangesetApplicatorQueueIsIdle]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self assertNumberOfSuccessfulChanges:2 numberOfFailedChanges:1]; } - (void)testDataSourceItemCacheIsUsedWhenChangesetIsReapplied { dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset: [[[[CKDataSourceChangesetBuilder dataSourceChangeset] withInsertedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @0}] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] build] userInfo:@{} qos:CKDataSourceQOSDefault]; }); [_dataSource sendNewState]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self waitUntilChangesetApplicatorQueueIsIdle]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self assertNumberOfSuccessfulChanges:1 numberOfFailedChanges:1]; XCTAssertTrue(_buildComponentCount == 1, @"`dataSourceItem` should only be built once because of cache."); } - (void)testDataSourceItemCacheIsNotUsedIfConfigurationIsChangedWhenChangesetIsReapplied { dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset: [[[[CKDataSourceChangesetBuilder dataSourceChangeset] withInsertedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @0}] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] build] userInfo:@{} qos:CKDataSourceQOSDefault]; }); [_dataSource sendNewStateWithSizeRange:{{0, 0}, {200, 200}}]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self waitUntilChangesetApplicatorQueueIsIdle]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self assertNumberOfSuccessfulChanges:1 numberOfFailedChanges:1]; XCTAssertTrue(_buildComponentCount == 2, @"`dataSourceItem` should be built twice because cache is invalidated."); } - (void)testStateUpdatesArePausedInDataSourceAfterApplyChangesetIsCalled { XCTAssertFalse(_dataSource.shouldPauseStateUpdates); dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset:defaultChangeset() userInfo:@{} qos:CKDataSourceQOSDefault]; }); [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; XCTAssertTrue(_dataSource.shouldPauseStateUpdates); [self waitUntilChangesetApplicatorQueueIsIdle]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; // After changeset applicator finishes processing changeset, `shouldPauseStateUpdates` is set to NO. XCTAssertFalse(_dataSource.shouldPauseStateUpdates); } - (void)testSpiltChangesetIsAppliedWithoutDefferedChangesetWhenViewportIsLargeEnough { [self enableSplitChangeset]; [_changesetApplicator setViewPort:{.size = {100, 200}}]; dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset: [[[[CKDataSourceChangesetBuilder dataSourceChangeset] withInsertedItems:@{ [NSIndexPath indexPathForItem:0 inSection:0]: @0, [NSIndexPath indexPathForItem:1 inSection:0]: @1, }] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] build] userInfo:@{}qos:CKDataSourceQOSDefault]; }); [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self assertNumberOfSuccessfulChanges:1 numberOfFailedChanges:0]; } - (void)testSpiltChangesetIsAppliedWithDefferedChangesetWhenViewportIsNotLargeEnough { [self enableSplitChangeset]; [_changesetApplicator setViewPort:{.size = {100, 100}}]; dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset: [[[[CKDataSourceChangesetBuilder dataSourceChangeset] withInsertedItems:@{ [NSIndexPath indexPathForItem:0 inSection:0]: @0, [NSIndexPath indexPathForItem:1 inSection:0]: @1, }] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] build] userInfo:@{}qos:CKDataSourceQOSDefault]; }); [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self assertNumberOfSuccessfulChanges:2 numberOfFailedChanges:0]; } - (void)testSpiltChangesetIsReappliedWithoutDefferedChangesetWhenDataSourceStateIsChanged { [self enableSplitChangeset]; [_changesetApplicator setViewPort:{.size = {100, 200}}]; dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset: [[[[CKDataSourceChangesetBuilder dataSourceChangeset] withInsertedItems:@{ [NSIndexPath indexPathForItem:0 inSection:0]: @0, [NSIndexPath indexPathForItem:1 inSection:0]: @1, }] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] build] userInfo:@{}qos:CKDataSourceQOSDefault]; }); [_dataSource sendNewState]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self waitUntilChangesetApplicatorQueueIsIdle]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self assertNumberOfSuccessfulChanges:1 numberOfFailedChanges:1]; } - (void)testSpiltChangesetIsReappliedWithDefferedChangesetWhenDataSourceStateIsChanged { [self enableSplitChangeset]; [_changesetApplicator setViewPort:{.size = {100, 100}}]; dispatch_sync(_queue, ^{ [self->_changesetApplicator applyChangeset: [[[[CKDataSourceChangesetBuilder dataSourceChangeset] withInsertedItems:@{ [NSIndexPath indexPathForItem:0 inSection:0]: @0, [NSIndexPath indexPathForItem:1 inSection:0]: @1, }] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] build] userInfo:@{}qos:CKDataSourceQOSDefault]; }); [_dataSource sendNewState]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self waitUntilChangesetApplicatorQueueIsIdle]; [self waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue]; [self assertNumberOfSuccessfulChanges:2 numberOfFailedChanges:2]; } - (void)testCurrentTraitCollectionIsCorrectInWorkQueue { if (@available(iOS 13.0, tvOS 13.0, *)) { [_changesetApplicator setTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceIdiom:UIUserInterfaceIdiomCarPlay]]; [_changesetApplicator applyChangeset: [[[[CKDataSourceChangesetBuilder dataSourceChangeset] withInsertedItems:@{ [NSIndexPath indexPathForItem:0 inSection:0]: @0, }] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] build] userInfo:@{} qos:CKDataSourceQOSDefault]; CKRunRunLoopUntilBlockIsTrue(^BOOL{ return _buildComponentCount == 1; }); XCTAssertEqual(_currentTraitCollection.userInterfaceIdiom, UIUserInterfaceIdiomCarPlay); } } static CKComponent *componentProvider(id<NSObject> model, id<NSObject> context) { return CK::ComponentBuilder() .size({100, 100}) .build(); } #pragma mark - CKAnalyticsListener - (void)willBuildComponentTreeWithScopeRoot:(CKComponentScopeRoot *)scopeRoot buildTrigger:(CKBuildTrigger)buildTrigger stateUpdates:(const CKComponentStateUpdateMap &)stateUpdates { if (@available(iOS 13.0, tvOS 13.0, *)) { _currentTraitCollection = [UITraitCollection currentTraitCollection]; } _buildComponentCount++; } - (void)didBuildComponentTreeWithScopeRoot:(CKComponentScopeRoot *)scopeRoot buildTrigger:(CKBuildTrigger)buildTrigger stateUpdates:(const CKComponentStateUpdateMap &)stateUpdates component:(CKComponent *)component boundsAnimation:(const CKComponentBoundsAnimation &)boundsAnimation { } - (void)didCollectAnimations:(const CKComponentAnimations &)animations fromComponents:(const CK::ComponentTreeDiff &)animatedComponents inComponentTreeWithRootComponent:(id<CKMountable>)component scopeRootIdentifier:(CKComponentScopeRootIdentifier)scopeRootID { } - (void)didLayoutComponentTreeWithRootComponent:(id<CKMountable>)component { } - (void)didMountComponentTreeWithRootComponent:(id<CKMountable>)component mountAnalyticsContext:(CK::Optional<CK::Component::MountAnalyticsContext>)mountAnalyticsContext { } - (void)didReuseNode:(CKTreeNode *)node inScopeRoot:(CKComponentScopeRoot *)scopeRoot fromPreviousScopeRoot:(CKComponentScopeRoot *)previousScopeRoot { } - (BOOL)shouldCollectMountInformationForRootComponent:(CKComponent *)component { return NO; } - (id<CKSystraceListener>)systraceListener { return nil; } - (BOOL)shouldCollectTreeNodeCreationInformation:(CKComponentScopeRoot *)scopeRoot { return NO; } - (void)didBuildTreeNodeForPrecomputedChild:(id<CKComponentProtocol>)component node:(CKTreeNode *)node parent:(CKTreeNode *)parent params:(const CKBuildComponentTreeParams &)params parentHasStateUpdate:(BOOL)parentHasStateUpdate {} - (void)willCollectAnimationsFromComponentTreeWithRootComponent:(id<CKMountable>)component { } - (void)willLayoutComponentTreeWithRootComponent:(id<CKMountable>)component buildTrigger:(CK::Optional<CKBuildTrigger>)buildTrigger { } - (void)willMountComponentTreeWithRootComponent:(id<CKMountable>)component { } - (void)didReceiveStateUpdateFromScopeHandle:(CKComponentScopeHandle *)handle rootIdentifier:(CKComponentScopeRootIdentifier)rootID { } #pragma mark - Helpers - (void)enableSplitChangeset { const auto preivousConfiguration = _dataSource.state.configuration; const auto configuration = [[CKDataSourceConfiguration alloc] initWithComponentProviderFunc:componentProvider context:preivousConfiguration.context sizeRange:preivousConfiguration.sizeRange options:{ .splitChangesetOptions = { .enabled = YES, }, } componentPredicates:preivousConfiguration.componentPredicates componentControllerPredicates:preivousConfiguration.componentControllerPredicates analyticsListener:preivousConfiguration.analyticsListener]; [_dataSource updateConfiguration:configuration mode:CKUpdateModeSynchronous userInfo:@{}]; } - (void)waitUntilChangesetApplicatorFinishesItsTasksOnMainQueue { __block BOOL didRun = NO; dispatch_async(dispatch_get_main_queue(), ^{ didRun = YES; }); CKRunRunLoopUntilBlockIsTrue(^BOOL{ return didRun; }); } - (void)waitUntilChangesetApplicatorQueueIsIdle { // Use `dispatch_sync` to block the main queue until the queue finishes its current task. dispatch_sync(_queue, ^{}); } - (void)assertNumberOfSuccessfulChanges:(NSUInteger)numberOfSuccessfulChanges numberOfFailedChanges:(NSUInteger)numberOfFailedChanges { // Number is doubled because internally `applyChange` calls `verifyChange` as well. XCTAssertEqual(_dataSource.verifyChangeCount, (numberOfFailedChanges + numberOfSuccessfulChanges) * 2); XCTAssertEqual(_dataSource.applyChangeCount, numberOfSuccessfulChanges); } static CKDataSourceChangeset *defaultChangeset() { return [[CKDataSourceChangesetBuilder dataSourceChangeset] build]; } @end #pragma mark - CKDataSourceMock @implementation CKDataSourceMock - (BOOL)applyChange:(CKDataSourceChange *)change { const auto applied = [super applyChange:change]; if (applied) { _applyChangeCount++; if (_didApplyChange) { _didApplyChange(); } } return applied; } - (BOOL)verifyChange:(CKDataSourceChange *)change { _verifyChangeCount++; if (_willVerifyChange) { _willVerifyChange(); } return [super verifyChange:change]; } - (void)sendNewState { [self sendNewStateWithSizeRange:self.state.configuration.sizeRange]; } - (void)sendNewStateWithSizeRange:(CKSizeRange)sizeRange { const auto configuration = [[CKDataSourceConfiguration alloc] initWithComponentProviderFunc:componentProvider context:self.state.configuration.context sizeRange:sizeRange options:self.state.configuration.options componentPredicates:self.state.configuration.componentPredicates componentControllerPredicates:self.state.configuration.componentControllerPredicates analyticsListener:self.state.configuration.analyticsListener]; [self updateConfiguration:configuration mode:CKUpdateModeSynchronous userInfo:@{}]; } @end