[ASTableViewThrashTests] Initial commit

This commit is contained in:
Adlai Holler 2016-06-21 18:46:08 -07:00
parent 6dac29a16f
commit fcf2db79f8
3 changed files with 396 additions and 1 deletions

View File

@ -545,6 +545,7 @@
CC3B208C1C3F7A5400798563 /* ASWeakSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3B20881C3F7A5400798563 /* ASWeakSet.m */; };
CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */; };
CC3B20901C3F892D00798563 /* ASBridgedPropertiesTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */; };
CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */; };
CC7FD9DE1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; };
CC7FD9DF1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */; };
CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */; };
@ -935,6 +936,7 @@
CC3B20881C3F7A5400798563 /* ASWeakSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASWeakSet.m; sourceTree = "<group>"; };
CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASWeakSetTests.m; sourceTree = "<group>"; };
CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBridgedPropertiesTests.mm; sourceTree = "<group>"; };
CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewThrashTests.m; sourceTree = "<group>"; };
CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = "<group>"; };
CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequest.m; sourceTree = "<group>"; };
CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequestTests.m; sourceTree = "<group>"; };
@ -1201,6 +1203,7 @@
052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */,
058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */,
3C9C128419E616EF00E942A0 /* ASTableViewTests.m */,
CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */,
058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m */,
254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */,
254C6B531BF8FF2A003EC431 /* ASTextKitTests.mm */,
@ -2171,6 +2174,7 @@
2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m in Sources */,
058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.m in Sources */,
058D0A3A195D057000B7D73C /* ASDisplayNodeTests.m in Sources */,
CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */,
058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */,
056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.m in Sources */,
AC026B581BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m in Sources */,

View File

@ -41,7 +41,7 @@ static void ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(NSMutableArray
}
}
static void ASRecursivelyFindIndexPathsForMultidimensionalArray(NSObject *obj, NSIndexPath *curIndexPath, NSMutableArray *res)
static void ASRecursivelyFindIndexPathsForMultidimensionalArray(NSObject *obj, NSIndexPath *curIndexPath, NSMutableArray <NSIndexPath *>*res)
{
if (![obj isKindOfClass:[NSArray class]]) {
[res addObject:curIndexPath];

View File

@ -0,0 +1,391 @@
//
// ASTableViewThrashTests.m
// AsyncDisplayKit
//
// Created by Adlai Holler on 6/21/16.
// Copyright © 2016 Facebook. All rights reserved.
//
@import XCTest;
#import <AsyncDisplayKit/AsyncDisplayKit.h>
typedef NS_ENUM(NSUInteger, ASThrashChangeType) {
ASThrashReplaceItem,
ASThrashReplaceSection,
ASThrashDeleteItem,
ASThrashDeleteSection,
ASThrashInsertItem,
ASThrashInsertSection
};
#define USE_UIKIT_REFERENCE 1
#define kInitialSectionCount 6
#define kInitialItemCount 6
#if USE_UIKIT_REFERENCE
#define kCellReuseID @"ASThrashTestCellReuseID"
#endif
static NSString *ASThrashArrayDescription(NSArray *array) {
NSMutableString *str = [NSMutableString stringWithString:@"(\n"];
NSInteger i = 0;
for (id obj in array) {
[str appendFormat:@"\t[%ld]: \"%@\",\n", i, obj];
i += 1;
}
[str appendString:@")"];
return str;
}
@interface ASThrashTestItem: NSObject
#if USE_UIKIT_REFERENCE
/// This is used to identify the row with the table view (UIKit only).
@property (nonatomic, readonly) CGFloat rowHeight;
#endif
@end
@implementation ASThrashTestItem
- (instancetype)init {
self = [super init];
if (self != nil) {
#if USE_UIKIT_REFERENCE
_rowHeight = arc4random_uniform(500);
#endif
}
return self;
}
+ (NSArray <ASThrashTestItem *> *)itemsWithCount:(NSInteger)count {
NSMutableArray *result = [NSMutableArray arrayWithCapacity:count];
for (NSInteger i = 0; i < count; i += 1) {
[result addObject:[[ASThrashTestItem alloc] init]];
}
return result;
}
- (NSString *)description {
#if USE_UIKIT_REFERENCE
return [NSString stringWithFormat:@"<Item: rowHeight=%lu>", (unsigned long)self.rowHeight];
#else
return [NSString stringWithFormat:@"<Item: %p>", self];
#endif
}
@end
@interface ASThrashTestSection: NSObject
@property (nonatomic, strong, readonly) NSMutableArray *items;
/// This is used to identify the section with the table view.
@property (nonatomic, readonly) CGFloat headerHeight;
@end
@implementation ASThrashTestSection
- (instancetype)initWithCount:(NSInteger)count {
self = [super init];
if (self != nil) {
_items = [NSMutableArray arrayWithCapacity:count];
_headerHeight = arc4random_uniform(500) + 1;
for (NSInteger i = 0; i < count; i++) {
[_items addObject:[ASThrashTestItem new]];
}
}
return self;
}
- (instancetype)init {
return [self initWithCount:0];
}
+ (NSMutableArray <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;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<Section: headerHeight=%lu, itemCount=%lu>", (unsigned long)self.headerHeight, (unsigned long)self.items.count];
}
@end
#if !USE_UIKIT_REFERENCE
@interface ASThrashTestNode: ASCellNode
@property (nonatomic, strong) ASThrashTestItem *item;
@end
@implementation ASThrashTestNode
@end
#endif
@interface ASThrashDataSource: NSObject
#if USE_UIKIT_REFERENCE
<UITableViewDataSource, UITableViewDelegate>
#else
<ASTableDataSource, ASTableDelegate>
#endif
@property (nonatomic, strong, readonly) NSMutableArray <ASThrashTestSection *> *data;
@end
@implementation ASThrashDataSource
- (instancetype)init {
self = [super init];
if (self != nil) {
_data = [ASThrashTestSection sectionsWithCount:kInitialSectionCount];
}
return self;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.data[section].items.count;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.data.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return self.data[section].headerHeight;
}
#if USE_UIKIT_REFERENCE
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return [tableView dequeueReusableCellWithIdentifier:kCellReuseID forIndexPath:indexPath];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item];
return item.rowHeight;
}
#else
- (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath {
ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item];
return ^{
ASThrashTestNode *tableNode = [[ASThrashTestNode alloc] init];
tableNode.item = item;
return tableNode;
};
}
#endif
@end
@implementation NSIndexSet (ASThrashHelpers)
- (NSArray <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;
}
@end
@interface ASTableViewThrashTests: XCTestCase
@end
@implementation ASTableViewThrashTests {
CGRect screenBounds;
ASThrashDataSource *ds;
UIWindow *window;
#if USE_UIKIT_REFERENCE
UITableView *tableView;
#else
ASTableNode *tableNode;
ASTableView *tableView;
#endif
NSInteger minimumItemCount;
NSInteger minimumSectionCount;
float fickleness;
}
- (void)setUp {
minimumItemCount = 5;
minimumSectionCount = 3;
fickleness = 0.1;
window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
ds = [[ASThrashDataSource alloc] init];
#if USE_UIKIT_REFERENCE
tableView = [[UITableView alloc] initWithFrame:window.bounds style:UITableViewStyleGrouped];
[window addSubview:tableView];
tableView.dataSource = ds;
tableView.delegate = ds;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID];
[window layoutIfNeeded];
#else
tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped];
tableNode.frame = window.bounds;
[window addSubnode:tableNode];
tableNode.dataSource = ds;
tableNode.delegate = ds;
[tableView reloadDataImmediately];
#endif
}
- (void)testInitialDataRead {
[self verifyTableStateWithHierarchy];
}
- (void)testThrashingWildly {
for (NSInteger i = 0; i < 100; i++) {
[self _testThrashingWildly];
}
}
- (void)_testThrashingWildly {
NSLog(@"Old data: %@", ASThrashArrayDescription(ds.data));
NSMutableArray <NSMutableIndexSet *> *deletedItems = [NSMutableArray array];
NSMutableArray <NSMutableIndexSet *> *replacedItems = [NSMutableArray array];
NSMutableArray <NSMutableIndexSet *> *insertedItems = [NSMutableArray array];
NSInteger i = 0;
// Randomly reload some items
for (ASThrashTestSection *section in ds.data) {
NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO];
NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count];
[section.items replaceObjectsAtIndexes:indexes withObjects:newItems];
[replacedItems addObject:indexes];
i += 1;
}
// Randomly replace some sections
NSMutableIndexSet *replacedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO];
NSArray *replacingSections = [ASThrashTestSection sectionsWithCount:replacedSections.count];
[ds.data replaceObjectsAtIndexes:replacedSections withObjects:replacingSections];
// Randomly delete some items
i = 0;
for (ASThrashTestSection *section in ds.data) {
if (section.items.count >= minimumItemCount) {
NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO];
/// Cannot reload & delete the same item.
[indexes removeIndexes:replacedItems[i]];
[section.items removeObjectsAtIndexes:indexes];
[deletedItems addObject:indexes];
} else {
[deletedItems addObject:[NSMutableIndexSet indexSet]];
}
i += 1;
}
// Randomly delete some sections
NSMutableIndexSet *deletedSections = nil;
if (ds.data.count >= minimumSectionCount) {
deletedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO];
// Cannot reload & delete the same section.
[deletedSections removeIndexes:replacedSections];
} else {
deletedSections = [NSMutableIndexSet indexSet];
}
[ds.data removeObjectsAtIndexes:deletedSections];
// Randomly insert some sections
NSMutableIndexSet *insertedSections = [self randomIndexesLessThan:(ds.data.count + 1) probability:fickleness insertMode:YES];
NSArray *newSections = [ASThrashTestSection sectionsWithCount:insertedSections.count];
[ds.data insertObjects:newSections atIndexes:insertedSections];
// Randomly insert some items
i = 0;
for (ASThrashTestSection *section in ds.data) {
NSMutableIndexSet *indexes = [self randomIndexesLessThan:(section.items.count + 1) probability:fickleness insertMode:YES];
NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count];
[section.items insertObjects:newItems atIndexes:indexes];
[insertedItems addObject:indexes];
i += 1;
}
NSLog(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data));
// TODO: Submit changes in random order, randomly chunked up
[tableView beginUpdates];
i = 0;
for (NSIndexSet *indexes in insertedItems) {
NSArray *indexPaths = [indexes indexPathsInSection:i];
NSLog(@"Requested to insert rows: %@", indexPaths);
[tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
i += 1;
}
[tableView insertSections:insertedSections withRowAnimation:UITableViewRowAnimationNone];
[tableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationNone];
i = 0;
for (NSIndexSet *indexes in deletedItems) {
NSArray *indexPaths = [indexes indexPathsInSection:i];
NSLog(@"Requested to delete rows: %@", indexPaths);
[tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
i += 1;
}
i = 0;
for (NSIndexSet *indexes in replacedItems) {
NSArray *indexPaths = [indexes indexPathsInSection:i];
NSLog(@"Requested to reload rows: %@", indexPaths);
[tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
i += 1;
}
[tableView endUpdates];
#if !USE_UIKIT_REFERENCE
[tableView waitUntilAllUpdatesAreCommitted];
#endif
[self verifyTableStateWithHierarchy];
}
/// `insertMode` means that for each index selected, the max goes up by one.
- (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode {
NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init];
u_int32_t cutoff = probability * 100;
for (NSInteger i = 0; i < max; i++) {
if (arc4random_uniform(100) < cutoff) {
[indexes addIndex:i];
if (insertMode) {
max += 1;
}
}
}
return indexes;
}
#pragma mark Helpers
- (void)verifyTableStateWithHierarchy {
NSArray <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];
#if USE_UIKIT_REFERENCE
XCTAssertEqual([tableView rectForRowAtIndexPath:indexPath].size.height, item.rowHeight);
#else
ASThrashTestNode *node = (ASThrashTestNode *)[tableView nodeForRowAtIndexPath:indexPath];
XCTAssertEqual(node.item, item);
#endif
}
}
}
@end