Swiftgram/AsyncDisplayKitTests/ASTableViewTests.m
Scott Goodson 57465c7fd3 Overhaul ASDataController and extensively test ASTableView.
This diff resolves all known consistency issues with ASTableView and ASCollectionView.
It includes significantly more aggressive thrash-testing in ASTableViewStressTest,
which now passes on a variety of device and simulator configurations.  It also updates
the unit tests run on every commit to ensure any regression is caught quickly.

A few of the salient changes in this diff:
- ASTableView now uses Rene's ASCollectionViewLayoutController, and actually uses a
UICollectionViewFlowLayout without any UICollectionView.  This resolves an issue where
ASFlowLayoutController was generating slightly out-of-bounds indicies when programmatically
scrolling past the end of the table content.  Because the custom implementation is likely
faster, I will revisit this later with profiling and possibly returning to the custom impl.
- There is now a second copy of the _nodes array maintained by ASDataController.  It shares
the same node instances, but this does add some overhead to manipulating the arrays. I've
filed a task to follow up with optimization, as there are several great opportunities to
make it faster.  However, I don't believe the overhead is a significant issue, and it does
guarantee correctness in even the toughest app usage scenarios.
- ASDataController no longer supports calling its delegate /before/ edit operations.  No
other class was relying on this behavior, and it would be unusual for an app developer to
use ASDataController directly.  However, it is possible that someone with a custom view
that integrates with ASDataController and ASRangeController could be affected by this.
- Further cleanup of organization, naming, additional comments, reduced code length
wherever possible.  Overall, significantly more accessible to a new reader.
2015-06-28 18:03:45 -07:00

189 lines
5.6 KiB
Objective-C

/* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <XCTest/XCTest.h>
#import "ASTableView.h"
#define NumberOfSections 10
#define NumberOfRowsPerSection 20
#define NumberOfReloadIterations 50
@interface ASTestTableView : ASTableView
@property (atomic, copy) void (^willDeallocBlock)(ASTableView *tableView);
@end
@implementation ASTestTableView
- (void)dealloc
{
if (_willDeallocBlock) {
_willDeallocBlock(self);
}
[super dealloc];
}
@end
@interface ASTableViewTestDelegate : NSObject <ASTableViewDataSource, ASTableViewDelegate>
@property (atomic, copy) void (^willDeallocBlock)(ASTableViewTestDelegate *delegate);
@end
@implementation ASTableViewTestDelegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 0;
}
- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath
{
return nil;
}
- (void)dealloc
{
if (_willDeallocBlock) {
_willDeallocBlock(self);
}
[super dealloc];
}
@end
@interface ASTableViewFilledDataSource : NSObject <ASTableViewDataSource, ASTableViewDelegate>
@end
@implementation ASTableViewFilledDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return NumberOfSections;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return NumberOfRowsPerSection;
}
- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath
{
ASTextCellNode *textCellNode = [ASTextCellNode new];
textCellNode.text = indexPath.description;
return textCellNode;
}
@end
@interface ASTableViewTests : XCTestCase
@end
@implementation ASTableViewTests
- (void)DISABLED_testTableViewDoesNotRetainItselfAndDelegate
{
ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
__block BOOL tableViewDidDealloc = NO;
tableView.willDeallocBlock = ^(ASTableView *v){
tableViewDidDealloc = YES;
};
ASTableViewTestDelegate *delegate = [[ASTableViewTestDelegate alloc] init];
__block BOOL delegateDidDealloc = NO;
delegate.willDeallocBlock = ^(ASTableViewTestDelegate *d){
delegateDidDealloc = YES;
};
tableView.asyncDataSource = delegate;
tableView.asyncDelegate = delegate;
[delegate release];
XCTAssertTrue(delegateDidDealloc, @"unexpected delegate lifetime:%@", delegate);
XCTAssertNoThrow([tableView release], @"unexpected exception when deallocating table view:%@", tableView);
XCTAssertTrue(tableViewDidDealloc, @"unexpected table view lifetime:%@", tableView);
}
- (NSIndexSet *)randomIndexSet
{
NSInteger randA = arc4random_uniform(NumberOfSections - 1);
NSInteger randB = arc4random_uniform(NumberOfSections - 1);
return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))];
}
- (NSArray *)randomIndexPathsExisting:(BOOL)existing
{
NSMutableArray *indexPaths = [NSMutableArray array];
[[self randomIndexSet] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSUInteger rowNum = NumberOfRowsPerSection;
NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx];
for (NSUInteger i = (existing ? 0 : rowNum); i < (existing ? rowNum : rowNum * 2); i++) {
// Maximize evility by sporadically skipping indicies 1/3rd of the time, but only if reloading existing rows
if (existing && arc4random_uniform(2) == 0) {
continue;
}
NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i];
[indexPaths addObject:indexPath];
}
}];
return indexPaths;
}
- (void)testReloadData
{
// Keep the viewport moderately sized so that new cells are loaded on scrolling
ASTableView *tableView = [[ASTableView alloc] initWithFrame:CGRectMake(0, 0, 100, 500)
style:UITableViewStylePlain
asyncDataFetching:YES];
ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new];
tableView.asyncDelegate = dataSource;
tableView.asyncDataSource = dataSource;
[tableView reloadData];
for (int i = 0; i < NumberOfReloadIterations; ++i) {
UITableViewRowAnimation rowAnimation = (arc4random_uniform(1) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone);
BOOL animatedScroll = (arc4random_uniform(1) == 0 ? YES : NO);
BOOL reloadRowsInsteadOfSections = (arc4random_uniform(1) == 0 ? YES : NO);
BOOL letRunloopProceed = (arc4random_uniform(1) == 0 ? YES : NO);
BOOL useBeginEndUpdates = (arc4random_uniform(2) == 0 ? YES : NO);
if (useBeginEndUpdates) {
[tableView beginUpdates];
}
if (reloadRowsInsteadOfSections) {
[tableView reloadRowsAtIndexPaths:[self randomIndexPathsExisting:YES] withRowAnimation:rowAnimation];
} else {
[tableView reloadSections:[self randomIndexSet] withRowAnimation:rowAnimation];
}
[tableView setContentOffset:CGPointMake(0, arc4random_uniform(tableView.contentSize.height - tableView.bounds.size.height)) animated:animatedScroll];
if (letRunloopProceed) {
// Run other stuff on the main queue for between 2ms and 1000ms.
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(1 / (1 + arc4random_uniform(500)))]];
}
if (useBeginEndUpdates) {
[tableView endUpdates];
}
}
}
@end