Start a thrash test suite for the collection node (#1246)

* Split up table view thrash tests

* Fix license

* Fix license

* Reenable thrash tests for table view

* Creating the collection view thrash tests

* Batch update animated tests

* Thrash wildly dispatch main

* Reset the thrash count

* One more test

* Lint

* Update Tests/ASThrashUtility.h

Co-Authored-By: mikezucc <mikezuccarino@gmail.com>

* Tiny code style change in ASCollectionViewThrashTests.mm
This commit is contained in:
Michael Zuccarino
2019-01-02 16:34:41 -08:00
committed by Huy Nguyen
parent 17e56042d3
commit 22acb985cb
5 changed files with 826 additions and 462 deletions

View File

@@ -190,6 +190,8 @@
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 */; };
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, ); }; };
@@ -759,6 +761,9 @@
92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMapNode.h; sourceTree = "<group>"; };
92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMapNode.mm; sourceTree = "<group>"; };
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 = "<group>"; };
9644CFDF2193777C00213478 /* ASThrashUtility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASThrashUtility.m; sourceTree = "<group>"; };
9692B4FE219E12370060C2C3 /* ASCollectionViewThrashTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionViewThrashTests.mm; sourceTree = "<group>"; };
9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackLayoutElement.h; sourceTree = "<group>"; };
9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAsciiArtBoxCreator.h; sourceTree = "<group>"; };
9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASAsciiArtBoxCreator.mm; sourceTree = "<group>"; };
@@ -1284,6 +1289,7 @@
058D09C5195D04C000B7D73C /* Tests */ = {
isa = PBXGroup;
children = (
9692B4FE219E12370060C2C3 /* ASCollectionViewThrashTests.mm */,
F325E48F217460B000AC93A4 /* ASTextNode2Tests.mm */,
F325E48B21745F9E00AC93A4 /* ASButtonNodeTests.mm */,
F3F698D1211CAD4600800CB1 /* ASDisplayViewAccessibilityTests.mm */,
@@ -1355,6 +1361,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 +2295,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 */,
@@ -2305,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 */,

View File

@@ -0,0 +1,217 @@
//
// ASCollectionViewThrashTests.mm
// Texture
//
// 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 <XCTest/XCTest.h>
#import <AsyncDisplayKit/AsyncDisplayKit.h>
#import <AsyncDisplayKit/ASCollectionView.h>
#import <stdatomic.h>
#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 <ASThrashTestSection *> *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
- (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
{
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];
[self applyUpdateUsingBatchUpdates:_update
toDataSource:ds
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)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];
@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:100 handler:nil];
}
#pragma mark Helpers
- (void)applyUpdateUsingBatchUpdates:(ASThrashUpdate *)update
toDataSource:(ASThrashDataSource *)dataSource animated:(BOOL)animated
useXCTestWait:(BOOL)wait
{
CollectionView *collectionView = dataSource.collectionView;
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.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];
}];
};
@try {
[collectionView performBatchAnimated:animated
updates:updateBlock
completion:^(BOOL finished) {
[expectation fulfill];
}];
} @catch (NSException *exception) {
_failed = YES;
XCTFail("TEST FAILED");
@throw exception;
}
if (wait) {
[self waitForExpectationsWithTimeout:1 handler:nil];
}
}
@end

View File

@@ -13,461 +13,13 @@
#import <AsyncDisplayKit/ASTableView+Undeprecated.h>
#import <stdatomic.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
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 <NSSecureCoding>
@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 <ASThrashTestItem *> *)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:@"<Item %lu>", (unsigned long)_itemID];
}
@end
@interface ASThrashTestSection: NSObject <NSCopying, NSSecureCoding>
@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 <ASThrashTestSection *> *)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:@"<Section %lu: itemCount=%lu, items=%@>", (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
<UITableViewDataSource, UITableViewDelegate>
#else
<ASTableDataSource, ASTableDelegate>
#endif
@property (nonatomic, readonly) UIWindow *window;
@property (nonatomic, readonly) TableView *tableView;
@property (nonatomic) NSArray <ASThrashTestSection *> *data;
// Only access on main
@property (nonatomic) ASWeakSet *allNodes;
@end
@implementation ASThrashDataSource
- (instancetype)initWithData:(NSArray <ASThrashTestSection *> *)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<NSString *,id> * _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 <NSIndexPath *> *)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 <NSSecureCoding>
@property (nonatomic, readonly) NSArray<ASThrashTestSection *> *oldData;
@property (nonatomic, readonly) NSMutableArray<ASThrashTestSection *> *data;
@property (nonatomic, readonly) NSMutableIndexSet *deletedSectionIndexes;
@property (nonatomic, readonly) NSMutableIndexSet *replacedSectionIndexes;
/// The sections used to replace the replaced sections.
@property (nonatomic, readonly) NSMutableArray<ASThrashTestSection *> *replacingSections;
@property (nonatomic, readonly) NSMutableIndexSet *insertedSectionIndexes;
@property (nonatomic, readonly) NSMutableArray<ASThrashTestSection *> *insertedSections;
@property (nonatomic, readonly) NSMutableArray<NSMutableIndexSet *> *deletedItemIndexes;
@property (nonatomic, readonly) NSMutableArray<NSMutableIndexSet *> *replacedItemIndexes;
/// The items used to replace the replaced items.
@property (nonatomic, readonly) NSMutableArray<NSArray <ASThrashTestItem *> *> *replacingItems;
@property (nonatomic, readonly) NSMutableArray<NSMutableIndexSet *> *insertedItemIndexes;
@property (nonatomic, readonly) NSMutableArray<NSArray <ASThrashTestItem *> *> *insertedItems;
- (instancetype)initWithData:(NSArray<ASThrashTestSection *> *)data;
+ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64;
- (NSString *)base64Representation;
@end
@implementation ASThrashUpdate
- (instancetype)initWithData:(NSArray<ASThrashTestSection *> *)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:@"<ASThrashUpdate %p:\nOld data: %@\nDeleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@>", 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
#import "ASThrashUtility.h"
@interface ASTableViewThrashTests: XCTestCase
@end
@implementation ASTableViewThrashTests {
@implementation ASTableViewThrashTests
{
// The current update, which will be logged in case of a failure.
ASThrashUpdate *_update;
BOOL _failed;
@@ -475,7 +27,8 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1;
#pragma mark Overrides
- (void)tearDown {
- (void)tearDown
{
if (_failed && _update != nil) {
NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation);
}
@@ -484,7 +37,8 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1;
}
// 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];
}
@@ -492,13 +46,15 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1;
#pragma mark Test Methods
// Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation.
- (void)DISABLED_testInitialDataRead {
ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]];
- (void)testInitialDataRead
{
ASThrashDataSource *ds = [[ASThrashDataSource alloc] initTableViewDataSourceWithData:[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];
@@ -507,20 +63,21 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1;
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];
}
// 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 {
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];
@@ -534,7 +91,8 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1;
#pragma mark Helpers
- (void)applyUpdate:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource {
- (void)applyUpdate:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource
{
TableView *tableView = dataSource.tableView;
[tableView beginUpdates];
@@ -571,14 +129,15 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1;
}
}
- (void)verifyDataSource:(ASThrashDataSource *)ds {
- (void)verifyDataSource:(ASThrashDataSource *)ds
{
TableView *tableView = ds.tableView;
NSArray <ASThrashTestSection *> *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];

111
Tests/ASThrashUtility.h Normal file
View File

@@ -0,0 +1,111 @@
//
// Tests/ASThrashUtility.h
// Texture
//
// 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 <AsyncDisplayKit/AsyncDisplayKit.h>
#import <stdatomic.h>
NS_ASSUME_NONNULL_BEGIN
#define kInitialSectionCount 10
#define kInitialItemCount 10
#define kMinimumItemCount 5
#define kMinimumSectionCount 3
#define kFickleness 0.1
#define kThrashingIterationCount 10
// 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 CollectionView UICollectionView
#define kCellReuseID @"ASThrashTestCellReuseID"
#else
#define TableView ASTableView
#define CollectionView ASCollectionNode
#endif
static NSInteger ASThrashUpdateCurrentSerializationVersion = 1;
@class ASThrashTestSection;
static atomic_uint ASThrashTestItemNextID;
@interface ASThrashTestItem: NSObject <NSSecureCoding>
@property (nonatomic, readonly) NSInteger itemID;
+ (NSMutableArray <ASThrashTestItem *> *)itemsWithCount:(NSInteger)count;
- (CGFloat)rowHeight;
@end
@interface ASThrashTestSection: NSObject <NSCopying, NSSecureCoding>
@property (nonatomic, readonly) NSMutableArray *items;
@property (nonatomic, readonly) NSInteger sectionID;
+ (NSMutableArray <ASThrashTestSection *> *)sectionsWithCount:(NSInteger)count;
- (instancetype)initWithCount:(NSInteger)count;
- (CGFloat)headerHeight;
@end
@interface ASThrashDataSource: NSObject
#if USE_UIKIT_REFERENCE
<UITableViewDataSource, UITableViewDelegate, UICollectionViewDataSource, UICollectionViewDelegate>
#else
<ASTableDataSource, ASTableDelegate, ASCollectionDelegate, ASCollectionDataSource>
#endif
@property (nonatomic, readonly) UIWindow *window;
@property (nonatomic, readonly) TableView *tableView;
@property (nonatomic, readonly) CollectionView *collectionView;
@property (nonatomic) NSArray <ASThrashTestSection *> *data;
// Only access on main
@property (nonatomic) ASWeakSet *allNodes;
- (instancetype)initTableViewDataSourceWithData:(NSArray <ASThrashTestSection *> *)data;
- (instancetype)initCollectionViewDataSourceWithData:(NSArray <ASThrashTestSection *> * _Nullable)data;
- (NSPredicate *)predicateForDeallocatedHierarchy;
@end
@interface NSIndexSet (ASThrashHelpers)
- (NSArray <NSIndexPath *> *)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 <NSSecureCoding>
@property (nonatomic, readonly) NSArray<ASThrashTestSection *> *oldData;
@property (nonatomic, readonly) NSMutableArray<ASThrashTestSection *> *data;
@property (nonatomic, readonly) NSMutableIndexSet *deletedSectionIndexes;
@property (nonatomic, readonly) NSMutableIndexSet *replacedSectionIndexes;
/// The sections used to replace the replaced sections.
@property (nonatomic, readonly) NSMutableArray<ASThrashTestSection *> *replacingSections;
@property (nonatomic, readonly) NSMutableIndexSet *insertedSectionIndexes;
@property (nonatomic, readonly) NSMutableArray<ASThrashTestSection *> *insertedSections;
@property (nonatomic, readonly) NSMutableArray<NSMutableIndexSet *> *deletedItemIndexes;
@property (nonatomic, readonly) NSMutableArray<NSMutableIndexSet *> *replacedItemIndexes;
/// The items used to replace the replaced items.
@property (nonatomic, readonly) NSMutableArray<NSArray <ASThrashTestItem *> *> *replacingItems;
@property (nonatomic, readonly) NSMutableArray<NSMutableIndexSet *> *insertedItemIndexes;
@property (nonatomic, readonly) NSMutableArray<NSArray <ASThrashTestItem *> *> *insertedItems;
- (instancetype)initWithData:(NSArray<ASThrashTestSection *> *)data;
+ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64;
- (NSString *)base64Representation;
- (NSString *)logFriendlyBase64Representation;
@end
NS_ASSUME_NONNULL_END

467
Tests/ASThrashUtility.m Normal file
View File

@@ -0,0 +1,467 @@
//
// ASTableViewThrashTests.mm
// Texture
//
// 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 "ASThrashUtility.h"
#import <AsyncDisplayKit/ASTableViewInternal.h>
#import <AsyncDisplayKit/ASTableView+Undeprecated.h>
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 <ASThrashTestItem *> *)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:@"<Item %lu>", (unsigned long)_itemID];
}
@end
static atomic_uint ASThrashTestSectionNextID = 1;
@implementation ASThrashTestSection
/// Create an array of sections with the given count
+ (NSMutableArray <ASThrashTestSection *> *)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:@"<Section %lu: itemCount=%lu, items=%@>", (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 <NSIndexPath *> *)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)initTableViewDataSourceWithData:(NSArray <ASThrashTestSection *> *)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;
}
- (instancetype)initCollectionViewDataSourceWithData:(NSArray <ASThrashTestSection *> *)data
{
self = [super init];
if (self != nil) {
_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];
[_window addSubview:_tableView];
_collectionView.delegate = self;
_collectionView.dataSource = self;
#if USE_UIKIT_REFERENCE
[_collectionView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID];
#else
[_collectionView reloadData];
[_collectionView waitUntilAllUpdatesAreProcessed];
#endif
[_collectionView layoutIfNeeded];
}
return self;
}
- (void)setData:(NSArray<ASThrashTestSection *> *)data
{
_data = data;
}
- (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;
}
- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section
{
return self.data[section].items.count;
}
- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode
{
return self.data.count;
}
/// 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<NSString *,id> * _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 *)collectionNode:(ASCollectionNode *)collectionNode nodeForItemAtIndexPath:(NSIndexPath *)indexPath
{
ASThrashTestNode *node = [[ASThrashTestNode alloc] init];
node.item = self.data[indexPath.section].items[indexPath.row];
[self.allNodes addObject:node];
return node;
}
- (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<ASThrashTestSection *> *)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:@"<ASThrashUpdate %p:\nOld data: %@\nDeleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@>", 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