From 3730f801f24e5e04ad4d65d70d6589c60a4d78fe Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Thu, 8 Nov 2018 16:14:53 -0800 Subject: [PATCH 01/12] Split up table view thrash tests --- AsyncDisplayKit.xcodeproj/project.pbxproj | 6 + Tests/ASTableViewThrashTests.mm | 434 +--------------------- Tests/ASThrashUtility.h | 106 ++++++ Tests/ASThrashUtility.m | 386 +++++++++++++++++++ 4 files changed, 499 insertions(+), 433 deletions(-) create mode 100644 Tests/ASThrashUtility.h create mode 100644 Tests/ASThrashUtility.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 7566729fb..23883f1bd 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -190,6 +190,7 @@ 92DD2FE61BF4D05E0074C9DD /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92DD2FE51BF4D05E0074C9DD /* MapKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */; }; 92DD2FE81BF4D0A80074C9DD /* ASMapNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9644CFE02193777C00213478 /* ASThrashUtility.m in Sources */ = {isa = PBXBuildFile; fileRef = 9644CFDF2193777C00213478 /* ASThrashUtility.m */; }; 9C49C3701B853961000B0DD5 /* ASStackLayoutElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C55866B1BD54A1900B50E3A /* ASAsciiArtBoxCreator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.mm */; }; 9C55866C1BD54A3000B50E3A /* ASAsciiArtBoxCreator.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -759,6 +760,8 @@ 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMapNode.h; sourceTree = ""; }; 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMapNode.mm; sourceTree = ""; }; 92DD2FE51BF4D05E0074C9DD /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; + 9644CFDE2193777C00213478 /* ASThrashUtility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASThrashUtility.h; sourceTree = ""; }; + 9644CFDF2193777C00213478 /* ASThrashUtility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASThrashUtility.m; sourceTree = ""; }; 9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackLayoutElement.h; sourceTree = ""; }; 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAsciiArtBoxCreator.h; sourceTree = ""; }; 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASAsciiArtBoxCreator.mm; sourceTree = ""; }; @@ -1355,6 +1358,8 @@ 81E95C131D62639600336598 /* ASTextNodeSnapshotTests.mm */, 058D0A36195D057000B7D73C /* ASTextNodeTests.mm */, 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */, + 9644CFDE2193777C00213478 /* ASThrashUtility.h */, + 9644CFDF2193777C00213478 /* ASThrashUtility.m */, CCE4F9BC1F0ECE5200062E4E /* ASTLayoutFixture.h */, CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */, CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.mm */, @@ -2287,6 +2292,7 @@ CCB2F34D1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.mm in Sources */, AE6987C11DD04E1000B9E458 /* ASPagerNodeTests.mm in Sources */, 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.mm in Sources */, + 9644CFE02193777C00213478 /* ASThrashUtility.m in Sources */, 696FCB311D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm in Sources */, CC583AD81EF9BDC300134156 /* OCMockObject+ASAdditions.mm in Sources */, 69FEE53D1D95A9AF0086F066 /* ASLayoutElementStyleTests.mm in Sources */, diff --git a/Tests/ASTableViewThrashTests.mm b/Tests/ASTableViewThrashTests.mm index dbf94ba2b..f9a45f9f9 100644 --- a/Tests/ASTableViewThrashTests.mm +++ b/Tests/ASTableViewThrashTests.mm @@ -13,6 +13,7 @@ #import #import +#import "ASThrashUtility.h" // Set to 1 to use UITableView and see if the issue still exists. #define USE_UIKIT_REFERENCE 0 @@ -31,439 +32,6 @@ #define kFickleness 0.1 #define kThrashingIterationCount 100 -static NSString *ASThrashArrayDescription(NSArray *array) { - NSMutableString *str = [NSMutableString stringWithString:@"(\n"]; - NSInteger i = 0; - for (id obj in array) { - [str appendFormat:@"\t[%ld]: \"%@\",\n", (long)i, obj]; - i += 1; - } - [str appendString:@")"]; - return str; -} - -static atomic_uint ASThrashTestItemNextID; -@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) { - _itemID = atomic_fetch_add(&ASThrashTestItemNextID, 1); - } - 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) { - [result addObject:[[ASThrashTestItem alloc] init]]; - } - return result; -} - -- (CGFloat)rowHeight { - return (self.itemID % 400) ?: 44; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", (unsigned long)_itemID]; -} - -@end - -@interface ASThrashTestSection: NSObject -@property (nonatomic, readonly) NSMutableArray *items; -@property (nonatomic, readonly) NSInteger sectionID; - -- (CGFloat)headerHeight; -@end - -static atomic_uint ASThrashTestSectionNextID = 1; -@implementation ASThrashTestSection - -/// 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) { - [result addObject:[[ASThrashTestSection alloc] initWithCount:kInitialItemCount]]; - } - return result; -} - -- (instancetype)initWithCount:(NSInteger)count { - self = [super init]; - if (self != nil) { - _sectionID = atomic_fetch_add(&ASThrashTestSectionNextID, 1); - _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)_sectionID, (unsigned long)self.items.count, ASThrashArrayDescription(self.items)]; -} - -- (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 - -#if !USE_UIKIT_REFERENCE -@interface ASThrashTestNode: ASCellNode -@property (nonatomic) ASThrashTestItem *item; -@end - -@implementation ASThrashTestNode - -- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize -{ - ASDisplayNodeAssertFalse(isinf(constrainedSize.width)); - return CGSizeMake(constrainedSize.width, 44); -} - -@end -#endif - -@interface ASThrashDataSource: NSObject -#if USE_UIKIT_REFERENCE - -#else - -#endif - -@property (nonatomic, readonly) UIWindow *window; -@property (nonatomic, readonly) TableView *tableView; -@property (nonatomic) NSArray *data; -// Only access on main -@property (nonatomic) ASWeakSet *allNodes; -@end - - -@implementation ASThrashDataSource - -- (instancetype)initWithData:(NSArray *)data { - self = [super init]; - if (self != nil) { - _data = [[NSArray alloc] initWithArray:data copyItems:YES]; - _window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - _tableView = [[TableView alloc] initWithFrame:_window.bounds style:UITableViewStylePlain]; - _allNodes = [[ASWeakSet alloc] init]; - [_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 reloadData]; - [_tableView waitUntilAllUpdatesAreCommitted]; -#endif - [_tableView layoutIfNeeded]; - } - 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; -} - -/// Object passed into predicate is ignored. -- (NSPredicate *)predicateForDeallocatedHierarchy -{ - ASWeakSet *allNodes = self.allNodes; - __weak UIWindow *window = _window; - __weak ASTableView *view = _tableView; - return [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { - return window == nil && view == nil && allNodes.isEmpty; - }]; -} - -#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 - -- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath -{ - ASThrashTestNode *node = [[ASThrashTestNode alloc] init]; - node.item = self.data[indexPath.section].items[indexPath.item]; - [self.allNodes addObject:node]; - return node; -} - -#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; -} - -/// `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, readonly) NSArray *oldData; -@property (nonatomic, readonly) NSMutableArray *data; -@property (nonatomic, readonly) NSMutableIndexSet *deletedSectionIndexes; -@property (nonatomic, readonly) NSMutableIndexSet *replacedSectionIndexes; -/// The sections used to replace the replaced sections. -@property (nonatomic, readonly) NSMutableArray *replacingSections; -@property (nonatomic, readonly) NSMutableIndexSet *insertedSectionIndexes; -@property (nonatomic, readonly) NSMutableArray *insertedSections; -@property (nonatomic, readonly) NSMutableArray *deletedItemIndexes; -@property (nonatomic, readonly) NSMutableArray *replacedItemIndexes; -/// The items used to replace the replaced items. -@property (nonatomic, readonly) NSMutableArray *> *replacingItems; -@property (nonatomic, readonly) NSMutableArray *insertedItemIndexes; -@property (nonatomic, readonly) NSMutableArray *> *insertedItems; - -- (instancetype)initWithData:(NSArray *)data; - -+ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64; -- (NSString *)base64Representation; -@end - -@implementation ASThrashUpdate - -- (instancetype)initWithData:(NSArray *)data { - self = [super init]; - if (self != nil) { - _data = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; - _oldData = [[NSArray 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) { - 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]; - _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]; - [_insertedItems addObject:newItems]; - [_insertedItemIndexes addObject:indexes]; - } else { - [_insertedItems addObject:@[]]; - [_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", - @"data", - @"deletedSectionIndexes", - @"replacedSectionIndexes", - @"replacingSections", - @"insertedSectionIndexes", - @"insertedSections", - @"deletedItemIndexes", - @"replacedItemIndexes", - @"replacingItems", - @"insertedItemIndexes", - @"insertedItems" - ]]; - [aCoder encodeObject:dict forKey:@"_dict"]; - [aCoder encodeInteger: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; -} - -- (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 - @interface ASTableViewThrashTests: XCTestCase @end diff --git a/Tests/ASThrashUtility.h b/Tests/ASThrashUtility.h new file mode 100644 index 000000000..69518d65c --- /dev/null +++ b/Tests/ASThrashUtility.h @@ -0,0 +1,106 @@ +// +// ASThrashUtility.h +// AsyncDisplayKitTests +// +// Created by Michael Zuccarino on 11/7/18. +// Copyright © 2018 Pinterest. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#define kInitialSectionCount 10 +#define kInitialItemCount 10 +#define kMinimumItemCount 5 +#define kMinimumSectionCount 3 +#define kFickleness 0.1 +#define kThrashingIterationCount 100 + +// Set to 1 to use UITableView and see if the issue still exists. +#define USE_UIKIT_REFERENCE 0 + +#if USE_UIKIT_REFERENCE +#define TableView UITableView +#define kCellReuseID @"ASThrashTestCellReuseID" +#else +#define TableView ASTableView +#endif + +static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; + +@class ASThrashTestSection; +static atomic_uint ASThrashTestItemNextID; +@interface ASThrashTestItem: NSObject +@property (nonatomic, readonly) NSInteger itemID; + ++ (NSMutableArray *)itemsWithCount:(NSInteger)count; + +- (CGFloat)rowHeight; +@end + + +@interface ASThrashTestSection: NSObject +@property (nonatomic, readonly) NSMutableArray *items; +@property (nonatomic, readonly) NSInteger sectionID; + ++ (NSMutableArray *)sectionsWithCount:(NSInteger)count; + +- (instancetype)initWithCount:(NSInteger)count; +- (CGFloat)headerHeight; +@end + +@interface ASThrashDataSource: NSObject +#if USE_UIKIT_REFERENCE + +#else + +#endif + +@property (nonatomic, readonly) UIWindow *window; +@property (nonatomic, readonly) TableView *tableView; +@property (nonatomic) NSArray *data; +// Only access on main +@property (nonatomic) ASWeakSet *allNodes; + +- (instancetype)initWithData:(NSArray *)data; +- (NSPredicate *)predicateForDeallocatedHierarchy; +@end + +@interface NSIndexSet (ASThrashHelpers) +- (NSArray *)indexPathsInSection:(NSInteger)section; +/// `insertMode` means that for each index selected, the max goes up by one. ++ (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode; +@end + +#if !USE_UIKIT_REFERENCE +@interface ASThrashTestNode: ASCellNode +@property (nonatomic) ASThrashTestItem *item; +@end +#endif + +@interface ASThrashUpdate : NSObject +@property (nonatomic, readonly) NSArray *oldData; +@property (nonatomic, readonly) NSMutableArray *data; +@property (nonatomic, readonly) NSMutableIndexSet *deletedSectionIndexes; +@property (nonatomic, readonly) NSMutableIndexSet *replacedSectionIndexes; +/// The sections used to replace the replaced sections. +@property (nonatomic, readonly) NSMutableArray *replacingSections; +@property (nonatomic, readonly) NSMutableIndexSet *insertedSectionIndexes; +@property (nonatomic, readonly) NSMutableArray *insertedSections; +@property (nonatomic, readonly) NSMutableArray *deletedItemIndexes; +@property (nonatomic, readonly) NSMutableArray *replacedItemIndexes; +/// The items used to replace the replaced items. +@property (nonatomic, readonly) NSMutableArray *> *replacingItems; +@property (nonatomic, readonly) NSMutableArray *insertedItemIndexes; +@property (nonatomic, readonly) NSMutableArray *> *insertedItems; + +- (instancetype)initWithData:(NSArray *)data; + ++ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64; +- (NSString *)base64Representation; +- (NSString *)logFriendlyBase64Representation; +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/ASThrashUtility.m b/Tests/ASThrashUtility.m new file mode 100644 index 000000000..9a6e814d8 --- /dev/null +++ b/Tests/ASThrashUtility.m @@ -0,0 +1,386 @@ +// +// ASThrashUtility.m +// AsyncDisplayKitTests +// +// Created by Michael Zuccarino on 11/7/18. +// Copyright © 2018 Pinterest. All rights reserved. +// + +#import "ASThrashUtility.h" +#import +#import + +static NSString *ASThrashArrayDescription(NSArray *array) { + NSMutableString *str = [NSMutableString stringWithString:@"(\n"]; + NSInteger i = 0; + for (id obj in array) { + [str appendFormat:@"\t[%ld]: \"%@\",\n", (long)i, obj]; + i += 1; + } + [str appendString:@")"]; + return str; +} + +@implementation ASThrashTestItem + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)init { + self = [super init]; + if (self != nil) { + _itemID = atomic_fetch_add(&ASThrashTestItemNextID, 1); + } + 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) { + [result addObject:[[ASThrashTestItem alloc] init]]; + } + return result; +} + +- (CGFloat)rowHeight { + return (self.itemID % 400) ?: 44; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", (unsigned long)_itemID]; +} + +@end + +static atomic_uint ASThrashTestSectionNextID = 1; +@implementation ASThrashTestSection + +/// 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) { + [result addObject:[[ASThrashTestSection alloc] initWithCount:kInitialItemCount]]; + } + return result; +} + +- (instancetype)initWithCount:(NSInteger)count { + self = [super init]; + if (self != nil) { + _sectionID = atomic_fetch_add(&ASThrashTestSectionNextID, 1); + _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)_sectionID, (unsigned long)self.items.count, ASThrashArrayDescription(self.items)]; +} + +- (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 + +@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; +} + +/// `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 + +@implementation ASThrashDataSource + +- (instancetype)initWithData:(NSArray *)data { + self = [super init]; + if (self != nil) { + _data = [[NSArray alloc] initWithArray:data copyItems:YES]; + _window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + _tableView = [[TableView alloc] initWithFrame:_window.bounds style:UITableViewStylePlain]; + _allNodes = [[ASWeakSet alloc] init]; + [_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 reloadData]; + [_tableView waitUntilAllUpdatesAreCommitted]; +#endif + [_tableView layoutIfNeeded]; + } + 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; +} + +/// Object passed into predicate is ignored. +- (NSPredicate *)predicateForDeallocatedHierarchy +{ + ASWeakSet *allNodes = self.allNodes; + __weak UIWindow *window = _window; + __weak ASTableView *view = _tableView; + return [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { + return window == nil && view == nil && allNodes.isEmpty; + }]; +} + +#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 + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + ASThrashTestNode *node = [[ASThrashTestNode alloc] init]; + node.item = self.data[indexPath.section].items[indexPath.item]; + [self.allNodes addObject:node]; + return node; +} + +#endif + +@end + +#if !USE_UIKIT_REFERENCE +@implementation ASThrashTestNode + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASDisplayNodeAssertFalse(isinf(constrainedSize.width)); + return CGSizeMake(constrainedSize.width, 44); +} + +@end +#endif + +@implementation ASThrashUpdate + +- (instancetype)initWithData:(NSArray *)data { + self = [super init]; + if (self != nil) { + _data = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; + _oldData = [[NSArray 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) { + 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]; + _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]; + [_insertedItems addObject:newItems]; + [_insertedItemIndexes addObject:indexes]; + } else { + [_insertedItems addObject:@[]]; + [_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", + @"data", + @"deletedSectionIndexes", + @"replacedSectionIndexes", + @"replacingSections", + @"insertedSectionIndexes", + @"insertedSections", + @"deletedItemIndexes", + @"replacedItemIndexes", + @"replacingItems", + @"insertedItemIndexes", + @"insertedItems" + ]]; + [aCoder encodeObject:dict forKey:@"_dict"]; + [aCoder encodeInteger: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; +} + +- (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 From f0ff7c02ca7fd3e1a0994a7e9723486693122921 Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Mon, 12 Nov 2018 14:38:56 -0800 Subject: [PATCH 02/12] Fix license --- Tests/ASThrashUtility.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ASThrashUtility.h b/Tests/ASThrashUtility.h index 69518d65c..cd14d0f76 100644 --- a/Tests/ASThrashUtility.h +++ b/Tests/ASThrashUtility.h @@ -1,9 +1,9 @@ // // ASThrashUtility.h -// AsyncDisplayKitTests +// Texture // -// Created by Michael Zuccarino on 11/7/18. -// Copyright © 2018 Pinterest. All rights reserved. +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // #import From edd3c11bdcfa0357f542585454b99d9675310a90 Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Mon, 12 Nov 2018 14:39:23 -0800 Subject: [PATCH 03/12] Fix license --- Tests/ASThrashUtility.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ASThrashUtility.m b/Tests/ASThrashUtility.m index 9a6e814d8..b2d15fdd2 100644 --- a/Tests/ASThrashUtility.m +++ b/Tests/ASThrashUtility.m @@ -1,9 +1,9 @@ // // ASThrashUtility.m -// AsyncDisplayKitTests +// Texture // -// Created by Michael Zuccarino on 11/7/18. -// Copyright © 2018 Pinterest. All rights reserved. +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // #import "ASThrashUtility.h" From cab2df8f62d4ca5df3931f28ddc6021ff95a3e5c Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Wed, 14 Nov 2018 16:26:54 -0800 Subject: [PATCH 04/12] Reenable thrash tests for table view --- Tests/ASTableViewThrashTests.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ASTableViewThrashTests.mm b/Tests/ASTableViewThrashTests.mm index f9a45f9f9..c9ac79393 100644 --- a/Tests/ASTableViewThrashTests.mm +++ b/Tests/ASTableViewThrashTests.mm @@ -60,13 +60,13 @@ - (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *) #pragma mark Test Methods // Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. -- (void)DISABLED_testInitialDataRead { +- (void)testInitialDataRead { ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; [self verifyDataSource:ds]; } /// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file -- (void)DISABLED_testRecordedThrashCase { +- (void)testRecordedThrashCase { NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL]; @@ -82,7 +82,7 @@ - (void)DISABLED_testRecordedThrashCase { } // Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. -- (void)DISABLED_testThrashingWildly { +- (void)testThrashingWildly { for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; @autoreleasepool { From 26f1ce0b57b15732fa298d770e7030dbca7ccdfc Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Mon, 19 Nov 2018 15:51:29 -0800 Subject: [PATCH 05/12] Creating the collection view thrash tests --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 ++ Tests/ASCollectionViewThrashTests.mm | 63 +++++++++++++++++++++++ Tests/ASTableViewThrashTests.mm | 25 ++------- Tests/ASThrashUtility.h | 6 ++- Tests/ASThrashUtility.m | 25 ++++++++- 5 files changed, 100 insertions(+), 23 deletions(-) create mode 100644 Tests/ASCollectionViewThrashTests.mm diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 23883f1bd..1c1318902 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -191,6 +191,7 @@ 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */; }; 92DD2FE81BF4D0A80074C9DD /* ASMapNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9644CFE02193777C00213478 /* ASThrashUtility.m in Sources */ = {isa = PBXBuildFile; fileRef = 9644CFDF2193777C00213478 /* ASThrashUtility.m */; }; + 9692B4FF219E12370060C2C3 /* ASCollectionViewThrashTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9692B4FE219E12370060C2C3 /* ASCollectionViewThrashTests.mm */; }; 9C49C3701B853961000B0DD5 /* ASStackLayoutElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C55866B1BD54A1900B50E3A /* ASAsciiArtBoxCreator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.mm */; }; 9C55866C1BD54A3000B50E3A /* ASAsciiArtBoxCreator.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -762,6 +763,7 @@ 92DD2FE51BF4D05E0074C9DD /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; 9644CFDE2193777C00213478 /* ASThrashUtility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASThrashUtility.h; sourceTree = ""; }; 9644CFDF2193777C00213478 /* ASThrashUtility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASThrashUtility.m; sourceTree = ""; }; + 9692B4FE219E12370060C2C3 /* ASCollectionViewThrashTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionViewThrashTests.mm; sourceTree = ""; }; 9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackLayoutElement.h; sourceTree = ""; }; 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAsciiArtBoxCreator.h; sourceTree = ""; }; 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASAsciiArtBoxCreator.mm; sourceTree = ""; }; @@ -1287,6 +1289,7 @@ 058D09C5195D04C000B7D73C /* Tests */ = { isa = PBXGroup; children = ( + 9692B4FE219E12370060C2C3 /* ASCollectionViewThrashTests.mm */, F325E48F217460B000AC93A4 /* ASTextNode2Tests.mm */, F325E48B21745F9E00AC93A4 /* ASButtonNodeTests.mm */, F3F698D1211CAD4600800CB1 /* ASDisplayViewAccessibilityTests.mm */, @@ -2311,6 +2314,7 @@ 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.mm in Sources */, 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.mm in Sources */, F325E48C21745F9E00AC93A4 /* ASButtonNodeTests.mm in Sources */, + 9692B4FF219E12370060C2C3 /* ASCollectionViewThrashTests.mm in Sources */, E586F96C1F9F9E2900ECE00E /* ASScrollNodeTests.mm in Sources */, CC8B05D81D73979700F54286 /* ASTextNodePerformanceTests.mm in Sources */, CC583AD91EF9BDC600134156 /* ASDisplayNode+OCMock.mm in Sources */, diff --git a/Tests/ASCollectionViewThrashTests.mm b/Tests/ASCollectionViewThrashTests.mm new file mode 100644 index 000000000..2368020f4 --- /dev/null +++ b/Tests/ASCollectionViewThrashTests.mm @@ -0,0 +1,63 @@ +// +// ASCollectionViewThrashTests.m +// AsyncDisplayKitTests +// +// Created by Michael Zuccarino on 11/15/18. +// Copyright © 2018 Pinterest. All rights reserved. +// + +#import +#import +#import +#import + +#import "ASThrashUtility.h" + +@interface ASCollectionViewThrashTests : XCTestCase + +@end + +@implementation ASCollectionViewThrashTests { + // The current update, which will be logged in case of a failure. + ASThrashUpdate *_update; + BOOL _failed; +} + +- (void)tearDown { + if (_failed && _update != nil) { + NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation); + } + _failed = NO; + _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 { + _failed = YES; + [super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; +} + +- (void)verifyDataSource:(ASThrashDataSource *)ds { + CollectionView *collectionView = ds.collectionView; + NSArray *data = [ds data]; + for (NSInteger i = 0; i < collectionView.numberOfSections; i++) { + XCTAssertEqual([collectionView numberOfItemsInSection:i], data[i].items.count); + + for (NSInteger j = 0; j < [collectionView numberOfItemsInSection:i]; j++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i]; + ASThrashTestItem *item = data[i].items[j]; + ASThrashTestNode *node = (ASThrashTestNode *)[collectionView nodeForItemAtIndexPath:indexPath]; + XCTAssertEqualObjects(node.item, item, @"Wrong node at index path %@", indexPath); + } + } +} + +#pragma mark Test Methods + +// Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. +- (void)testInitialDataRead { + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; + [self verifyDataSource:ds]; +} + +@end diff --git a/Tests/ASTableViewThrashTests.mm b/Tests/ASTableViewThrashTests.mm index c9ac79393..d5e9bb183 100644 --- a/Tests/ASTableViewThrashTests.mm +++ b/Tests/ASTableViewThrashTests.mm @@ -15,23 +15,6 @@ #import "ASThrashUtility.h" -// Set to 1 to use UITableView and see if the issue still exists. -#define USE_UIKIT_REFERENCE 0 - -#if USE_UIKIT_REFERENCE -#define TableView UITableView -#define kCellReuseID @"ASThrashTestCellReuseID" -#else -#define TableView ASTableView -#endif - -#define kInitialSectionCount 10 -#define kInitialItemCount 10 -#define kMinimumItemCount 5 -#define kMinimumSectionCount 3 -#define kFickleness 0.1 -#define kThrashingIterationCount 100 - @interface ASTableViewThrashTests: XCTestCase @end @@ -61,7 +44,7 @@ - (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *) // Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. - (void)testInitialDataRead { - ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initTableViewDataSourceWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; [self verifyDataSource:ds]; } @@ -75,7 +58,7 @@ - (void)testRecordedThrashCase { return; } - ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:_update.oldData]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initTableViewDataSourceWithData:_update.oldData]; ds.tableView.test_enableSuperUpdateCallLogging = YES; [self applyUpdate:_update toDataSource:ds]; [self verifyDataSource:ds]; @@ -88,7 +71,7 @@ - (void)testThrashingWildly { @autoreleasepool { NSArray *sections = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; _update = [[ASThrashUpdate alloc] initWithData:sections]; - ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:sections]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initTableViewDataSourceWithData:sections]; [self applyUpdate:_update toDataSource:ds]; [self verifyDataSource:ds]; @@ -146,7 +129,7 @@ - (void)verifyDataSource:(ASThrashDataSource *)ds { 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]; diff --git a/Tests/ASThrashUtility.h b/Tests/ASThrashUtility.h index cd14d0f76..00ae05102 100644 --- a/Tests/ASThrashUtility.h +++ b/Tests/ASThrashUtility.h @@ -23,9 +23,11 @@ NS_ASSUME_NONNULL_BEGIN #if USE_UIKIT_REFERENCE #define TableView UITableView +#define CollectionView UICollectionView #define kCellReuseID @"ASThrashTestCellReuseID" #else #define TableView ASTableView +#define CollectionView ASCollectionNode #endif static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @@ -60,11 +62,13 @@ static atomic_uint ASThrashTestItemNextID; @property (nonatomic, readonly) UIWindow *window; @property (nonatomic, readonly) TableView *tableView; +@property (nonatomic, readonly) CollectionView *collectionView; @property (nonatomic) NSArray *data; // Only access on main @property (nonatomic) ASWeakSet *allNodes; -- (instancetype)initWithData:(NSArray *)data; +- (instancetype)initTableViewDataSourceWithData:(NSArray *)data; +- (instancetype)initCollectionViewDataSourceWithData:(NSArray *)data; - (NSPredicate *)predicateForDeallocatedHierarchy; @end diff --git a/Tests/ASThrashUtility.m b/Tests/ASThrashUtility.m index b2d15fdd2..ce0944134 100644 --- a/Tests/ASThrashUtility.m +++ b/Tests/ASThrashUtility.m @@ -164,7 +164,7 @@ + (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)p @implementation ASThrashDataSource -- (instancetype)initWithData:(NSArray *)data { +- (instancetype)initTableViewDataSourceWithData:(NSArray *)data { self = [super init]; if (self != nil) { _data = [[NSArray alloc] initWithArray:data copyItems:YES]; @@ -187,6 +187,29 @@ - (instancetype)initWithData:(NSArray *)data { return self; } +- (instancetype)initCollectionViewDataSourceWithData:(NSArray *)data { + self = [super init]; + if (self != nil) { + _data = [[NSArray alloc] initWithArray:data copyItems:YES]; + _window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + _collectionView = [[CollectionView alloc] initWithCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init]]; + _allNodes = [[ASWeakSet alloc] init]; + [_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 reloadData]; + [_tableView waitUntilAllUpdatesAreCommitted]; +#endif + [_tableView layoutIfNeeded]; + } + return self; +} + - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.data[section].items.count; } From 1dbb74b92259a8a5bf9233f344b79fac50c9015d Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Tue, 20 Nov 2018 12:54:15 -0800 Subject: [PATCH 06/12] Batch update animated tests --- Tests/ASCollectionViewThrashTests.mm | 85 ++++++++++++++++++++++++++++ Tests/ASThrashUtility.h | 4 +- Tests/ASThrashUtility.m | 30 +++++++--- 3 files changed, 109 insertions(+), 10 deletions(-) diff --git a/Tests/ASCollectionViewThrashTests.mm b/Tests/ASCollectionViewThrashTests.mm index 2368020f4..a9fb057d6 100644 --- a/Tests/ASCollectionViewThrashTests.mm +++ b/Tests/ASCollectionViewThrashTests.mm @@ -60,4 +60,89 @@ - (void)testInitialDataRead { [self verifyDataSource:ds]; } +/// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file +- (void)testRecordedThrashCase { + NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; + NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL]; + + _update = [ASThrashUpdate thrashUpdateWithBase64String:base64]; + if (_update == nil) { + return; + } + + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:_update.oldData]; + // why is ds.collectionView.test_enableSuperUpdateCallLogging available on table view but now collection view>? +// ds.collectionView.test_enableSuperUpdateCallLogging = YES; + [self applyUpdateUsingBatchUpdates:_update + toDataSource:ds + animated:NO]; + [self verifyDataSource:ds]; +} + +#pragma mark Helpers + +- (void)applyUpdateUsingBatchUpdates:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource animated:(BOOL)animated { + CollectionView *collectionView = dataSource.collectionView; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for collection view to update"]; + + void (^updateBlock)() = ^ void (){ + dataSource.data = update.data; + + [collectionView insertSections:update.insertedSectionIndexes]; + [collectionView deleteSections:update.insertedSectionIndexes]; + [collectionView reloadSections:update.insertedSectionIndexes]; + }; + + BOOL failed = NO; + @try { + [collectionView performBatchAnimated:animated + updates:updateBlock + completion:^(BOOL finished) { + [expectation fulfill]; + }]; + } @catch (NSException *exception) { + failed = YES; + @throw exception; + } + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + +// CollectionView *collectionView = dataSource.collectionView; +// +// [collectionView begin]; +// 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 endUpdatesAnimated:NO completion:nil]; +//#if !USE_UIKIT_REFERENCE +// [tableView waitUntilAllUpdatesAreCommitted]; +//#endif +// } @catch (NSException *exception) { +// _failed = YES; +// @throw exception; +// } +} + @end diff --git a/Tests/ASThrashUtility.h b/Tests/ASThrashUtility.h index 00ae05102..63047baae 100644 --- a/Tests/ASThrashUtility.h +++ b/Tests/ASThrashUtility.h @@ -55,9 +55,9 @@ static atomic_uint ASThrashTestItemNextID; @interface ASThrashDataSource: NSObject #if USE_UIKIT_REFERENCE - + #else - + #endif @property (nonatomic, readonly) UIWindow *window; diff --git a/Tests/ASThrashUtility.m b/Tests/ASThrashUtility.m index ce0944134..07050e07f 100644 --- a/Tests/ASThrashUtility.m +++ b/Tests/ASThrashUtility.m @@ -195,17 +195,15 @@ - (instancetype)initCollectionViewDataSourceWithData:(NSArray Date: Tue, 20 Nov 2018 13:24:48 -0800 Subject: [PATCH 07/12] Thrash wildly dispatch main --- Tests/ASCollectionViewThrashTests.mm | 123 +++++++++++++++++---------- Tests/ASThrashUtility.h | 2 +- 2 files changed, 80 insertions(+), 45 deletions(-) diff --git a/Tests/ASCollectionViewThrashTests.mm b/Tests/ASCollectionViewThrashTests.mm index a9fb057d6..660ed58c5 100644 --- a/Tests/ASCollectionViewThrashTests.mm +++ b/Tests/ASCollectionViewThrashTests.mm @@ -75,26 +75,94 @@ - (void)testRecordedThrashCase { // ds.collectionView.test_enableSuperUpdateCallLogging = YES; [self applyUpdateUsingBatchUpdates:_update toDataSource:ds - animated:NO]; + animated:NO + useXCTestWait:YES]; [self verifyDataSource:ds]; } +- (void)testThrashingWildly { + for (NSInteger i = 0; i < kThrashingIterationCount; i++) { + [self setUp]; + @autoreleasepool { + NSArray *sections = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + _update = [[ASThrashUpdate alloc] initWithData:sections]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:sections]; + + [self applyUpdateUsingBatchUpdates:_update + toDataSource:ds + animated:NO + useXCTestWait:NO]; + [self verifyDataSource:ds]; + [self expectationForPredicate:[ds predicateForDeallocatedHierarchy] evaluatedWithObject:(id)kCFNull handler:nil]; + } + [self waitForExpectationsWithTimeout:3 handler:nil]; + + [self tearDown]; + } +} + +- (void)testThrashingWildlyDispatchWildly { + XCTestExpectation *expectation = [self expectationWithDescription:@"last test ran"]; + for (NSInteger i = 0; i < kThrashingIterationCount; i++) { + [self setUp]; + @autoreleasepool { + dispatch_async(dispatch_get_main_queue(), ^{ + NSArray *sections = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + _update = [[ASThrashUpdate alloc] initWithData:sections]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:sections]; + + [self applyUpdateUsingBatchUpdates:_update + toDataSource:ds + animated:NO + useXCTestWait:NO]; + [self verifyDataSource:ds]; + if (i == kThrashingIterationCount-1) { + [expectation fulfill]; + } + }); + } + + [self tearDown]; + } + + [self waitForExpectationsWithTimeout:60 handler:nil]; +} + #pragma mark Helpers -- (void)applyUpdateUsingBatchUpdates:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource animated:(BOOL)animated { +- (void)applyUpdateUsingBatchUpdates:(ASThrashUpdate *)update + toDataSource:(ASThrashDataSource *)dataSource animated:(BOOL)animated + useXCTestWait:(BOOL)wait { CollectionView *collectionView = dataSource.collectionView; - XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for collection view to update"]; + XCTestExpectation *expectation; + if (wait) { + expectation = [self expectationWithDescription:@"Wait for collection view to update"]; + } void (^updateBlock)() = ^ void (){ dataSource.data = update.data; [collectionView insertSections:update.insertedSectionIndexes]; - [collectionView deleteSections:update.insertedSectionIndexes]; - [collectionView reloadSections:update.insertedSectionIndexes]; + [collectionView deleteSections:update.deletedSectionIndexes]; + [collectionView reloadSections:update.replacedSectionIndexes]; + + [update.insertedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [collectionView insertItemsAtIndexPaths:indexPaths]; + }]; + + [update.deletedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [collectionView deleteItemsAtIndexPaths:indexPaths]; + }]; + + [update.replacedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [collectionView reloadItemsAtIndexPaths:indexPaths]; + }]; }; - BOOL failed = NO; @try { [collectionView performBatchAnimated:animated updates:updateBlock @@ -102,47 +170,14 @@ - (void)applyUpdateUsingBatchUpdates:(ASThrashUpdate *)update toDataSource:(ASTh [expectation fulfill]; }]; } @catch (NSException *exception) { - failed = YES; + _failed = YES; + XCTFail("TEST FAILED"); @throw exception; } - [self waitForExpectationsWithTimeout:5 handler:nil]; - - -// CollectionView *collectionView = dataSource.collectionView; -// -// [collectionView begin]; -// 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 endUpdatesAnimated:NO completion:nil]; -//#if !USE_UIKIT_REFERENCE -// [tableView waitUntilAllUpdatesAreCommitted]; -//#endif -// } @catch (NSException *exception) { -// _failed = YES; -// @throw exception; -// } + if (wait) { + [self waitForExpectationsWithTimeout:5 handler:nil]; + } } @end diff --git a/Tests/ASThrashUtility.h b/Tests/ASThrashUtility.h index 63047baae..aae12c992 100644 --- a/Tests/ASThrashUtility.h +++ b/Tests/ASThrashUtility.h @@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN #define kMinimumItemCount 5 #define kMinimumSectionCount 3 #define kFickleness 0.1 -#define kThrashingIterationCount 100 +#define kThrashingIterationCount 2 // Set to 1 to use UITableView and see if the issue still exists. #define USE_UIKIT_REFERENCE 0 From a99f028b274c6616983e2156e43456c3f856fa80 Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Tue, 20 Nov 2018 13:35:53 -0800 Subject: [PATCH 08/12] Reset the thrash count --- Tests/ASCollectionViewThrashTests.mm | 13 +++++++------ Tests/ASThrashUtility.h | 7 ++++--- Tests/ASThrashUtility.m | 5 +++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Tests/ASCollectionViewThrashTests.mm b/Tests/ASCollectionViewThrashTests.mm index 660ed58c5..3d363fcdc 100644 --- a/Tests/ASCollectionViewThrashTests.mm +++ b/Tests/ASCollectionViewThrashTests.mm @@ -1,9 +1,10 @@ // -// ASCollectionViewThrashTests.m -// AsyncDisplayKitTests +// ASTableViewThrashTests.mm +// Texture // -// Created by Michael Zuccarino on 11/15/18. -// Copyright © 2018 Pinterest. All rights reserved. +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // #import @@ -125,7 +126,7 @@ - (void)testThrashingWildlyDispatchWildly { [self tearDown]; } - [self waitForExpectationsWithTimeout:60 handler:nil]; + [self waitForExpectationsWithTimeout:100 handler:nil]; } #pragma mark Helpers @@ -176,7 +177,7 @@ - (void)applyUpdateUsingBatchUpdates:(ASThrashUpdate *)update } if (wait) { - [self waitForExpectationsWithTimeout:5 handler:nil]; + [self waitForExpectationsWithTimeout:1 handler:nil]; } } diff --git a/Tests/ASThrashUtility.h b/Tests/ASThrashUtility.h index aae12c992..653289740 100644 --- a/Tests/ASThrashUtility.h +++ b/Tests/ASThrashUtility.h @@ -1,8 +1,9 @@ // -// ASThrashUtility.h +// ASTableViewThrashTests.mm // Texture // -// Copyright (c) Pinterest, Inc. All rights reserved. +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // @@ -16,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN #define kMinimumItemCount 5 #define kMinimumSectionCount 3 #define kFickleness 0.1 -#define kThrashingIterationCount 2 +#define kThrashingIterationCount 100 // Set to 1 to use UITableView and see if the issue still exists. #define USE_UIKIT_REFERENCE 0 diff --git a/Tests/ASThrashUtility.m b/Tests/ASThrashUtility.m index 07050e07f..2a93eb5f9 100644 --- a/Tests/ASThrashUtility.m +++ b/Tests/ASThrashUtility.m @@ -1,8 +1,9 @@ // -// ASThrashUtility.m +// ASTableViewThrashTests.mm // Texture // -// Copyright (c) Pinterest, Inc. All rights reserved. +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // From d29cb0e85a8cbba2fa0f1bb48e72dc9e027109df Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Tue, 18 Dec 2018 13:45:32 -0800 Subject: [PATCH 09/12] One more test --- Tests/ASCollectionViewThrashTests.mm | 50 ++++++++++++++++++++++------ Tests/ASThrashUtility.h | 4 +-- Tests/ASThrashUtility.m | 6 +++- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/Tests/ASCollectionViewThrashTests.mm b/Tests/ASCollectionViewThrashTests.mm index 3d363fcdc..8d31095b3 100644 --- a/Tests/ASCollectionViewThrashTests.mm +++ b/Tests/ASCollectionViewThrashTests.mm @@ -1,5 +1,5 @@ // -// ASTableViewThrashTests.mm +// ASCollectionViewThrashTests.mm // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. @@ -33,12 +33,14 @@ - (void)tearDown { } // 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 { +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected +{ _failed = YES; [super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; } -- (void)verifyDataSource:(ASThrashDataSource *)ds { +- (void)verifyDataSource:(ASThrashDataSource *)ds +{ CollectionView *collectionView = ds.collectionView; NSArray *data = [ds data]; for (NSInteger i = 0; i < collectionView.numberOfSections; i++) { @@ -55,14 +57,15 @@ - (void)verifyDataSource:(ASThrashDataSource *)ds { #pragma mark Test Methods -// Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. -- (void)testInitialDataRead { +- (void)testInitialDataRead +{ ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; [self verifyDataSource:ds]; } /// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file -- (void)testRecordedThrashCase { +- (void)testRecordedThrashCase +{ NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL]; @@ -72,8 +75,6 @@ - (void)testRecordedThrashCase { } ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:_update.oldData]; - // why is ds.collectionView.test_enableSuperUpdateCallLogging available on table view but now collection view>? -// ds.collectionView.test_enableSuperUpdateCallLogging = YES; [self applyUpdateUsingBatchUpdates:_update toDataSource:ds animated:NO @@ -81,7 +82,8 @@ - (void)testRecordedThrashCase { [self verifyDataSource:ds]; } -- (void)testThrashingWildly { +- (void)testThrashingWildly +{ for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; @autoreleasepool { @@ -102,7 +104,35 @@ - (void)testThrashingWildly { } } -- (void)testThrashingWildlyDispatchWildly { +- (void)testThrashingWildlyOnSameCollectionView +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"last test ran"]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:nil]; + for (NSInteger i = 0; i < 1000; i++) { + [self setUp]; + @autoreleasepool { + NSArray *sections = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + _update = [[ASThrashUpdate alloc] initWithData:sections]; + [ds setData:sections]; + [ds.collectionView reloadData]; + + [self applyUpdateUsingBatchUpdates:_update + toDataSource:ds + animated:NO + useXCTestWait:NO]; + [self verifyDataSource:ds]; + if (i == 999) { + [expectation fulfill]; + } + } + + [self tearDown]; + } + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)testThrashingWildlyDispatchWildly +{ XCTestExpectation *expectation = [self expectationWithDescription:@"last test ran"]; for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; diff --git a/Tests/ASThrashUtility.h b/Tests/ASThrashUtility.h index 653289740..bc80ff2d0 100644 --- a/Tests/ASThrashUtility.h +++ b/Tests/ASThrashUtility.h @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN #define kMinimumItemCount 5 #define kMinimumSectionCount 3 #define kFickleness 0.1 -#define kThrashingIterationCount 100 +#define kThrashingIterationCount 10 // Set to 1 to use UITableView and see if the issue still exists. #define USE_UIKIT_REFERENCE 0 @@ -69,7 +69,7 @@ static atomic_uint ASThrashTestItemNextID; @property (nonatomic) ASWeakSet *allNodes; - (instancetype)initTableViewDataSourceWithData:(NSArray *)data; -- (instancetype)initCollectionViewDataSourceWithData:(NSArray *)data; +- (instancetype)initCollectionViewDataSourceWithData:(NSArray * _Nullable)data; - (NSPredicate *)predicateForDeallocatedHierarchy; @end diff --git a/Tests/ASThrashUtility.m b/Tests/ASThrashUtility.m index 2a93eb5f9..42c2dc431 100644 --- a/Tests/ASThrashUtility.m +++ b/Tests/ASThrashUtility.m @@ -191,7 +191,7 @@ - (instancetype)initTableViewDataSourceWithData:(NSArray - (instancetype)initCollectionViewDataSourceWithData:(NSArray *)data { self = [super init]; if (self != nil) { - _data = [[NSArray alloc] initWithArray:data copyItems:YES]; + _data = data != nil ? [[NSArray alloc] initWithArray:data copyItems:YES] : [[NSArray alloc] init]; _window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; _collectionView = [[CollectionView alloc] initWithCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init]]; _allNodes = [[ASWeakSet alloc] init]; @@ -209,6 +209,10 @@ - (instancetype)initCollectionViewDataSourceWithData:(NSArray *)data { + _data = data; +} + - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.data[section].items.count; } From 03c2a7edf154a99f4a014bd85e3e4b603af51c7b Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Tue, 18 Dec 2018 13:49:23 -0800 Subject: [PATCH 10/12] Lint --- Tests/ASCollectionViewThrashTests.mm | 6 +- Tests/ASTableViewThrashTests.mm | 24 ++++-- Tests/ASThrashUtility.m | 117 ++++++++++++++++++--------- 3 files changed, 98 insertions(+), 49 deletions(-) diff --git a/Tests/ASCollectionViewThrashTests.mm b/Tests/ASCollectionViewThrashTests.mm index 8d31095b3..04897e10e 100644 --- a/Tests/ASCollectionViewThrashTests.mm +++ b/Tests/ASCollectionViewThrashTests.mm @@ -18,7 +18,8 @@ @interface ASCollectionViewThrashTests : XCTestCase @end -@implementation ASCollectionViewThrashTests { +@implementation ASCollectionViewThrashTests +{ // The current update, which will be logged in case of a failure. ASThrashUpdate *_update; BOOL _failed; @@ -163,7 +164,8 @@ - (void)testThrashingWildlyDispatchWildly - (void)applyUpdateUsingBatchUpdates:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource animated:(BOOL)animated - useXCTestWait:(BOOL)wait { + useXCTestWait:(BOOL)wait +{ CollectionView *collectionView = dataSource.collectionView; XCTestExpectation *expectation; diff --git a/Tests/ASTableViewThrashTests.mm b/Tests/ASTableViewThrashTests.mm index d5e9bb183..8d7064cd0 100644 --- a/Tests/ASTableViewThrashTests.mm +++ b/Tests/ASTableViewThrashTests.mm @@ -18,7 +18,8 @@ @interface ASTableViewThrashTests: XCTestCase @end -@implementation ASTableViewThrashTests { +@implementation ASTableViewThrashTests +{ // The current update, which will be logged in case of a failure. ASThrashUpdate *_update; BOOL _failed; @@ -26,7 +27,8 @@ @implementation ASTableViewThrashTests { #pragma mark Overrides -- (void)tearDown { +- (void)tearDown +{ if (_failed && _update != nil) { NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation); } @@ -35,7 +37,8 @@ - (void)tearDown { } // 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 { +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected +{ _failed = YES; [super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; } @@ -43,13 +46,15 @@ - (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *) #pragma mark Test Methods // Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. -- (void)testInitialDataRead { +- (void)testInitialDataRead +{ ASThrashDataSource *ds = [[ASThrashDataSource alloc] initTableViewDataSourceWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; [self verifyDataSource:ds]; } /// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file -- (void)testRecordedThrashCase { +- (void)testRecordedThrashCase +{ NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL]; @@ -65,7 +70,8 @@ - (void)testRecordedThrashCase { } // Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. -- (void)testThrashingWildly { +- (void)testThrashingWildly +{ for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; @autoreleasepool { @@ -85,7 +91,8 @@ - (void)testThrashingWildly { #pragma mark Helpers -- (void)applyUpdate:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource { +- (void)applyUpdate:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource +{ TableView *tableView = dataSource.tableView; [tableView beginUpdates]; @@ -122,7 +129,8 @@ - (void)applyUpdate:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *) } } -- (void)verifyDataSource:(ASThrashDataSource *)ds { +- (void)verifyDataSource:(ASThrashDataSource *)ds +{ TableView *tableView = ds.tableView; NSArray *data = [ds data]; XCTAssertEqual(data.count, tableView.numberOfSections); diff --git a/Tests/ASThrashUtility.m b/Tests/ASThrashUtility.m index 42c2dc431..c555d3f77 100644 --- a/Tests/ASThrashUtility.m +++ b/Tests/ASThrashUtility.m @@ -11,7 +11,8 @@ #import #import -static NSString *ASThrashArrayDescription(NSArray *array) { +static NSString *ASThrashArrayDescription(NSArray *array) +{ NSMutableString *str = [NSMutableString stringWithString:@"(\n"]; NSInteger i = 0; for (id obj in array) { @@ -24,11 +25,13 @@ @implementation ASThrashTestItem -+ (BOOL)supportsSecureCoding { ++ (BOOL)supportsSecureCoding +{ return YES; } -- (instancetype)init { +- (instancetype)init +{ self = [super init]; if (self != nil) { _itemID = atomic_fetch_add(&ASThrashTestItemNextID, 1); @@ -36,7 +39,8 @@ - (instancetype)init { return self; } -- (instancetype)initWithCoder:(NSCoder *)aDecoder { +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ self = [super init]; if (self != nil) { _itemID = [aDecoder decodeIntegerForKey:@"itemID"]; @@ -45,11 +49,13 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { return self; } -- (void)encodeWithCoder:(NSCoder *)aCoder { +- (void)encodeWithCoder:(NSCoder *)aCoder +{ [aCoder encodeInteger:_itemID forKey:@"itemID"]; } -+ (NSMutableArray *)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]]; @@ -57,11 +63,13 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { return result; } -- (CGFloat)rowHeight { +- (CGFloat)rowHeight +{ return (self.itemID % 400) ?: 44; } -- (NSString *)description { +- (NSString *)description +{ return [NSString stringWithFormat:@"", (unsigned long)_itemID]; } @@ -71,7 +79,8 @@ - (NSString *)description { @implementation ASThrashTestSection /// Create an array of sections with the given count -+ (NSMutableArray *)sectionsWithCount:(NSInteger)count { ++ (NSMutableArray *)sectionsWithCount:(NSInteger)count +{ NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; for (NSInteger i = 0; i < count; i += 1) { [result addObject:[[ASThrashTestSection alloc] initWithCount:kInitialItemCount]]; @@ -79,7 +88,8 @@ @implementation ASThrashTestSection return result; } -- (instancetype)initWithCount:(NSInteger)count { +- (instancetype)initWithCount:(NSInteger)count +{ self = [super init]; if (self != nil) { _sectionID = atomic_fetch_add(&ASThrashTestSectionNextID, 1); @@ -88,11 +98,13 @@ - (instancetype)initWithCount:(NSInteger)count { return self; } -- (instancetype)init { +- (instancetype)init +{ return [self initWithCount:0]; } -- (instancetype)initWithCoder:(NSCoder *)aDecoder { +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ self = [super init]; if (self != nil) { _items = [aDecoder decodeObjectOfClass:[NSArray class] forKey:@"items"]; @@ -102,31 +114,37 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { return self; } -+ (BOOL)supportsSecureCoding { ++ (BOOL)supportsSecureCoding +{ return YES; } -- (void)encodeWithCoder:(NSCoder *)aCoder { +- (void)encodeWithCoder:(NSCoder *)aCoder +{ [aCoder encodeObject:_items forKey:@"items"]; [aCoder encodeInteger:_sectionID forKey:@"sectionID"]; } -- (CGFloat)headerHeight { +- (CGFloat)headerHeight +{ return self.sectionID % 400 ?: 44; } -- (NSString *)description { +- (NSString *)description +{ return [NSString stringWithFormat:@"
", (unsigned long)_sectionID, (unsigned long)self.items.count, ASThrashArrayDescription(self.items)]; } -- (id)copyWithZone:(NSZone *)zone { +- (id)copyWithZone:(NSZone *)zone +{ ASThrashTestSection *copy = [[ASThrashTestSection alloc] init]; copy->_sectionID = _sectionID; copy->_items = [_items mutableCopy]; return copy; } -- (BOOL)isEqual:(id)object { +- (BOOL)isEqual:(id)object +{ if ([object isKindOfClass:[ASThrashTestSection class]]) { return [(ASThrashTestSection *)object sectionID] == _sectionID; } else { @@ -138,7 +156,8 @@ - (BOOL)isEqual:(id)object { @implementation NSIndexSet (ASThrashHelpers) -- (NSArray *)indexPathsInSection:(NSInteger)section { +- (NSArray *)indexPathsInSection:(NSInteger)section +{ NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; [self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { [result addObject:[NSIndexPath indexPathForItem:idx inSection:section]]; @@ -147,7 +166,8 @@ @implementation NSIndexSet (ASThrashHelpers) } /// `insertMode` means that for each index selected, the max goes up by one. -+ (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode { ++ (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++) { @@ -165,7 +185,8 @@ + (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)p @implementation ASThrashDataSource -- (instancetype)initTableViewDataSourceWithData:(NSArray *)data { +- (instancetype)initTableViewDataSourceWithData:(NSArray *)data +{ self = [super init]; if (self != nil) { _data = [[NSArray alloc] initWithArray:data copyItems:YES]; @@ -188,7 +209,8 @@ - (instancetype)initTableViewDataSourceWithData:(NSArray return self; } -- (instancetype)initCollectionViewDataSourceWithData:(NSArray *)data { +- (instancetype)initCollectionViewDataSourceWithData:(NSArray *)data +{ self = [super init]; if (self != nil) { _data = data != nil ? [[NSArray alloc] initWithArray:data copyItems:YES] : [[NSArray alloc] init]; @@ -209,29 +231,35 @@ - (instancetype)initCollectionViewDataSourceWithData:(NSArray *)data { +- (void)setData:(NSArray *)data +{ _data = data; } -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ return self.data[section].items.count; } -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ return self.data.count; } -- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ return self.data[section].headerHeight; } -- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section { +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ return self.data[section].items.count; } -- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode { +- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode +{ return self.data.count; } @@ -248,18 +276,21 @@ - (NSPredicate *)predicateForDeallocatedHierarchy #if USE_UIKIT_REFERENCE -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ return [tableView dequeueReusableCellWithIdentifier:kCellReuseID forIndexPath:indexPath]; } -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item]; return item.rowHeight; } #else -- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForItemAtIndexPath:(NSIndexPath *)indexPath { +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ ASThrashTestNode *node = [[ASThrashTestNode alloc] init]; node.item = self.data[indexPath.section].items[indexPath.row]; [self.allNodes addObject:node]; @@ -292,7 +323,8 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize @implementation ASThrashUpdate -- (instancetype)initWithData:(NSArray *)data { +- (instancetype)initWithData:(NSArray *)data +{ self = [super init]; if (self != nil) { _data = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; @@ -376,19 +408,23 @@ - (instancetype)initWithData:(NSArray *)data { return self; } -+ (BOOL)supportsSecureCoding { ++ (BOOL)supportsSecureCoding +{ return YES; } -+ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64 { ++ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64 +{ return [NSKeyedUnarchiver unarchiveObjectWithData:[[NSData alloc] initWithBase64EncodedString:base64 options:kNilOptions]]; } -- (NSString *)base64Representation { +- (NSString *)base64Representation +{ return [[NSKeyedArchiver archivedDataWithRootObject:self] base64EncodedStringWithOptions:kNilOptions]; } -- (void)encodeWithCoder:(NSCoder *)aCoder { +- (void)encodeWithCoder:(NSCoder *)aCoder +{ NSDictionary *dict = [self dictionaryWithValuesForKeys:@[ @"oldData", @"data", @@ -407,7 +443,8 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeInteger:ASThrashUpdateCurrentSerializationVersion forKey:@"_version"]; } -- (instancetype)initWithCoder:(NSCoder *)aDecoder { +- (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."); @@ -417,11 +454,13 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { return self; } -- (NSString *)description { +- (NSString *)description +{ return [NSString stringWithFormat:@"", self, ASThrashArrayDescription(_oldData), ASThrashArrayDescription(_deletedItemIndexes), _deletedSectionIndexes, ASThrashArrayDescription(_replacedItemIndexes), _replacedSectionIndexes, ASThrashArrayDescription(_insertedItemIndexes), _insertedSectionIndexes, ASThrashArrayDescription(_data)]; } -- (NSString *)logFriendlyBase64Representation { +- (NSString *)logFriendlyBase64Representation +{ return [NSString stringWithFormat:@"\n\n**********\nBase64 Representation:\n**********\n%@\n**********\nEnd Base64 Representation\n**********", self.base64Representation]; } From 5d3d6fc207c948cd360adf71fb2eb842c7048f62 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 18 Dec 2018 13:51:47 -0800 Subject: [PATCH 11/12] Update Tests/ASThrashUtility.h Co-Authored-By: mikezucc --- Tests/ASThrashUtility.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ASThrashUtility.h b/Tests/ASThrashUtility.h index bc80ff2d0..09e784775 100644 --- a/Tests/ASThrashUtility.h +++ b/Tests/ASThrashUtility.h @@ -1,5 +1,5 @@ // -// ASTableViewThrashTests.mm +// Tests/ASThrashUtility.h // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. From 51bca6490bd8e54c202eda157061f423689fb84f Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 2 Jan 2019 15:32:56 -0800 Subject: [PATCH 12/12] Tiny code style change in ASCollectionViewThrashTests.mm --- Tests/ASCollectionViewThrashTests.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/ASCollectionViewThrashTests.mm b/Tests/ASCollectionViewThrashTests.mm index 04897e10e..4650639d1 100644 --- a/Tests/ASCollectionViewThrashTests.mm +++ b/Tests/ASCollectionViewThrashTests.mm @@ -25,7 +25,8 @@ @implementation ASCollectionViewThrashTests BOOL _failed; } -- (void)tearDown { +- (void)tearDown +{ if (_failed && _update != nil) { NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation); }