From fcf2db79f863c4d375b451c7656239f5468a6f54 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 21 Jun 2016 18:46:08 -0700 Subject: [PATCH 01/10] [ASTableViewThrashTests] Initial commit --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 + .../Private/ASMultidimensionalArrayUtils.mm | 2 +- AsyncDisplayKitTests/ASTableViewThrashTests.m | 391 ++++++++++++++++++ 3 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 AsyncDisplayKitTests/ASTableViewThrashTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 8ed10ccad0..62c3baffc7 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -545,6 +545,7 @@ CC3B208C1C3F7A5400798563 /* ASWeakSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3B20881C3F7A5400798563 /* ASWeakSet.m */; }; CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */; }; CC3B20901C3F892D00798563 /* ASBridgedPropertiesTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */; }; + CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */; }; CC7FD9DE1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC7FD9DF1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */; }; CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */; }; @@ -935,6 +936,7 @@ CC3B20881C3F7A5400798563 /* ASWeakSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASWeakSet.m; sourceTree = ""; }; CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASWeakSetTests.m; sourceTree = ""; }; CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBridgedPropertiesTests.mm; sourceTree = ""; }; + CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewThrashTests.m; sourceTree = ""; }; CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = ""; }; CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequest.m; sourceTree = ""; }; CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequestTests.m; sourceTree = ""; }; @@ -1201,6 +1203,7 @@ 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */, 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */, 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */, + CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */, 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m */, 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */, 254C6B531BF8FF2A003EC431 /* ASTextKitTests.mm */, @@ -2171,6 +2174,7 @@ 2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m in Sources */, 058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.m in Sources */, 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.m in Sources */, + CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */, 058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */, 056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.m in Sources */, AC026B581BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m in Sources */, diff --git a/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm b/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm index 2652912915..5c7e437387 100644 --- a/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm +++ b/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm @@ -41,7 +41,7 @@ static void ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(NSMutableArray } } -static void ASRecursivelyFindIndexPathsForMultidimensionalArray(NSObject *obj, NSIndexPath *curIndexPath, NSMutableArray *res) +static void ASRecursivelyFindIndexPathsForMultidimensionalArray(NSObject *obj, NSIndexPath *curIndexPath, NSMutableArray *res) { if (![obj isKindOfClass:[NSArray class]]) { [res addObject:curIndexPath]; diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m new file mode 100644 index 0000000000..79cf4ef084 --- /dev/null +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -0,0 +1,391 @@ +// +// ASTableViewThrashTests.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 6/21/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +@import XCTest; +#import + +typedef NS_ENUM(NSUInteger, ASThrashChangeType) { + ASThrashReplaceItem, + ASThrashReplaceSection, + ASThrashDeleteItem, + ASThrashDeleteSection, + ASThrashInsertItem, + ASThrashInsertSection +}; + +#define USE_UIKIT_REFERENCE 1 +#define kInitialSectionCount 6 +#define kInitialItemCount 6 + +#if USE_UIKIT_REFERENCE +#define kCellReuseID @"ASThrashTestCellReuseID" +#endif + +static NSString *ASThrashArrayDescription(NSArray *array) { + NSMutableString *str = [NSMutableString stringWithString:@"(\n"]; + NSInteger i = 0; + for (id obj in array) { + [str appendFormat:@"\t[%ld]: \"%@\",\n", i, obj]; + i += 1; + } + [str appendString:@")"]; + return str; +} +@interface ASThrashTestItem: NSObject +#if USE_UIKIT_REFERENCE +/// This is used to identify the row with the table view (UIKit only). +@property (nonatomic, readonly) CGFloat rowHeight; +#endif +@end + +@implementation ASThrashTestItem + +- (instancetype)init { + self = [super init]; + if (self != nil) { +#if USE_UIKIT_REFERENCE + _rowHeight = arc4random_uniform(500); +#endif + } + return self; +} + ++ (NSArray *)itemsWithCount:(NSInteger)count { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; + for (NSInteger i = 0; i < count; i += 1) { + [result addObject:[[ASThrashTestItem alloc] init]]; + } + return result; +} + +- (NSString *)description { +#if USE_UIKIT_REFERENCE + return [NSString stringWithFormat:@"", (unsigned long)self.rowHeight]; +#else + return [NSString stringWithFormat:@"", self]; +#endif +} + +@end + +@interface ASThrashTestSection: NSObject +@property (nonatomic, strong, readonly) NSMutableArray *items; +/// This is used to identify the section with the table view. +@property (nonatomic, readonly) CGFloat headerHeight; +@end + +@implementation ASThrashTestSection + +- (instancetype)initWithCount:(NSInteger)count { + self = [super init]; + if (self != nil) { + _items = [NSMutableArray arrayWithCapacity:count]; + _headerHeight = arc4random_uniform(500) + 1; + for (NSInteger i = 0; i < count; i++) { + [_items addObject:[ASThrashTestItem new]]; + } + } + return self; +} + +- (instancetype)init { + return [self initWithCount:0]; +} + ++ (NSMutableArray *)sectionsWithCount:(NSInteger)count { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; + for (NSInteger i = 0; i < count; i += 1) { + [result addObject:[[ASThrashTestSection alloc] initWithCount:kInitialItemCount]]; + } + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", (unsigned long)self.headerHeight, (unsigned long)self.items.count]; +} + +@end + +#if !USE_UIKIT_REFERENCE +@interface ASThrashTestNode: ASCellNode +@property (nonatomic, strong) ASThrashTestItem *item; +@end + +@implementation ASThrashTestNode + +@end +#endif + +@interface ASThrashDataSource: NSObject +#if USE_UIKIT_REFERENCE + +#else + +#endif +@property (nonatomic, strong, readonly) NSMutableArray *data; +@end + + +@implementation ASThrashDataSource + +- (instancetype)init { + self = [super init]; + if (self != nil) { + _data = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + } + return self; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.data[section].items.count; +} + + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return self.data.count; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { + return self.data[section].headerHeight; +} + +#if USE_UIKIT_REFERENCE + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + return [tableView dequeueReusableCellWithIdentifier:kCellReuseID forIndexPath:indexPath]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item]; + return item.rowHeight; +} + +#else + +- (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath { + ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item]; + return ^{ + ASThrashTestNode *tableNode = [[ASThrashTestNode alloc] init]; + tableNode.item = item; + return tableNode; + }; +} + +#endif + +@end + + +@implementation NSIndexSet (ASThrashHelpers) + +- (NSArray *)indexPathsInSection:(NSInteger)section { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + [self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [result addObject:[NSIndexPath indexPathForItem:idx inSection:section]]; + }]; + return result; +} + +@end + +@interface ASTableViewThrashTests: XCTestCase +@end + +@implementation ASTableViewThrashTests { + CGRect screenBounds; + ASThrashDataSource *ds; + UIWindow *window; +#if USE_UIKIT_REFERENCE + UITableView *tableView; +#else + ASTableNode *tableNode; + ASTableView *tableView; +#endif + + NSInteger minimumItemCount; + NSInteger minimumSectionCount; + float fickleness; +} + +- (void)setUp { + minimumItemCount = 5; + minimumSectionCount = 3; + fickleness = 0.1; + window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ds = [[ASThrashDataSource alloc] init]; +#if USE_UIKIT_REFERENCE + tableView = [[UITableView alloc] initWithFrame:window.bounds style:UITableViewStyleGrouped]; + [window addSubview:tableView]; + tableView.dataSource = ds; + tableView.delegate = ds; + [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID]; + [window layoutIfNeeded]; +#else + tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + tableNode.frame = window.bounds; + [window addSubnode:tableNode]; + tableNode.dataSource = ds; + tableNode.delegate = ds; + [tableView reloadDataImmediately]; +#endif + +} + +- (void)testInitialDataRead { + [self verifyTableStateWithHierarchy]; +} + +- (void)testThrashingWildly { + for (NSInteger i = 0; i < 100; i++) { + [self _testThrashingWildly]; + } +} + +- (void)_testThrashingWildly { + NSLog(@"Old data: %@", ASThrashArrayDescription(ds.data)); + NSMutableArray *deletedItems = [NSMutableArray array]; + NSMutableArray *replacedItems = [NSMutableArray array]; + NSMutableArray *insertedItems = [NSMutableArray array]; + NSInteger i = 0; + + // Randomly reload some items + for (ASThrashTestSection *section in ds.data) { + NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; + [replacedItems addObject:indexes]; + i += 1; + } + + // Randomly replace some sections + NSMutableIndexSet *replacedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO]; + NSArray *replacingSections = [ASThrashTestSection sectionsWithCount:replacedSections.count]; + [ds.data replaceObjectsAtIndexes:replacedSections withObjects:replacingSections]; + + // Randomly delete some items + i = 0; + for (ASThrashTestSection *section in ds.data) { + if (section.items.count >= minimumItemCount) { + NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO]; + + /// Cannot reload & delete the same item. + [indexes removeIndexes:replacedItems[i]]; + + [section.items removeObjectsAtIndexes:indexes]; + [deletedItems addObject:indexes]; + } else { + [deletedItems addObject:[NSMutableIndexSet indexSet]]; + } + i += 1; + } + + // Randomly delete some sections + NSMutableIndexSet *deletedSections = nil; + if (ds.data.count >= minimumSectionCount) { + deletedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO]; + + // Cannot reload & delete the same section. + [deletedSections removeIndexes:replacedSections]; + } else { + deletedSections = [NSMutableIndexSet indexSet]; + } + [ds.data removeObjectsAtIndexes:deletedSections]; + + // Randomly insert some sections + NSMutableIndexSet *insertedSections = [self randomIndexesLessThan:(ds.data.count + 1) probability:fickleness insertMode:YES]; + NSArray *newSections = [ASThrashTestSection sectionsWithCount:insertedSections.count]; + [ds.data insertObjects:newSections atIndexes:insertedSections]; + + // Randomly insert some items + i = 0; + for (ASThrashTestSection *section in ds.data) { + NSMutableIndexSet *indexes = [self randomIndexesLessThan:(section.items.count + 1) probability:fickleness insertMode:YES]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items insertObjects:newItems atIndexes:indexes]; + [insertedItems addObject:indexes]; + i += 1; + } + + NSLog(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); + + // TODO: Submit changes in random order, randomly chunked up + + [tableView beginUpdates]; + i = 0; + for (NSIndexSet *indexes in insertedItems) { + NSArray *indexPaths = [indexes indexPathsInSection:i]; + NSLog(@"Requested to insert rows: %@", indexPaths); + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + i += 1; + } + + [tableView insertSections:insertedSections withRowAnimation:UITableViewRowAnimationNone]; + + [tableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationNone]; + + i = 0; + for (NSIndexSet *indexes in deletedItems) { + NSArray *indexPaths = [indexes indexPathsInSection:i]; + NSLog(@"Requested to delete rows: %@", indexPaths); + [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + i += 1; + } + + i = 0; + for (NSIndexSet *indexes in replacedItems) { + NSArray *indexPaths = [indexes indexPathsInSection:i]; + NSLog(@"Requested to reload rows: %@", indexPaths); + [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + i += 1; + } + + [tableView endUpdates]; +#if !USE_UIKIT_REFERENCE + [tableView waitUntilAllUpdatesAreCommitted]; +#endif + [self verifyTableStateWithHierarchy]; +} + +/// `insertMode` means that for each index selected, the max goes up by one. +- (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode { + NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init]; + u_int32_t cutoff = probability * 100; + for (NSInteger i = 0; i < max; i++) { + if (arc4random_uniform(100) < cutoff) { + [indexes addIndex:i]; + if (insertMode) { + max += 1; + } + } + } + return indexes; +} + +#pragma mark Helpers + +- (void)verifyTableStateWithHierarchy { + NSArray *data = [ds data]; + XCTAssertEqual(data.count, tableView.numberOfSections); + for (NSInteger i = 0; i < tableView.numberOfSections; i++) { + XCTAssertEqual([tableView numberOfRowsInSection:i], data[i].items.count); + XCTAssertEqual([tableView rectForHeaderInSection:i].size.height, data[i].headerHeight); + + for (NSInteger j = 0; j < [tableView numberOfRowsInSection:i]; j++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i]; + ASThrashTestItem *item = data[i].items[j]; +#if USE_UIKIT_REFERENCE + XCTAssertEqual([tableView rectForRowAtIndexPath:indexPath].size.height, item.rowHeight); +#else + ASThrashTestNode *node = (ASThrashTestNode *)[tableView nodeForRowAtIndexPath:indexPath]; + XCTAssertEqual(node.item, item); +#endif + } + } +} + +@end From 64835c0db7abe5d7f7300cf252939637234f77c1 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 13:04:47 -0700 Subject: [PATCH 02/10] [ASThrashTesting] It's working! + Cleanup --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 111 ++++++++++-------- 1 file changed, 64 insertions(+), 47 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index 79cf4ef084..df16362774 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -18,14 +18,20 @@ typedef NS_ENUM(NSUInteger, ASThrashChangeType) { ASThrashInsertSection }; -#define USE_UIKIT_REFERENCE 1 -#define kInitialSectionCount 6 -#define kInitialItemCount 6 +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +#define USE_UIKIT_REFERENCE 0 + +#define kInitialSectionCount 20 +#define kInitialItemCount 20 #if USE_UIKIT_REFERENCE #define kCellReuseID @"ASThrashTestCellReuseID" #endif +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" static NSString *ASThrashArrayDescription(NSArray *array) { NSMutableString *str = [NSMutableString stringWithString:@"(\n"]; NSInteger i = 0; @@ -36,6 +42,8 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [str appendString:@")"]; return str; } +#pragma clang diagnostic pop + @interface ASThrashTestItem: NSObject #if USE_UIKIT_REFERENCE /// This is used to identify the row with the table view (UIKit only). @@ -75,6 +83,7 @@ static NSString *ASThrashArrayDescription(NSArray *array) { @interface ASThrashTestSection: NSObject @property (nonatomic, strong, readonly) NSMutableArray *items; + /// This is used to identify the section with the table view. @property (nonatomic, readonly) CGFloat headerHeight; @end @@ -170,9 +179,9 @@ static NSString *ASThrashArrayDescription(NSArray *array) { - (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath { ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item]; return ^{ - ASThrashTestNode *tableNode = [[ASThrashTestNode alloc] init]; - tableNode.item = item; - return tableNode; + ASThrashTestNode *node = [[ASThrashTestNode alloc] init]; + node.item = item; + return node; }; } @@ -197,13 +206,11 @@ static NSString *ASThrashArrayDescription(NSArray *array) { @end @implementation ASTableViewThrashTests { - CGRect screenBounds; ASThrashDataSource *ds; UIWindow *window; #if USE_UIKIT_REFERENCE UITableView *tableView; #else - ASTableNode *tableNode; ASTableView *tableView; #endif @@ -226,7 +233,8 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID]; [window layoutIfNeeded]; #else - tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + ASTableNode *tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + tableView = tableNode.view; tableNode.frame = window.bounds; [window addSubnode:tableNode]; tableNode.dataSource = ds; @@ -240,18 +248,25 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [self verifyTableStateWithHierarchy]; } -- (void)testThrashingWildly { +- (void)DISABLED_testThrashingWildly { for (NSInteger i = 0; i < 100; i++) { + [self setUp]; [self _testThrashingWildly]; + [self tearDown]; } } - (void)_testThrashingWildly { - NSLog(@"Old data: %@", ASThrashArrayDescription(ds.data)); + [self verifyTableStateWithHierarchy]; + LOG(@"\n*******\nNext Iteration\n*******\nOld data: %@", ASThrashArrayDescription(ds.data)); + + // NOTE: This is not a deep copy, so these sections will still have their + // item counts updated throughout the update. + NSArray *oldSections = [ds.data copy]; + NSMutableArray *deletedItems = [NSMutableArray array]; NSMutableArray *replacedItems = [NSMutableArray array]; NSMutableArray *insertedItems = [NSMutableArray array]; - NSInteger i = 0; // Randomly reload some items for (ASThrashTestSection *section in ds.data) { @@ -259,7 +274,6 @@ static NSString *ASThrashArrayDescription(NSArray *array) { NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; [replacedItems addObject:indexes]; - i += 1; } // Randomly replace some sections @@ -268,32 +282,39 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [ds.data replaceObjectsAtIndexes:replacedSections withObjects:replacingSections]; // Randomly delete some items - i = 0; - for (ASThrashTestSection *section in ds.data) { + [ds.data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { if (section.items.count >= minimumItemCount) { NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO]; /// Cannot reload & delete the same item. - [indexes removeIndexes:replacedItems[i]]; + [indexes removeIndexes:replacedItems[idx]]; [section.items removeObjectsAtIndexes:indexes]; [deletedItems addObject:indexes]; } else { [deletedItems addObject:[NSMutableIndexSet indexSet]]; } - i += 1; - } + }]; // Randomly delete some sections NSMutableIndexSet *deletedSections = nil; if (ds.data.count >= minimumSectionCount) { deletedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO]; - - // Cannot reload & delete the same section. - [deletedSections removeIndexes:replacedSections]; } else { deletedSections = [NSMutableIndexSet indexSet]; } + // Cannot replace & delete the same section. + [deletedSections removeIndexes:replacedSections]; + + // Cannot delete/replace item in deleted/replaced section + [deletedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [replacedItems[idx] removeAllIndexes]; + [deletedItems[idx] removeAllIndexes]; + }]; + [replacedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [replacedItems[idx] removeAllIndexes]; + [deletedItems[idx] removeAllIndexes]; + }]; [ds.data removeObjectsAtIndexes:deletedSections]; // Randomly insert some sections @@ -302,47 +323,43 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [ds.data insertObjects:newSections atIndexes:insertedSections]; // Randomly insert some items - i = 0; for (ASThrashTestSection *section in ds.data) { - NSMutableIndexSet *indexes = [self randomIndexesLessThan:(section.items.count + 1) probability:fickleness insertMode:YES]; - NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; - [section.items insertObjects:newItems atIndexes:indexes]; - [insertedItems addObject:indexes]; - i += 1; + // Only insert items into the old sections – not replaced/inserted sections. + if ([oldSections containsObject:section]) { + NSMutableIndexSet *indexes = [self randomIndexesLessThan:(section.items.count + 1) probability:fickleness insertMode:YES]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items insertObjects:newItems atIndexes:indexes]; + [insertedItems addObject:indexes]; + } else { + [insertedItems addObject:[NSMutableIndexSet indexSet]]; + } } - NSLog(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); + LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); // TODO: Submit changes in random order, randomly chunked up [tableView beginUpdates]; - i = 0; - for (NSIndexSet *indexes in insertedItems) { - NSArray *indexPaths = [indexes indexPathsInSection:i]; - NSLog(@"Requested to insert rows: %@", indexPaths); + [insertedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - i += 1; - } + }]; [tableView insertSections:insertedSections withRowAnimation:UITableViewRowAnimationNone]; [tableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationNone]; - i = 0; - for (NSIndexSet *indexes in deletedItems) { - NSArray *indexPaths = [indexes indexPathsInSection:i]; - NSLog(@"Requested to delete rows: %@", indexPaths); - [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - i += 1; - } + [tableView reloadSections:replacedSections withRowAnimation:UITableViewRowAnimationNone]; - i = 0; - for (NSIndexSet *indexes in replacedItems) { - NSArray *indexPaths = [indexes indexPathsInSection:i]; - NSLog(@"Requested to reload rows: %@", indexPaths); + [deletedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [replacedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - i += 1; - } + }]; [tableView endUpdates]; #if !USE_UIKIT_REFERENCE From 290897cb566ce6b47fc098ea3bfb164bb139bcc8 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 13:06:16 -0700 Subject: [PATCH 03/10] [ASThrashTesting] Little more cleanup --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index df16362774..b24542d798 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -9,15 +9,6 @@ @import XCTest; #import -typedef NS_ENUM(NSUInteger, ASThrashChangeType) { - ASThrashReplaceItem, - ASThrashReplaceSection, - ASThrashDeleteItem, - ASThrashDeleteSection, - ASThrashInsertItem, - ASThrashInsertSection -}; - //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) @@ -63,7 +54,7 @@ static NSString *ASThrashArrayDescription(NSArray *array) { return self; } -+ (NSArray *)itemsWithCount:(NSInteger)count { ++ (NSMutableArray *)itemsWithCount:(NSInteger)count { NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; for (NSInteger i = 0; i < count; i += 1) { [result addObject:[[ASThrashTestItem alloc] init]]; @@ -93,11 +84,8 @@ static NSString *ASThrashArrayDescription(NSArray *array) { - (instancetype)initWithCount:(NSInteger)count { self = [super init]; if (self != nil) { - _items = [NSMutableArray arrayWithCapacity:count]; + _items = [ASThrashTestItem itemsWithCount:count]; _headerHeight = arc4random_uniform(500) + 1; - for (NSInteger i = 0; i < count; i++) { - [_items addObject:[ASThrashTestItem new]]; - } } return self; } From 15e03d85cf4ce0fb8ebdd22ca687f203fae4d0a2 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 14:19:59 -0700 Subject: [PATCH 04/10] [ASThrashTesting] Move update into an archivable object so we can replay tests --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 431 ++++++++++++------ 1 file changed, 280 insertions(+), 151 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index b24542d798..dc1ddbbd34 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -16,6 +16,9 @@ #define kInitialSectionCount 20 #define kInitialItemCount 20 +#define kMinimumItemCount 5 +#define kMinimumSectionCount 3 +#define kFickleness 0.1 #if USE_UIKIT_REFERENCE #define kCellReuseID @"ASThrashTestCellReuseID" @@ -35,25 +38,40 @@ static NSString *ASThrashArrayDescription(NSArray *array) { } #pragma clang diagnostic pop -@interface ASThrashTestItem: NSObject -#if USE_UIKIT_REFERENCE -/// This is used to identify the row with the table view (UIKit only). -@property (nonatomic, readonly) CGFloat rowHeight; -#endif +static volatile int32_t ASThrashTestItemNextID = 1; +@interface ASThrashTestItem: NSObject +@property (nonatomic, readonly) NSInteger itemID; + +- (CGFloat)rowHeight; @end @implementation ASThrashTestItem ++ (BOOL)supportsSecureCoding { + return YES; +} + - (instancetype)init { self = [super init]; if (self != nil) { -#if USE_UIKIT_REFERENCE - _rowHeight = arc4random_uniform(500); -#endif + _itemID = OSAtomicIncrement32(&ASThrashTestItemNextID); } return self; } +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + if (self != nil) { + _itemID = [aDecoder decodeIntegerForKey:@"itemID"]; + NSAssert(_itemID > 0, @"Failed to decode %@", self); + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeInteger:_itemID forKey:@"itemID"]; +} + + (NSMutableArray *)itemsWithCount:(NSInteger)count { NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; for (NSInteger i = 0; i < count; i += 1) { @@ -62,38 +80,28 @@ static NSString *ASThrashArrayDescription(NSArray *array) { return result; } +- (CGFloat)rowHeight { + return (self.itemID % 400) ?: 44; +} + + - (NSString *)description { -#if USE_UIKIT_REFERENCE - return [NSString stringWithFormat:@"", (unsigned long)self.rowHeight]; -#else - return [NSString stringWithFormat:@"", self]; -#endif + return [NSString stringWithFormat:@"", (unsigned long)_itemID]; } @end -@interface ASThrashTestSection: NSObject +@interface ASThrashTestSection: NSObject @property (nonatomic, strong, readonly) NSMutableArray *items; +@property (nonatomic, readonly) NSInteger sectionID; -/// This is used to identify the section with the table view. -@property (nonatomic, readonly) CGFloat headerHeight; +- (CGFloat)headerHeight; @end +static volatile int32_t ASThrashTestSectionNextID = 1; @implementation ASThrashTestSection -- (instancetype)initWithCount:(NSInteger)count { - self = [super init]; - if (self != nil) { - _items = [ASThrashTestItem itemsWithCount:count]; - _headerHeight = arc4random_uniform(500) + 1; - } - return self; -} - -- (instancetype)init { - return [self initWithCount:0]; -} - +/// Create an array of sections with the given count + (NSMutableArray *)sectionsWithCount:(NSInteger)count { NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; for (NSInteger i = 0; i < count; i += 1) { @@ -102,8 +110,59 @@ static NSString *ASThrashArrayDescription(NSArray *array) { return result; } +- (instancetype)initWithCount:(NSInteger)count { + self = [super init]; + if (self != nil) { + _sectionID = OSAtomicIncrement32(&ASThrashTestSectionNextID); + _items = [ASThrashTestItem itemsWithCount:count]; + } + return self; +} + +- (instancetype)init { + return [self initWithCount:0]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + if (self != nil) { + _items = [aDecoder decodeObjectOfClass:[NSArray class] forKey:@"items"]; + _sectionID = [aDecoder decodeIntegerForKey:@"sectionID"]; + NSAssert(_sectionID > 0, @"Failed to decode %@", self); + } + return self; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_items forKey:@"items"]; + [aCoder encodeInteger:_sectionID forKey:@"sectionID"]; +} + +- (CGFloat)headerHeight { + return self.sectionID % 400 ?: 44; +} + - (NSString *)description { - return [NSString stringWithFormat:@"", (unsigned long)self.headerHeight, (unsigned long)self.items.count]; + return [NSString stringWithFormat:@"
", (unsigned long)_sectionID, (unsigned long)self.items.count]; +} + +- (id)copyWithZone:(NSZone *)zone { + ASThrashTestSection *copy = [[ASThrashTestSection alloc] init]; + copy->_sectionID = _sectionID; + copy->_items = [_items mutableCopy]; + return copy; +} + +- (BOOL)isEqual:(id)object { + if ([object isKindOfClass:[ASThrashTestSection class]]) { + return [(ASThrashTestSection *)object sectionID] == _sectionID; + } else { + return NO; + } } @end @@ -188,6 +247,195 @@ static NSString *ASThrashArrayDescription(NSArray *array) { return result; } +/// `insertMode` means that for each index selected, the max goes up by one. ++ (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode { + NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init]; + u_int32_t cutoff = probability * 100; + for (NSInteger i = 0; i < max; i++) { + if (arc4random_uniform(100) < cutoff) { + [indexes addIndex:i]; + if (insertMode) { + max += 1; + } + } + } + return indexes; +} + +@end + +static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; + +@interface ASThrashUpdate : NSObject +@property (nonatomic, strong, readonly) NSMutableArray *oldData; +@property (nonatomic, strong, readonly) NSMutableArray *data; +@property (nonatomic, strong, readonly) NSMutableIndexSet *deletedSectionIndexes; +@property (nonatomic, strong, readonly) NSMutableIndexSet *replacedSectionIndexes; +/// The sections used to replace the replaced sections. +@property (nonatomic, strong, readonly) NSMutableArray *replacingSections; +@property (nonatomic, strong, readonly) NSMutableIndexSet *insertedSectionIndexes; +@property (nonatomic, strong, readonly) NSMutableArray *insertedSections; +@property (nonatomic, strong, readonly) NSMutableArray *deletedItemIndexes; +@property (nonatomic, strong, readonly) NSMutableArray *replacedItemIndexes; +/// The items used to replace the replaced items. +@property (nonatomic, strong, readonly) NSMutableArray *replacingItems; +@property (nonatomic, strong, readonly) NSMutableArray *insertedItemIndexes; +@property (nonatomic, strong, readonly) NSMutableArray *insertedItems; + +/// NOTE: `data` will be modified +- (instancetype)initWithData:(NSArray *)data; + ++ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64; +- (NSString *)base64Representation; +@end + +@implementation ASThrashUpdate + +- (instancetype)initWithData:(NSMutableArray *)data { + self = [super init]; + if (self != nil) { + _oldData = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; + + _deletedItemIndexes = [NSMutableArray array]; + _replacedItemIndexes = [NSMutableArray array]; + _insertedItemIndexes = [NSMutableArray array]; + + // Randomly reload some items + for (ASThrashTestSection *section in data) { + NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; + [_replacedItemIndexes addObject:indexes]; + } + + // Randomly replace some sections + _replacedSectionIndexes = [NSIndexSet randomIndexesLessThan:data.count probability:kFickleness insertMode:NO]; + _replacingSections = [ASThrashTestSection sectionsWithCount:_replacedSectionIndexes.count]; + [data replaceObjectsAtIndexes:_replacedSectionIndexes withObjects:_replacingSections]; + + // Randomly delete some items + [data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { + if (section.items.count >= kMinimumItemCount) { + NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO]; + + /// Cannot reload & delete the same item. + [indexes removeIndexes:_replacedItemIndexes[idx]]; + + [section.items removeObjectsAtIndexes:indexes]; + [_deletedItemIndexes addObject:indexes]; + } else { + [_deletedItemIndexes addObject:[NSMutableIndexSet indexSet]]; + } + }]; + + // Randomly delete some sections + if (data.count >= kMinimumSectionCount) { + _deletedSectionIndexes = [NSIndexSet randomIndexesLessThan:data.count probability:kFickleness insertMode:NO]; + } else { + _deletedSectionIndexes = [NSMutableIndexSet indexSet]; + } + // Cannot replace & delete the same section. + [_deletedSectionIndexes removeIndexes:_replacedSectionIndexes]; + + // Cannot delete/replace item in deleted/replaced section + [_deletedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [_replacedItemIndexes[idx] removeAllIndexes]; + [_deletedItemIndexes[idx] removeAllIndexes]; + }]; + [_replacedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [_replacedItemIndexes[idx] removeAllIndexes]; + [_deletedItemIndexes[idx] removeAllIndexes]; + }]; + [data removeObjectsAtIndexes:_deletedSectionIndexes]; + + // Randomly insert some sections + _insertedSectionIndexes = [NSIndexSet randomIndexesLessThan:(data.count + 1) probability:kFickleness insertMode:YES]; + _insertedSections = [ASThrashTestSection sectionsWithCount:_insertedSectionIndexes.count]; + [data insertObjects:_insertedSections atIndexes:_insertedSectionIndexes]; + + // Randomly insert some items + for (ASThrashTestSection *section in data) { + // Only insert items into the old sections – not replaced/inserted sections. + if ([_oldData containsObject:section]) { + NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:(section.items.count + 1) probability:kFickleness insertMode:YES]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items insertObjects:newItems atIndexes:indexes]; + [_insertedItemIndexes addObject:indexes]; + } else { + [_insertedItemIndexes addObject:[NSMutableIndexSet indexSet]]; + } + } + } + return self; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + ++ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64 { + return [NSKeyedUnarchiver unarchiveObjectWithData:[[NSData alloc] initWithBase64EncodedString:base64 options:kNilOptions]]; +} + +- (NSString *)base64Representation { + return [[NSKeyedArchiver archivedDataWithRootObject:self] base64EncodedStringWithOptions:kNilOptions]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + NSDictionary *dict = [self dictionaryWithValuesForKeys:@[ + @"oldData", + @"deletedSectionIndexes", + @"replacedSectionIndexes", + @"reloadedSections", + @"insertedSectionIndexes", + @"_insertedSectionIndexes", + @"deletedItemIndexes", + @"replacedItemIndexes", + @"reloadedItems", + @"insertedItemIndexes", + @"_insertedItemIndexes" + ]]; + [aCoder encodeObject:dict forKey:@"_dict"]; + [aCoder encodeObject:@(ASThrashUpdateCurrentSerializationVersion) forKey:@"_version"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + if (self != nil) { + NSAssert(ASThrashUpdateCurrentSerializationVersion == [aDecoder decodeIntegerForKey:@"_version"], @"This thrash update was archived from a different version and can't be read. Sorry."); + NSDictionary *dict = [aDecoder decodeObjectOfClass:[NSDictionary class] forKey:@"_dict"]; + [self setValuesForKeysWithDictionary:dict]; + } + return self; +} + +- (void)applyToTableView:(UITableView *)tableView { + [tableView beginUpdates]; + + [tableView insertSections:_insertedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [tableView deleteSections:_deletedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [tableView reloadSections:_replacedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [_insertedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [_deletedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [_replacedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [tableView endUpdates]; +} + @end @interface ASTableViewThrashTests: XCTestCase @@ -201,16 +449,9 @@ static NSString *ASThrashArrayDescription(NSArray *array) { #else ASTableView *tableView; #endif - - NSInteger minimumItemCount; - NSInteger minimumSectionCount; - float fickleness; } - (void)setUp { - minimumItemCount = 5; - minimumSectionCount = 3; - fickleness = 0.1; window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; ds = [[ASThrashDataSource alloc] init]; #if USE_UIKIT_REFERENCE @@ -248,129 +489,17 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [self verifyTableStateWithHierarchy]; LOG(@"\n*******\nNext Iteration\n*******\nOld data: %@", ASThrashArrayDescription(ds.data)); - // NOTE: This is not a deep copy, so these sections will still have their - // item counts updated throughout the update. - NSArray *oldSections = [ds.data copy]; - - NSMutableArray *deletedItems = [NSMutableArray array]; - NSMutableArray *replacedItems = [NSMutableArray array]; - NSMutableArray *insertedItems = [NSMutableArray array]; - - // Randomly reload some items - for (ASThrashTestSection *section in ds.data) { - NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO]; - NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; - [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; - [replacedItems addObject:indexes]; - } - - // Randomly replace some sections - NSMutableIndexSet *replacedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO]; - NSArray *replacingSections = [ASThrashTestSection sectionsWithCount:replacedSections.count]; - [ds.data replaceObjectsAtIndexes:replacedSections withObjects:replacingSections]; - - // Randomly delete some items - [ds.data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { - if (section.items.count >= minimumItemCount) { - NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO]; - - /// Cannot reload & delete the same item. - [indexes removeIndexes:replacedItems[idx]]; - - [section.items removeObjectsAtIndexes:indexes]; - [deletedItems addObject:indexes]; - } else { - [deletedItems addObject:[NSMutableIndexSet indexSet]]; - } - }]; - - // Randomly delete some sections - NSMutableIndexSet *deletedSections = nil; - if (ds.data.count >= minimumSectionCount) { - deletedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO]; - } else { - deletedSections = [NSMutableIndexSet indexSet]; - } - // Cannot replace & delete the same section. - [deletedSections removeIndexes:replacedSections]; - - // Cannot delete/replace item in deleted/replaced section - [deletedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { - [replacedItems[idx] removeAllIndexes]; - [deletedItems[idx] removeAllIndexes]; - }]; - [replacedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { - [replacedItems[idx] removeAllIndexes]; - [deletedItems[idx] removeAllIndexes]; - }]; - [ds.data removeObjectsAtIndexes:deletedSections]; - - // Randomly insert some sections - NSMutableIndexSet *insertedSections = [self randomIndexesLessThan:(ds.data.count + 1) probability:fickleness insertMode:YES]; - NSArray *newSections = [ASThrashTestSection sectionsWithCount:insertedSections.count]; - [ds.data insertObjects:newSections atIndexes:insertedSections]; - - // Randomly insert some items - for (ASThrashTestSection *section in ds.data) { - // Only insert items into the old sections – not replaced/inserted sections. - if ([oldSections containsObject:section]) { - NSMutableIndexSet *indexes = [self randomIndexesLessThan:(section.items.count + 1) probability:fickleness insertMode:YES]; - NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; - [section.items insertObjects:newItems atIndexes:indexes]; - [insertedItems addObject:indexes]; - } else { - [insertedItems addObject:[NSMutableIndexSet indexSet]]; - } - } + ASThrashUpdate *update = [[ASThrashUpdate alloc] initWithData:ds.data]; LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); - // TODO: Submit changes in random order, randomly chunked up - - [tableView beginUpdates]; - [insertedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:idx]; - [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - - [tableView insertSections:insertedSections withRowAnimation:UITableViewRowAnimationNone]; - - [tableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationNone]; - - [tableView reloadSections:replacedSections withRowAnimation:UITableViewRowAnimationNone]; - - [deletedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:sec]; - [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - - [replacedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:sec]; - [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - - [tableView endUpdates]; + [update applyToTableView:tableView]; #if !USE_UIKIT_REFERENCE [tableView waitUntilAllUpdatesAreCommitted]; #endif [self verifyTableStateWithHierarchy]; } -/// `insertMode` means that for each index selected, the max goes up by one. -- (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode { - NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init]; - u_int32_t cutoff = probability * 100; - for (NSInteger i = 0; i < max; i++) { - if (arc4random_uniform(100) < cutoff) { - [indexes addIndex:i]; - if (insertMode) { - max += 1; - } - } - } - return indexes; -} - #pragma mark Helpers - (void)verifyTableStateWithHierarchy { From 4184c21c0c8905489237e681db5093c1f478eb22 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 15:46:47 -0700 Subject: [PATCH 05/10] [ASThrashTesting] Continue setting up replay feature --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 47 +++++++++++++++---- .../TestResources/ASThrashTestRecordedCase | 0 2 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index dc1ddbbd34..d32bab23e6 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -386,17 +386,17 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @"oldData", @"deletedSectionIndexes", @"replacedSectionIndexes", - @"reloadedSections", + @"replacingSections", @"insertedSectionIndexes", - @"_insertedSectionIndexes", + @"insertedSections", @"deletedItemIndexes", @"replacedItemIndexes", - @"reloadedItems", + @"replacingItems", @"insertedItemIndexes", - @"_insertedItemIndexes" + @"insertedItems" ]]; [aCoder encodeObject:dict forKey:@"_dict"]; - [aCoder encodeObject:@(ASThrashUpdateCurrentSerializationVersion) forKey:@"_version"]; + [aCoder encodeInteger:ASThrashUpdateCurrentSerializationVersion forKey:@"_version"]; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { @@ -432,8 +432,12 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; NSArray *indexPaths = [indexes indexPathsInSection:sec]; [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; }]; - - [tableView endUpdates]; + @try { + [tableView endUpdates]; + } @catch (NSException *exception) { + NSLog(@"Rejected update base64: %@", self.base64Representation); + @throw exception; + } } @end @@ -449,6 +453,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; #else ASTableView *tableView; #endif + ASThrashUpdate *currentUpdate; } - (void)setUp { @@ -477,7 +482,28 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; [self verifyTableStateWithHierarchy]; } -- (void)DISABLED_testThrashingWildly { +- (void)testSpecificThrashing { + NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; + NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:nil]; + + ASThrashUpdate *update = [ASThrashUpdate thrashUpdateWithBase64String:base64]; + if (update == nil) { + return; + } + + currentUpdate = update; + + LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); + + [update applyToTableView:tableView]; +#if !USE_UIKIT_REFERENCE + XCTAssertNoThrow([tableView waitUntilAllUpdatesAreCommitted], @"Update assertion failure: %@", update); +#endif + [self verifyTableStateWithHierarchy]; + currentUpdate = nil; +} + +- (void)testThrashingWildly { for (NSInteger i = 0; i < 100; i++) { [self setUp]; [self _testThrashingWildly]; @@ -486,18 +512,19 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; } - (void)_testThrashingWildly { - [self verifyTableStateWithHierarchy]; LOG(@"\n*******\nNext Iteration\n*******\nOld data: %@", ASThrashArrayDescription(ds.data)); ASThrashUpdate *update = [[ASThrashUpdate alloc] initWithData:ds.data]; + currentUpdate = update; LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); [update applyToTableView:tableView]; #if !USE_UIKIT_REFERENCE - [tableView waitUntilAllUpdatesAreCommitted]; + XCTAssertNoThrow([tableView waitUntilAllUpdatesAreCommitted], @"Update assertion failure: %@", update); #endif [self verifyTableStateWithHierarchy]; + currentUpdate = nil; } #pragma mark Helpers diff --git a/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase b/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase new file mode 100644 index 0000000000..e69de29bb2 From 383667f2c31282d376f40cd73ba5e2fcd617d9ff Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 17:02:46 -0700 Subject: [PATCH 06/10] [ASThrashTesting] Rocking and rolling! --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 268 +++++++++--------- .../TestResources/ASThrashTestRecordedCase | 1 + 2 files changed, 142 insertions(+), 127 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index d32bab23e6..6b8493966e 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -9,20 +9,21 @@ @import XCTest; #import -//#define LOG(...) NSLog(__VA_ARGS__) -#define LOG(...) - #define USE_UIKIT_REFERENCE 0 +#if USE_UIKIT_REFERENCE +#define TableView UITableView +#define kCellReuseID @"ASThrashTestCellReuseID" +#else +#define TableView ASTableView +#endif + #define kInitialSectionCount 20 #define kInitialItemCount 20 #define kMinimumItemCount 5 #define kMinimumSectionCount 3 #define kFickleness 0.1 - -#if USE_UIKIT_REFERENCE -#define kCellReuseID @"ASThrashTestCellReuseID" -#endif +#define kThrashingIterationCount 100 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" @@ -183,16 +184,32 @@ static volatile int32_t ASThrashTestSectionNextID = 1; #else #endif -@property (nonatomic, strong, readonly) NSMutableArray *data; + +@property (nonatomic, strong, readonly) UIWindow *window; +@property (nonatomic, strong, readonly) TableView *tableView; +@property (nonatomic, strong) NSArray *data; @end @implementation ASThrashDataSource -- (instancetype)init { +- (instancetype)initWithData:(NSArray *)data { self = [super init]; if (self != nil) { - _data = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + _data = [[NSArray alloc] initWithArray:data copyItems:YES]; + _window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + _tableView = [[TableView alloc] initWithFrame:_window.bounds style:UITableViewStylePlain]; + [_window addSubview:_tableView]; +#if USE_UIKIT_REFERENCE + _tableView.dataSource = self; + _tableView.delegate = self; + [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID]; +#else + _tableView.asyncDelegate = self; + _tableView.asyncDataSource = self; + [_tableView reloadDataImmediately]; +#endif + [_tableView layoutIfNeeded]; } return self; } @@ -267,7 +284,7 @@ static volatile int32_t ASThrashTestSectionNextID = 1; static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @interface ASThrashUpdate : NSObject -@property (nonatomic, strong, readonly) NSMutableArray *oldData; +@property (nonatomic, strong, readonly) NSArray *oldData; @property (nonatomic, strong, readonly) NSMutableArray *data; @property (nonatomic, strong, readonly) NSMutableIndexSet *deletedSectionIndexes; @property (nonatomic, strong, readonly) NSMutableIndexSet *replacedSectionIndexes; @@ -278,9 +295,9 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @property (nonatomic, strong, readonly) NSMutableArray *deletedItemIndexes; @property (nonatomic, strong, readonly) NSMutableArray *replacedItemIndexes; /// The items used to replace the replaced items. -@property (nonatomic, strong, readonly) NSMutableArray *replacingItems; +@property (nonatomic, strong, readonly) NSMutableArray *> *replacingItems; @property (nonatomic, strong, readonly) NSMutableArray *insertedItemIndexes; -@property (nonatomic, strong, readonly) NSMutableArray *insertedItems; +@property (nonatomic, strong, readonly) NSMutableArray *> *insertedItems; /// NOTE: `data` will be modified - (instancetype)initWithData:(NSArray *)data; @@ -291,30 +308,34 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @implementation ASThrashUpdate -- (instancetype)initWithData:(NSMutableArray *)data { +- (instancetype)initWithData:(NSArray *)data { self = [super init]; if (self != nil) { + _data = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; _oldData = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; _deletedItemIndexes = [NSMutableArray array]; _replacedItemIndexes = [NSMutableArray array]; _insertedItemIndexes = [NSMutableArray array]; + _replacingItems = [NSMutableArray array]; + _insertedItems = [NSMutableArray array]; // Randomly reload some items - for (ASThrashTestSection *section in data) { + for (ASThrashTestSection *section in _data) { NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO]; NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; + [_replacingItems addObject:newItems]; [_replacedItemIndexes addObject:indexes]; } // Randomly replace some sections - _replacedSectionIndexes = [NSIndexSet randomIndexesLessThan:data.count probability:kFickleness insertMode:NO]; + _replacedSectionIndexes = [NSIndexSet randomIndexesLessThan:_data.count probability:kFickleness insertMode:NO]; _replacingSections = [ASThrashTestSection sectionsWithCount:_replacedSectionIndexes.count]; - [data replaceObjectsAtIndexes:_replacedSectionIndexes withObjects:_replacingSections]; + [_data replaceObjectsAtIndexes:_replacedSectionIndexes withObjects:_replacingSections]; // Randomly delete some items - [data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { + [_data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { if (section.items.count >= kMinimumItemCount) { NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO]; @@ -329,8 +350,8 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; }]; // Randomly delete some sections - if (data.count >= kMinimumSectionCount) { - _deletedSectionIndexes = [NSIndexSet randomIndexesLessThan:data.count probability:kFickleness insertMode:NO]; + if (_data.count >= kMinimumSectionCount) { + _deletedSectionIndexes = [NSIndexSet randomIndexesLessThan:_data.count probability:kFickleness insertMode:NO]; } else { _deletedSectionIndexes = [NSMutableIndexSet indexSet]; } @@ -346,22 +367,24 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; [_replacedItemIndexes[idx] removeAllIndexes]; [_deletedItemIndexes[idx] removeAllIndexes]; }]; - [data removeObjectsAtIndexes:_deletedSectionIndexes]; + [_data removeObjectsAtIndexes:_deletedSectionIndexes]; // Randomly insert some sections - _insertedSectionIndexes = [NSIndexSet randomIndexesLessThan:(data.count + 1) probability:kFickleness insertMode:YES]; + _insertedSectionIndexes = [NSIndexSet randomIndexesLessThan:(_data.count + 1) probability:kFickleness insertMode:YES]; _insertedSections = [ASThrashTestSection sectionsWithCount:_insertedSectionIndexes.count]; - [data insertObjects:_insertedSections atIndexes:_insertedSectionIndexes]; + [_data insertObjects:_insertedSections atIndexes:_insertedSectionIndexes]; // Randomly insert some items - for (ASThrashTestSection *section in data) { + for (ASThrashTestSection *section in _data) { // Only insert items into the old sections – not replaced/inserted sections. if ([_oldData containsObject:section]) { NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:(section.items.count + 1) probability:kFickleness insertMode:YES]; NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; [section.items insertObjects:newItems atIndexes:indexes]; + [_insertedItems addObject:newItems]; [_insertedItemIndexes addObject:indexes]; } else { + [_insertedItems addObject:@[]]; [_insertedItemIndexes addObject:[NSMutableIndexSet indexSet]]; } } @@ -383,18 +406,19 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; - (void)encodeWithCoder:(NSCoder *)aCoder { NSDictionary *dict = [self dictionaryWithValuesForKeys:@[ - @"oldData", - @"deletedSectionIndexes", - @"replacedSectionIndexes", - @"replacingSections", - @"insertedSectionIndexes", - @"insertedSections", - @"deletedItemIndexes", - @"replacedItemIndexes", - @"replacingItems", - @"insertedItemIndexes", - @"insertedItems" - ]]; + @"oldData", + @"data", + @"deletedSectionIndexes", + @"replacedSectionIndexes", + @"replacingSections", + @"insertedSectionIndexes", + @"insertedSections", + @"deletedItemIndexes", + @"replacedItemIndexes", + @"replacingItems", + @"insertedItemIndexes", + @"insertedItems" + ]]; [aCoder encodeObject:dict forKey:@"_dict"]; [aCoder encodeInteger:ASThrashUpdateCurrentSerializationVersion forKey:@"_version"]; } @@ -409,35 +433,12 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; return self; } -- (void)applyToTableView:(UITableView *)tableView { - [tableView beginUpdates]; - - [tableView insertSections:_insertedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; - - [tableView deleteSections:_deletedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; - - [tableView reloadSections:_replacedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; - - [_insertedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:idx]; - [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - - [_deletedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:sec]; - [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - - [_replacedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:sec]; - [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - @try { - [tableView endUpdates]; - } @catch (NSException *exception) { - NSLog(@"Rejected update base64: %@", self.base64Representation); - @throw exception; - } +- (NSString *)description { + return [NSString stringWithFormat:@"", self, ASThrashArrayDescription(_oldData), ASThrashArrayDescription(_deletedItemIndexes), _deletedSectionIndexes, ASThrashArrayDescription(_replacedItemIndexes), _replacedSectionIndexes, ASThrashArrayDescription(_insertedItemIndexes), _insertedSectionIndexes, ASThrashArrayDescription(_data)]; +} + +- (NSString *)logFriendlyBase64Representation { + return [NSString stringWithFormat:@"\n\n**********\nBase64 Representation:\n**********\n%@\n**********\nEnd Base64 Representation\n**********", self.base64Representation]; } @end @@ -446,90 +447,103 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @end @implementation ASTableViewThrashTests { - ASThrashDataSource *ds; - UIWindow *window; -#if USE_UIKIT_REFERENCE - UITableView *tableView; -#else - ASTableView *tableView; -#endif - ASThrashUpdate *currentUpdate; + // The current update, which will be logged in case of a failure. + ASThrashUpdate *_update; } -- (void)setUp { - window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - ds = [[ASThrashDataSource alloc] init]; -#if USE_UIKIT_REFERENCE - tableView = [[UITableView alloc] initWithFrame:window.bounds style:UITableViewStyleGrouped]; - [window addSubview:tableView]; - tableView.dataSource = ds; - tableView.delegate = ds; - [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID]; - [window layoutIfNeeded]; -#else - ASTableNode *tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; - tableView = tableNode.view; - tableNode.frame = window.bounds; - [window addSubnode:tableNode]; - tableNode.dataSource = ds; - tableNode.delegate = ds; - [tableView reloadDataImmediately]; -#endif +#pragma mark Overrides +- (void)tearDown { + _update = nil; } +// NOTE: Despite the documentation, this is not always called if an exception is caught. +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected { + [self logCurrentUpdateIfNeeded]; + [super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; +} + +#pragma mark Test Methods + - (void)testInitialDataRead { - [self verifyTableStateWithHierarchy]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:20]]; + [self verifyDataSource:ds]; } -- (void)testSpecificThrashing { +/// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file +- (void)DISABLED_testRecordedThrashCase { NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; - NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:nil]; + NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL]; - ASThrashUpdate *update = [ASThrashUpdate thrashUpdateWithBase64String:base64]; - if (update == nil) { + _update = [ASThrashUpdate thrashUpdateWithBase64String:base64]; + if (_update == nil) { return; } - currentUpdate = update; - - LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); - - [update applyToTableView:tableView]; -#if !USE_UIKIT_REFERENCE - XCTAssertNoThrow([tableView waitUntilAllUpdatesAreCommitted], @"Update assertion failure: %@", update); -#endif - [self verifyTableStateWithHierarchy]; - currentUpdate = nil; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:_update.oldData]; + [self applyUpdate:_update toDataSource:ds]; + [self verifyDataSource:ds]; } -- (void)testThrashingWildly { - for (NSInteger i = 0; i < 100; i++) { +- (void)DISABLED_testThrashingWildly { + for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; - [self _testThrashingWildly]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:20]]; + _update = [[ASThrashUpdate alloc] initWithData:ds.data]; + + [self applyUpdate:_update toDataSource:ds]; + [self verifyDataSource:ds]; [self tearDown]; } } -- (void)_testThrashingWildly { - LOG(@"\n*******\nNext Iteration\n*******\nOld data: %@", ASThrashArrayDescription(ds.data)); - - ASThrashUpdate *update = [[ASThrashUpdate alloc] initWithData:ds.data]; - currentUpdate = update; - - LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); - - [update applyToTableView:tableView]; -#if !USE_UIKIT_REFERENCE - XCTAssertNoThrow([tableView waitUntilAllUpdatesAreCommitted], @"Update assertion failure: %@", update); -#endif - [self verifyTableStateWithHierarchy]; - currentUpdate = nil; -} - #pragma mark Helpers -- (void)verifyTableStateWithHierarchy { +- (void)logCurrentUpdateIfNeeded { + if (_update != nil) { + NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation); + } +} + +- (void)applyUpdate:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource { + TableView *tableView = dataSource.tableView; + + [tableView beginUpdates]; + dataSource.data = update.data; + + [tableView insertSections:update.insertedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [tableView deleteSections:update.deletedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [tableView reloadSections:update.replacedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [update.insertedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [update.deletedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [update.replacedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + @try { + [tableView endUpdates]; +#if !USE_UIKIT_REFERENCE + [tableView waitUntilAllUpdatesAreCommitted]; +#endif + } @catch (NSException *exception) { + [self logCurrentUpdateIfNeeded]; + @throw exception; + } +} + +- (void)verifyDataSource:(ASThrashDataSource *)ds { + TableView *tableView = ds.tableView; NSArray *data = [ds data]; XCTAssertEqual(data.count, tableView.numberOfSections); for (NSInteger i = 0; i < tableView.numberOfSections; i++) { diff --git a/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase b/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase index e69de29bb2..9e8343590e 100644 --- a/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase +++ b/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase @@ -0,0 +1 @@ +YnBsaXN0MDDUAAEAAgADAAQABQAGDfYN91gkdmVyc2lvblgkb2JqZWN0c1kkYXJjaGl2ZXJUJHRvcBIAAYagrxEDRQAHAAgADwAtAC4ALwAwADEAMgAzADQANQA2ADcAOAA5AEAARgBdAGEAaABrAG4AcQB0AHcAegB9AIAAgwCGAIkAjACPAJIAlQCYAJsAngChAKYAqgCuAMUAyADLAM4A0QDUANcA2gDdAOAA4wDmAOkA7ADvAPIA9QD4APsA/gEBAQUBHAEfASIBJQEoASsBLgExATQBNwE6AT0BQAFDAUYBSQFMAU8BUgFVAVgBXAFzAXYBeQF8AX8BggGFAYgBiwGOAZEBlAGXAZoBnQGgAaMBpgGpAawBrwG1AbkBvgHDAdsB4AHjAeYB6AHqAewB7wHyAfQB9gH5AfwB/wICAgUCBwIKAg0CDwITAhYCGAIaAhwCIAIjAiYCKQIsAkMCSAJLAk4CUwJWAlkCXQJgAmQCZwJsAm8CcgJ4AnsCfgKBAoUCiAKNApACkwKXApoCngKhAqYCqQKsArECtAK3Ar0CwALDAsYCywLOAtEC2ALbAt4C4QLkAu4C8QL0AvcC+gL9AwADAwMHAwoDEAMTAxYDGQMeAyEDJAMnAz4DQgNZA1wDXwNiA2UDaANrA24DcQN0A3cDegN9A4ADgwOGA4kDjAOPA5IDlQOZA7ADswO2A7kDvAO/A8IDxQPIA8sDzgPRA9QD1wPaA90D4APjA+YD6QPsA/AEBwQKBA0EEAQTBBYEGQQcBB8EIgQlBCgEKwQuBDEENAQ3BDoEPQRABEMERwReBGEEZARnBGoEbQRwBHMEdgR5BHwEfwSCBIUEiASLBI4EkQSUBJcEmgSeBLUEuAS7BL4EwQTEBMcEygTNBNAE0wTWBNkE3ATfBOIE5QToBOsE7gTxBPUFDAUPBRIFFQUYBRsFHgUhBSQFJwUqBS0FMAUzBTYFOQU8BT8FQgVFBUgFTAVjBWYFaQVsBW8FcgV1BXgFewV+BYEFhAWHBYoFjQWQBZMFlgWZBZwFnwWjBboFvQXABcMFxgXJBcwFzwXSBdUF2AXbBd4F4QXkBecF6gXtBfAF8wX2BfoGEQYUBhcGGgYdBiAGIwYmBikGLAYvBjIGNQY4BjsGPgZBBkQGRwZKBk0GUQZoBmsGbgZxBnQGdwZ6Bn0GgAaDBoYGiQaMBo8GkgaVBpgGmwaeBqEGpAaoBr8GwgbFBsgGywbOBtEG1AbXBtoG3QbgBuMG5gbpBuwG7wbyBvUG+Ab7Bv8HFgcZBxwHHwciByUHKAcrBy4HMQc0BzcHOgc9B0AHQwdGB0kHTAdPB1IHVgdtB3AHcwd2B3kHfAd/B4IHhQeIB4sHjgeRB5QHlweaB50HoAejB6YHqQetB8QHxwfKB80H0AfTB9YH2QfcB98H4gflB+gH6wfuB/EH9Af3B/oH/QgACAQIGwgeCCEIJAgnCCoILQgwCDMINgg5CDwIPwhCCEUISAhLCE4IUQhUCFcIWwhyCHUIeAh7CH4IgQiECIcIigiNCJAIkwiWCJkInAifCKIIpQioCKsIrgiyCMkIzAjPCNII1QjYCNsI3gjhCOQI5wjqCO0I8AjzCPYI+Qj8CP8JAgkFCQkJIAkjCSYJKQksCS8JMgk1CTgJOwk+CUEJRAlHCUoJTQlQCVMJVglZCVwJYAl3CXoJfQmACYMJhgmJCYwJjwmSCZUJmAmbCZ4JoQmkCacJqgmtCbAJswm3Cc4J0QnUCdcJ2gndCeAJ4wnmCekJ7AnvCfIJ9Qn4CfsJ/goBCgQKBwoKCiIKJQo8Cj8KQgpcCl8KYgplCmgKfQqACpUKmAqwCrMKtgq6Cs0K0ArTCtYK2QrcCt8K4grlCugK6wruCvEK9Ar3CvoK/QsBCxcLGgsdCyALIwsmCykLLAsvCzILNQs4CzsLPgtBC0QLRwtKC00LUAtTC2wLbwtyC3ULjAuPC5ILrAuvC7ILtQu4C8wLzwvmC+kL7AvvDAgMCwwODBEMFQwoDCsMLgwxDDQMNww6DD0MQAxDDEYMSQxMDE8MUgxVDFgMWwxyDHUMeAx7DH4MgQyWDJkMnAyfDLYMuQy8DL8M1gzYDNsM3QzgDOMM5gzpDOwM7gzwDPIM9Az2DPgM+gz9DQANAw0GDQkNCw0ODRENFA0XDRoNMQ0zDTYNOQ08DT8NQg1FDUgNSw1NDU8NUQ1TDVYNWQ1cDV8NYQ1kDWcNag1tDXANcw11DXgNew1+DYENgw2bDZ8NpQ2oDaoNrQ2wDbUNug2+DcQNxw3MDdIN2Q3eDeIN5Q3oDe4N8lUkbnVsbNMACQAKAAsADAANAA5VX2RpY3RYX3ZlcnNpb25WJGNsYXNzgAIQAYEDRNMAEAARAAsAEgA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/rxAUAV4BXwFgAWEBYgFjAWQBZQFmAWcBaAFpAWoBawFsAW0BbgFvAXABcYBXgFiAWYBagFuAXIBdgF6AX4BggGGAYoBjgGSAZYBmgGeAaIBpgGqAJ9IAXgALAXQAYBEEToAT0gBeAAsBdwBgEQRPgBPSAF4ACwF6AGARBFCAE9IAXgALAX0AYBEEUYAT0gBeAAsBgABgEQRSgBPSAF4ACwGDAGARBFOAE9IAXgALAYYAYBEEVIAT0gBeAAsBiQBgEQRVgBPSAF4ACwGMAGARBFaAE9IAXgALAY8AYBEEV4AT0gBeAAsBkgBgEQRYgBPSAF4ACwGVAGARBFmAE9IAXgALAZgAYBEEWoAT0gBeAAsBmwBgEQRbgBPSAF4ACwGeAGARBFyAE9IAXgALAaEAYBEEXYAT0gBeAAsBpABgEQRegBPSAF4ACwGnAGARBF+AE9IAXgALAaoAYBEEYIAT0gBeAAsBrQBgEQRhgBPTAbAACwGxAbIBswG0XE5TUmFuZ2VDb3VudFtOU1JhbmdlRGF0YRACgG6AbNIBtgALAbcBuFdOUy5kYXRhRAYCEAGAbdIAYgBjAboBu11OU011dGFibGVEYXRhowG8Ab0AZ11OU011dGFibGVEYXRhVk5TRGF0YdIAYgBjAb8BwF8QEU5TTXV0YWJsZUluZGV4U2V0owHBAcIAZ18QEU5TTXV0YWJsZUluZGV4U2V0Wk5TSW5kZXhTZXTSABEACwHEAD+vEBUBxQHGAccByAHJAcoBywHMAc0BzgHPAdAB0QHSAdMB1AHVAdYB1wHYAdmAcIBxgHOAdIB1gHaAeIB5gHqAfIB9gH+AgICCgIOAhYCGgIeAiICKgIyAJ9QB3AALAbAB3QHeAbMADQANWk5TTG9jYXRpb25YTlNMZW5ndGgQAIBu0wGwAAsBsQGyAbMB4oBugHLSAbYACwHkAbhEAgEUAoBt0gGwAAsB3gGzgG7SAbAACwHeAbOAbtIBsAALAd4Bs4Bu0wGwAAsBsQGyAbMB7oBugHfSAbYACwHwAbhEEAETAYBt0gGwAAsB3gGzgG7SAbAACwHeAbOAbtMBsAALAbEBsgGzAfiAboB70gG2AAsB+gG4RAMBDAGAbdQB3AALAbAB3QH9AbMADQANEAeAbtMBsAALAbEBsgGzAgGAboB+0gG2AAsCAwG4RAwCFAGAbdI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AE9IAXgALBRMAYBECgoAT0gBeAAsFFgBgEQKDgBPSAF4ACwUZAGARAoSAE9IAXgALBRwAYBEChYAT0gBeAAsFHwBgEQKGgBPSAF4ACwUiAGARAoeAE9IAXgALBSUAYBECiIAT0gBeAAsFKABgEQKJgBPSAF4ACwUrAGARAoqAE9IAXgALBS4AYBECi4AT0gBeAAsFMQBgEQKMgBPSAF4ACwU0AGARAo2AE9IAXgALBTcAYBECjoAT0gBeAAsFOgBgEQKPgBPSAF4ACwU9AGARApCAE9IAXgALBUAAYBECkYAT0gBeAAsFQwBgEQKSgBPSAF4ACwVGAGARApOAE9MAQQBCAAsFSQVKAEWBAVIQW4Ao0gARAAsFTQA/rxAUBU4FTwVQBVEFUgVTBVQFVQVWBVcFWAVZBVoFWwVcBV0FXgVfBWAFYYEBU4EBVIEBVYEBVoEBV4EBWIEBWYEBWoEBW4EBXIEBXYEBXoEBX4EBYIEBYYEBYoEBY4EBZIEBZYEBZoAn0gBeAAsFZABgEQKUgBPSAF4ACwVnAGARApWAE9IAXgALBWoAYBECloAT0gBeAAsFbQBgEQKXgBPSAF4ACwVwAGARApiAE9IAXgALBXMAYBECmYAT0gBeAAsFdgBgEQKagBPSAF4ACwV5AGARApuAE9IAXgALBXwAYBECnIAT0gBeAAsFfwBgEQKdgBPSAF4ACwWCAGARAp6AE9IAXgALBYUAYBECn4AT0gBeAAsFiABgEQKggBPSAF4ACwWLAGARAqGAE9IAXgALBY4AYBECooAT0gBeAAsFkQBgEQKjgBPSAF4ACwWUAGARAqSAE9IAXgALBZcAYBECpYAT0gBeAAsFmgBgEQKmgBPSAF4ACwWdAGARAqeAE9MAQQBCAAsFoAWhAEWBAWgQXIAo0gARAAsFpAA/rxAUBaUFpgWnBagFqQWqBasFrAWtBa4FrwWwBbEFsgWzBbQFtQW2BbcFuIEBaYEBaoEBa4EBbIEBbYEBboEBb4EBcIEBcYEBcoEBc4EBdIEBdYEBdoEBd4EBeIEBeYEBeoEBe4EBfIAn0gBeAAsFuwBgEQKogBPSAF4ACwW+AGARAqmAE9IAXgALBcEAYBECqoAT0gBeAAsFxABgEQKrgBPSAF4ACwXHAGARAqyAE9IAXgALBcoAYBECrYAT0gBeAAsFzQBgEQKugBP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CToECT4ECUIECUYDGgQJTgQJUgMeBAliAJ9IAXgALDHMAYBEEc4AT0gBeAAsMdgBgEQR0gBPSAF4ACwx5AGARBHWAE9IAXgALDHwAYBEEdoAT0wBBAEIACwx/CV4ARYEC7YAo0gARAAsMggA/rxASCWIJYwMbCWUJZglnCWgMiglqCWsMjQlsCW4JbwlxCXIJcwMcgQJbgQJcgMmBAl6BAl+BAmCBAmGBAu6BAmOBAmSBAu+BAmWBAmeBAmiBAmqBAmuBAmyAyoAn0gBeAAsMlwBgEQR3gBPSAF4ACwyaAGARBHiAE9MAQQBCAAsMnQm1AEWBAvGAKNIAEQALDKAAP68QFAm5CboJuwm8Cb0Mpgm+Cb8JwAnBCcIJwwnECcUJxgnHCcgJyQnLCcyBAnGBAnKBAnOBAnSBAnWBAvKBAnaBAneBAniBAnmBAnqBAnuBAnyBAn2BAn6BAn+BAoCBAoGBAoOBAoSAJ9IAXgALDLcAYBEEeYAT0wGwAAsBsQIQAbMMu4BugQL00gG2AAsMvQG4RgIBDQEQAoBt0gARAAsMwAA/rxAUDMEMwgzDDMQMxQzGDMcMyAzJDMoMywzMDM0MzgzPDNAM0QzSDNMM1IEC9oEC94EC+IEC+YEC+4EC/YEC/oEC/4EDAIEDAYEDAoEDA4EDBIEDBYEDB4EDCYEDCoEDC4EDDYEDD4An0gGwAAsB3gGzgG7UAdwACwGwAd0M2QGzAA0ADRANgG7SAbAACwHeAbOAbtMBsAALAbEBsgGzDN+AboEC+tIBtgALDOEBuEQDAQ0BgG3TAbAACwGxAbIBswzlgG6BAvzSAbYACwznAbhEAgEOAYBt1AHcAAsBsAHdDOoBswANAA0QDoBu0gGwAAsB3gGzgG7SAbAACwHeAbOAbtIBsAALAd4Bs4Bu0gGwAAsB3gGzgG7UAdwACwGwAd0CEAGzAA0ADYBu0gGwAAsB3gGzgG7SAbAACwHeAbOAbtMBsAALAbECEAGzDPyAboEDBtIBtgALDP4BuEYCAQoBDwGAbdMBsAALAbEBsgGzDQKAboEDCNIBtgALDQQBuEQEAQYBgG3UAdwACwGwAd0NBwGzAA0ADRATgG7SAbAACwHeAbOAbtMBsAALAbECEAGzDQ2AboEDDNIBtgALDQ8BuEYEAQYBEQKAbdMBsAALAbECHQGzDROAboEDDtI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9FUYVSBVKFUwVThVQFVkVXBVeFWcVahVsFXUVeBV6FYMVhhWIFZEVoBWiFaQVphWoFaoVrBWuFbAVuRW8Fb4VxxXKFcwV1RXYFdoV4xXmFegV8RX0FfYV/xYCFgQWDRYQFhIWGxYeFiAWIhYrFi4WMBY5FkAWQhZEFkYWSBZRFlQWVhZfFmIWZBZtFnAWchZ7FoAWghaEFoYWjxaSFpQWnRagFqIWqxasFq4WtxbiFuQW5hboFusW7hbxFvQW9xb6Fv0XABcDFwYXCRcMFw8XEhcVFxgXGxcdFyoXLBcuFzAXORdkF2YXaBdqF2wXbhdwF3IXdBd2F3gXehd8F34XgBeCF4QXhheIF4oXjBeOF5cXmhecF6UXqBeqF7MXthe4F8EXxBfGF88X0hfUF90X4BfiF+sX7hfwF/kX/Bf+GAcYChgMGBUYGBgaGCMYJhgoGDEYNBg2GD8YQhhEGE0YUBhSGFsYXhhgGGkYbBhuGHcYehh8GIUYiBiKGJMYlhiYGKEYpBimGLMYtRi3GLkYwhjtGO8Y8RjzGPUY9xj5GPsY/Rj/GQEZAxkFGQcZCRkLGQ0ZDxkRGRMZFRkXGSAZIxklGS4ZMRkzGTwZPxlBGUoZTRlPGVgZWxldGWYZaRlrGXQZdxl5GYIZhRmHGZAZkxmVGZ4ZoRmjGawZrxmxGboZvRm/GcgZyxnNGdYZ2RnbGeQZ5xnpGfIZ9Rn3GgAaAxoFGg4aERoTGhwaHxohGioaLRovGjwaPhpAGkIaSxp2Gngaehp8Gn4agBqDGoYaiRqMGo8akhqVGpgamxqeGqEapBqnGqoarRqvGrgauxq9GsYayRrLGtQa1xrZGuIa5RrnGvAa8xr1Gv4bARsDGwwbDxsRGxobHRsfGygbKxstGzYbORs7G0QbRxtJG1IbVRtXG2AbYxtlG24bcRtzG3wbfxuBG4objRuPG5gbmxudG6YbqRurG7Qbtxu5G8IbxRvHG9Qb1xvZG9sb5BwPHBIcFRwYHBscHhwhHCQcJxwqHC0cMBwzHDYcORw8HD8cQhxFHEgcSxxNHFYcWRxbHGQcZxxpHHIcdRx3HIAcgxyFHI4ckRyTHJwcnxyhHKocrRyvHLgcuxy9HMYcyRzLHNQc1xzZHOIc5RznHPAc8xz1HP4dAR0DHQwdDx0RHRodHR0fHSgdKx0tHTYdOR07HUQdRx1JHVIdVR1XHWAdYx1lHXIddR13HXkdgh2tHbAdsx22HbkdvB2/HcIdxR3IHcsdzh3RHdQd1x3aHd0d4B3jHeYd6R3rHfQd9x35HgIeBR4HHhAeEx4VHh4eIR4jHiweLx4xHjoePR4/HkgeSx5NHlYeWR5bHmQeZx5pHnIedR53HoAegx6FHo4ekR6THpwenx6hHqoerR6vHrgeux69HsYeyR7LHtQe1x7ZHuIe5R7nHvAe8x71Hv4fAR8DHxAfEx8VHxcfIB9LH04fUR9UH1cfWh9dH2AfYx9mH2kfbB9vH3IfdR94H3sffh+BH4Qfhx+JH5IflR+XH6Afox+lH64fsR+zH7wfvx/BH8ofzR/PH9gf2x/dH+Yf6R/rH/Qf9x/5IAIgBSAHIBAgEyAVIB4gISAjICwgLyAxIDogPSA/IEggSyBNIFYgWSBbIGQgZyBpIHIgdSB3IIAggyCFII4gkSCTIJwgnyChIK4gsSCzILUgviDpIOwg7yDyIPUg+CD7IP4hASEEIQchCiENIRAhEyEWIRkhHCEfISIhJSEnITAhMyE1IT4hQSFDIUwhTyFRIVohXSFfIWghayFtIXYheSF7IYQhhyGJIZIhlSGXIaAhoyGlIa4hsSGzIbwhvyHBIcohzSHPIdgh2yHdIeYh6SHrIfQh9yH5IgIiBSIHIhAiEyIVIh4iISIjIiwiLyIxIjoiPSI/IkwiTyJRIlMiXCKHIooijSKQIpMiliKZIpwinyKiIqUiqCKrIq4isSK0IrciuiK9IsAiwyLFIs4i0SLTItwi3yLhIuoi7SLvIvgi+yL9IwYjCSMLIxQjFyMZIyIjJSMnIzAjMyM1Iz4jQSNDI0wjTyNRI1ojXSNfI2gjayNtI3YjeSN7I4QjhyOJI5IjlSOXI6AjoyOlI64jsSOzI7wjvyPBI8ojzSPPI9gj2yPdI+oj7SPvI/Ej+iQlJCgkKyQuJDEkNCQ3JDokPSRAJEMkRiRJJEwkTyRSJFUkWCRbJF4kYSRjJGwkbyRxJHokfSR/JIgkiySNJJYkmSSbJKQkpySpJLIktSS3JMAkwyTFJM4k0STTJNwk3yThJOok7STvJPgk+yT9JQYlCSULJRQlFyUZJSIlJSUnJTAlMyU1JT4lQSVDJUwlTyVRJVolXSVfJWglayVtJXYleSV7JYgliyWNJY8lmCXDJcYlySXMJc8l0iXVJdgl2yXeJeEl5CXnJeol7SXwJfMl9iX5Jfwl/yYBJgomDSYPJhgmGyYdJiYmKSYrJjQmNyY5JkImRSZHJlAmUyZVJl4mYSZjJmwmbyZxJnomfSZ/JogmiyaNJpYmmSabJqQmpyapJrImtSa3JsAmwybFJs4m0SbTJtwm3ybhJuom7SbvJvgm+yb9JwYnCScLJxQnFycZJyYnKScrJy0nNidhJ2QnZydqJ20ncCdzJ3YneSd8J38ngieFJ4gniyeOJ5EnlCeXJ5onnSefJ6gnqyetJ7YnuSe7J8QnxyfJJ9In1SfXJ+An4yflJ+4n8SfzJ/wn/ygBKAooDSgPKBgoGygdKCYoKSgrKDQoNyg5KEIoRShHKFAoUyhVKF4oYShjKGwobyhxKHoofSh/KIgoiyiNKJYomSibKKQopyipKLIotSi3KMQoxyjJKMso1Cj/KQIpBSkIKQspDikRKRQpFykaKR0pICkjKSYpKSksKS8pMik1KTgpOyk9KUYpSSlLKVQpVylZKWIpZSlnKXApcyl1KX4pgSmDKYwpjymRKZopnSmfKagpqymtKbYpuSm7KcQpxynJKdIp1SnXKeAp4ynlKe4p8SnzKfwp/yoBKgoqDSoPKhgqGyodKiYqKSorKjQqNyo5KkIqRSpHKlAqUypVKmIqZSpnKmkqciqdKqAqoyqmKqkqrCqvKrIqtSq4KrsqvirBKsQqxyrKKs0q0CrTKtYq2SrbKuQq5yrpKvIq9Sr3KwArAysFKw4rESsTKxwrHyshKyorLSsvKzgrOys9K0YrSStLK1QrVytZK2IrZStnK3Arcyt1K34rgSuDK4wrjyuRK5ornSufK6grqyutK7YruSu7K8QrxyvJK9Ir1SvXK+Ar4yvlK+4r8SvzLAAsAywFLAcsECw7LD4sQSxELEcsSixNLFAsUyxWLFksXCxfLGIsZSxoLGssbixxLHQsdyx5LIIshSyHLJAskyyVLJ4soSyjLKwsryyxLLosvSy/LMgsyyzNLNYs2SzbLOQs5yzpLPIs9Sz3LQAtAy0FLQ4tES0TLRwtHy0hLSotLS0vLTgtOy09LUYtSS1LLVQtVy1ZLWItZS1nLXAtcy11LX4tgS2DLYwtjy2RLZ4toS2jLaUtri3ZLdwt3y3iLeUt6C3rLe4t8S30Lfct+i39LgAuAy4GLgkuDC4PLhIuFS4XLiAuIy4lLi4uMS4zLjwuPy5BLkouTS5PLlguWy5dLmYuaS5rLnQudy55LoIuhS6HLpAuky6VLp4uoS6jLqwury6xLrouvS6/Lsguyy7NLtYu2S7bLuQu5y7pLvIu9S73LwAvAy8FLw4vES8TLxwvHy8hLyovLS8vLzwvPy9BL0MvTC93L3ovfS+AL4Mvhi+JL4wvjy+SL5UvmC+bL54voS+kL6cvqi+tL7Avsy+1L74vwS/DL8wvzy/RL9ov3S/fL+gv6y/tL/Yv+S/7MAQwBzAJMBIwFTAXMCAwIzAlMC4wMTAzMDwwPzBBMEowTTBPMFgwWzBdMGYwaTBrMHQwdzB5MIIwhTCHMJAwkzCVMJ4woTCjMKwwrzCxMLowvTC/MMgwyzDNMNow3TDfMOEw6jEVMRgxGzEeMSExJDEnMSoxLTEwMTMxNjE5MTwxPzFCMUUxSDFLMU4xUTFTMVwxXzFhMWoxbTFvMXgxezF9MYYxiTGLMZQxlzGZMaIxpTGnMbAxszG1Mb4xwTHDMcwxzzHRMdox3THfMegx6zHtMfYx+TH7MgQyBzIJMhIyFTIXMiAyIzIlMi4yMTIzMjwyPzJBMkoyTTJPMlgyWzJdMmYyaTJrMngyezJ9Mn8yiDKzMrYyuTK8Mr8ywjLFMsgyyzLOMtEy1DLXMtoy3TLgMuMy5jLpMuwy7zLxMvoy/TL/MwgzCzMNMxYzGTMbMyQzJzMpMzIzNTM3M0AzQzNFM04zUTNTM1wzXzNhM2ozbTNvM3gzezN9M4YziTOLM5QzlzOZM6IzpTOnM7AzszO1M74zwTPDM8wzzzPRM9oz3TPfM+gz6zPtM/Yz+TP7NAQ0BzQJNBY0GTQbNB00JjRRNFQ0VzRaNF00YDRjNGY0aTRsNG80cjR1NHg0ezR+NIE0hDSHNIo0jTSPNJg0mzSdNKY0qTSrNLQ0tzS5NMI0xTTHNNA00zTVNN404TTjNOw07zTxNPo0/TT/NQg1CzUNNRY1GTUbNSQ1JzUpNTI1NTU3NUA1QzVFNU41UTVTNVw1XzVhNWo1bTVvNXg1ezV9NYY1iTWLNZQ1lzWZNaI1pTWnNbQ1tzW5Nbs1xDXvNfI19TX4Nfs1/jYBNgQ2BzYKNg02EDYTNhY2GTYcNh82IjYlNig2KzYtNjY2OTY7NkQ2RzZJNlI2VTZXNmA2YzZlNm42cTZzNnw2fzaBNoo2jTaPNpg2mzadNqY2qTarNrQ2tza5NsI2xTbHNtA20zbVNt424TbjNuw27zbxNvo2/Tb/Nwg3CzcNNxY3GTcbNyQ3JzcpNzI3NTc3N0A3QzdFN043ezd+N4E3gzeGN4k3jDePN5I3lTeYN5s3njehN6M3pjepN6s3rTewN7M3tje4N8U3yDfKN9M3/jgBOAM4BTgHOAk4CzgNOA84ETgTOBU4FzgZOBs4HTgfOCE4IzglOCc4KTgyODU4NzhEOEc4SThSOIM4hTiHOIo4jDiOOJA4kziWOJk4nDifOKE4pDinOKo4rTiwOLM4tji5OLw4vzjCOMQ4zTjQONI42zjeOOA46TjsOO44+zj+OQA5CTkwOTM5Njk5OTw5PzlBOUQ5RzlKOU05UDlTOVY5WTlcOV85YjllOWc5dDl3OXk5gjmpOaw5rjmxObQ5tzm6Ob05wDnDOcY5yTnMOc850jnVOdg52jndOd857DnvOfE5+jonOio6LTowOjM6NTo4Ojo6PTpAOkM6RTpIOks6TjpROlQ6VzpaOl06YDpjOmU6bjpxOnM6fDp/OoE6jjqROpM6lTqeOsE6xDrHOso6zTrQOtM61jrZOtw63zriOuU66DrrOu468TrzOvw6/zsBOwo7DTsPOxg7GzsdOyY7KTsrOzQ7Nzs5O0I7RTtHO1A7UztVO147YTtjO2w7bztxO3o7fTt/O4g7izuNO5Y7mTubO6Q7pzupO7I7tTu3O8A7wzvFO8470TvTO+A74zvlO+c78DwZPBw8HzwiPCU8KDwrPC48MTw0PDc8Ojw9PEA8QzxGPEk8TDxPPFI8VDxdPGA8YjxrPG48cDx5PHw8fjyHPIo8jDyVPJg8mjyjPKY8qDyxPLQ8tjy/PMI8xDzNPNA80jzbPN484DzpPOw87jz3PPo8/D0FPQg9Cj0TPRY9GD0hPSQ9Jj0vPTI9ND09PUA9Qj1LPU49UD1ZPVw9Xj1rPW49cD15Pag9qz2uPbE9tD23Pbo9vT3APcI9xT3IPcs9zj3RPdQ91z3aPd094D3jPeY96T3rPfQ99z35PgI+BT4HPhQ+Fz4ZPiI+TT5QPlM+VT5YPls+Xj5hPmQ+Zz5qPm0+cD5zPnU+eD57Pn4+gT6EPoc+iT6SPpU+lz6kPqc+qT6yPuM+5j7pPuw+7z7yPvU++D77Pv4/AT8EPwY/CT8MPw8/Ej8VPxg/Gz8dPyA/Iz8mPyg/MT80PzY/Pz9CP0Q/TT9QP1I/Xz9iP2Q/bT+SP5U/mD+bP54/oT+kP6c/qj+tP68/sj+1P7g/uz++P8A/wz/FP9I/1T/XP+BAC0AOQBFAFEAXQBpAHEAfQCJAJEAnQClALEAvQDJANUA4QDtAPkBBQENARUBOQFFAU0BcQF9AYUBuQHFAc0B8QKtArkCwQLNAtkC4QLtAvkDBQMRAx0DKQMxAzkDRQNRA10DaQN1A4EDiQORA5kDoQPFA9ED2QP9BAkEEQQ1BEEESQR9BIkEkQSZBL0FSQVVBWEFbQV5BYUFkQWdBakFtQXBBc0F2QXlBfEF/QYJBhEGNQZBBkkGbQZ5BoEGpQaxBrkG3QbpBvEHFQchBykHTQdZB2EHhQeRB5kHvQfJB9EH9QgBCAkILQg5CEEIZQhxCHkInQipCLEI1QjhCOkJDQkZCSEJRQlRCVkJfQmJCZEJxQnRCdkJ/QqpCrUKwQrNCtkK5QrxCvkLBQsRCx0LKQs1C0ELTQtZC2ELbQt5C4ELjQuVC7kLxQvNC/EL/QwFDCkMNQw9DGEMbQx1DKkMtQy9DOENfQ2JDZUNnQ2pDbUNwQ3NDdkN5Q3xDf0OCQ4VDiEOLQ45DkUOTQ5VDnkOhQ6NDrEOvQ7FDvkPBQ8NDzEP3Q/pD/UQARANEBkQJRAxED0QSRBVEGEQbRB5EIUQkRCdEKkQtRDBEM0Q1RD5EQURDRFBEUkRVRF5EZURnRHBEm0SeRKFEpESnRKpErUSwRLNEtkS5RLxEv0TCRMVEyETLRM5E0UTURNdE2UTiRORE9UT3RPlFAkUERRFFE0UWRR9FJEUmRTNFNUU4RUFFRkVIRVlFW0VdRWZFaEVxRXNFfEV+RYdFiUWaRZxFpUWnRbBFskW/RcFFxEXNRdRF1kXjReVF6EXxRfZF+EYJRgtGDUYWRhhGJUYnRipGM0Y6RjxGSUZLRk5GV0ZgRmJGc0Z1RndGgEarRq5GsUa0RrdGuka9RsBGw0bGRslGzEbPRtJG1UbYRttG3kbhRuRG50bpRvJG9EcBRwNHBkcPRxRHFkcnRylHK0c8Rz5HQEdNR09HUkdbR2BHYkdvR3FHdEd9R4RHhkePR5FHmkecR61Hr0e4R7pHx0fJR8xH1UfaR9xH6UfrR+5H90f8R/5IB0gJSBZIGEgbSCRIKUgrSDhIOkg9SEZIT0hRSF5IYEhjSGxIdUh3SIBIgkiPSJFIlEidSKRIpkizSLVIuEjBSMZIyEjRSNNI3EkJSQxJD0kSSRVJGEkbSR5JIUkkSSdJKkktSTBJM0k2STlJPEk/SUJJRUlISUpJU0lWSVlJW0lkSWtJbklxSXRJdkl/SYBJg0mMSZFJmkmbSZ1JpkmnSalJskm3SbpJvUm/SchJzUnQSdNJ1UneSeFJ5EnmSe9J9kn5SfxJ/0oBSgpKC0oNShZKG0oeSiFKI0osSjNKNko5SjxKPkpHSlBKU0pWSllKXEpeSmdKbEpvSnJKdEp9SoBKg0qFSpJKlEqXSqBKp0qpSrJKuUq8Sr9KwkrESs1K2krfSuxK9UsESwlLGEsqSy9LNAAAAAAAAAICAAAAAAAADfoAAAAAAAAAAAAAAAAAAEs2 \ No newline at end of file From 3ca95778df40e376d352ff987dab00e9a7a1847e Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 17:11:21 -0700 Subject: [PATCH 07/10] [ASThrashTesting] Some cleanup --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index 6b8493966e..4289c62984 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -9,6 +9,7 @@ @import XCTest; #import +// Set to 1 to use UITableView and see if the issue still exists. #define USE_UIKIT_REFERENCE 0 #if USE_UIKIT_REFERENCE @@ -85,7 +86,6 @@ static volatile int32_t ASThrashTestItemNextID = 1; return (self.itemID % 400) ?: 44; } - - (NSString *)description { return [NSString stringWithFormat:@"", (unsigned long)_itemID]; } @@ -299,7 +299,6 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @property (nonatomic, strong, readonly) NSMutableArray *insertedItemIndexes; @property (nonatomic, strong, readonly) NSMutableArray *> *insertedItems; -/// NOTE: `data` will be modified - (instancetype)initWithData:(NSArray *)data; + (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64; From bd7f90f7ee41b32d137c4a145d418ca09ed836a8 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 17:14:01 -0700 Subject: [PATCH 08/10] [ASThrashTesting] Remove unneeded clang diagnostics --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index 4289c62984..cb1f9947ee 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -26,8 +26,6 @@ #define kFickleness 0.1 #define kThrashingIterationCount 100 -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-function" static NSString *ASThrashArrayDescription(NSArray *array) { NSMutableString *str = [NSMutableString stringWithString:@"(\n"]; NSInteger i = 0; @@ -38,7 +36,6 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [str appendString:@")"]; return str; } -#pragma clang diagnostic pop static volatile int32_t ASThrashTestItemNextID = 1; @interface ASThrashTestItem: NSObject From 9fc3ec9096e9fe5015708c9f761bf4069e5ef5fd Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 17:19:36 -0700 Subject: [PATCH 09/10] [ASThrashTesting] Numbers so magic --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index cb1f9947ee..f1c6c8805e 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -462,7 +462,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; #pragma mark Test Methods - (void)testInitialDataRead { - ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:20]]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; [self verifyDataSource:ds]; } @@ -484,7 +484,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; - (void)DISABLED_testThrashingWildly { for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; - ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:20]]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; _update = [[ASThrashUpdate alloc] initWithData:ds.data]; [self applyUpdate:_update toDataSource:ds]; From 12336de32548cf825bae840f8d8e844c084aa4c6 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 17:25:09 -0700 Subject: [PATCH 10/10] [ASThrashTesting] Array is immutable --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index f1c6c8805e..5d6fa8f637 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -308,7 +308,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; self = [super init]; if (self != nil) { _data = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; - _oldData = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; + _oldData = [[NSArray alloc] initWithArray:data copyItems:YES]; _deletedItemIndexes = [NSMutableArray array]; _replacedItemIndexes = [NSMutableArray array];