Skip to content

Commit

Permalink
Leak tests around cancelation
Browse files Browse the repository at this point in the history
  • Loading branch information
benski committed Nov 1, 2016
1 parent c656f50 commit 54fb04a
Show file tree
Hide file tree
Showing 6 changed files with 509 additions and 164 deletions.
290 changes: 248 additions & 42 deletions BAPromise.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions BAPromiseTests/CancelTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,45 @@ -(void)testCancelAsyncARC
[self waitForExpectationsWithTimeout:0.5 handler:nil];
}

-(void)testCancelTokenReject
{
XCTestExpectation *expectation = [self expectationWithDescription:@"Promise Resolution"];
BAPromiseClient *promise = [[BAPromiseClient alloc] init];
[promise cancelled:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[expectation fulfill];
});
}];
[promise rejectWithError:[NSError errorWithDomain:@"org.benski" code:0 userInfo:nil]];
BACancelToken *token = [promise rejected:^(NSError *obj) {
XCTFail(@"unepected rejection");
}];
[token cancel];
[self waitForExpectationsWithTimeout:0.5 handler:nil];

}

-(void)testCancelAsyncARCReject
{
XCTestExpectation *expectation = [self expectationWithDescription:@"Promise Resolution"];
BAPromiseClient *promise = [[BAPromiseClient alloc] init];
[promise cancelled:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[expectation fulfill];
});
}];
dispatch_async(dispatch_get_main_queue(), ^{
[promise rejectWithError:[NSError errorWithDomain:@"org.benski" code:0 userInfo:nil]];
BACancelToken *token = [promise rejected:^(NSError *error) {
XCTFail(@"unepected rejection");
}];
[token cancel];

});

[self waitForExpectationsWithTimeout:0.5 handler:nil];
}

-(void)testCancelThen
{
XCTestExpectation *expectation1 = [self expectationWithDescription:@"Promise reached inside of then block"];
Expand Down
2 changes: 1 addition & 1 deletion BAPromiseTests/ChainTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ -(void)testSimpleWhenFail
{
XCTestExpectation *expectation = [self expectationWithDescription:@"Promise Resolution"];
BAPromiseClient *promise = [[BAPromiseClient alloc] init];
BAPromise *anotherPromise = [BAPromiseClient rejectedPromise:nil];
BAPromise *anotherPromise = [BAPromiseClient rejectedPromise:[NSError errorWithDomain:@"org.benski" code:0 userInfo:nil]];

[promise fulfillWithObject:anotherPromise];
[promise rejected:^(id obj) {
Expand Down
193 changes: 81 additions & 112 deletions Classes/BAPromise.m
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ @interface BACancelToken ()
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic) BAPromiseState promiseState;
@property (atomic) BOOL cancelled;
@property (nonatomic, strong) dispatch_block_t onCancel;
@property (nonatomic, copy) dispatch_block_t onCancel;
@end

@implementation BACancelToken
Expand Down Expand Up @@ -59,7 +59,7 @@ -(void)cancelled:(dispatch_block_t)onCancel
if (self.cancelled) {
wrappedCancelBlock();
} else {
_onCancel = wrappedCancelBlock;
self.onCancel = wrappedCancelBlock;
}
}
});
Expand All @@ -69,7 +69,6 @@ -(void)cancel
{
self.cancelled = YES;
dispatch_async(self.queue, ^{

if (self.onCancel) {
self.onCancel();
self.onCancel=nil;
Expand All @@ -78,11 +77,42 @@ -(void)cancel
}
@end

@interface BAPromiseBlocks : NSObject
@property (nonatomic, copy) BAPromiseOnFulfilledBlock done;
@property (nonatomic, copy) BAPromiseOnFulfilledBlock observed;
@property (nonatomic, copy) BAPromiseOnRejectedBlock rejected;
@property (nonatomic, copy) BAPromiseFinallyBlock finally;
@end

@implementation BAPromiseBlocks
- (BOOL)shouldKeepPromise
{
return self.done != nil || self.finally != nil;
}

- (void)callBlocksWithObject:(id)object
{
if ([object isKindOfClass:NSError.class]) {
if (self.rejected) {
self.rejected(object);
}
} else {
if (self.done) {
self.done(object);
}
if (self.observed) {
self.observed(object);
}
}
if (self.finally) {
self.finally();
}
}
@end


@interface BAPromise ()
@property (nonatomic, strong) NSMutableArray *doneBlocks;
@property (nonatomic, strong) NSMutableArray *observerBlocks;
@property (nonatomic, strong) NSMutableArray *rejectedBlocks;
@property (nonatomic, strong) NSMutableArray *finallyBlocks;
@property (nonatomic, strong) NSMutableArray<BAPromiseBlocks *> *blocks;
@property (nonatomic, strong) id fulfilledObject;
@end

Expand Down Expand Up @@ -118,15 +148,14 @@ -(BACancelToken *)done:(BAPromiseOnFulfilledBlock)onFulfilled
BAPromiseOnRejectedBlock wrappedRejectedBlock;
BAPromiseFinallyBlock wrappedFinallyBlock;

BACancelToken *cancellationToken;

cancellationToken = [BACancelToken new];
BACancelToken *cancellationToken = [BACancelToken new];

__weak typeof(self) weakSelf = self;
// wrap the passed in blocks to dispatch to the appropriate queue and check for cancellaltion
if (onFulfilled) {
wrappedDoneBlock = ^(id obj) {
if (thread) {
[self performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
[weakSelf performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
if (!cancellationToken.cancelled) {
onFulfilled(obj);
}
Expand All @@ -144,7 +173,7 @@ -(BACancelToken *)done:(BAPromiseOnFulfilledBlock)onFulfilled
if (onObserved) {
wrappedObservedBlock = ^(id obj) {
if (thread) {
[thread performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
[weakSelf performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
if (!cancellationToken.cancelled) {
onObserved(obj);
}
Expand All @@ -162,7 +191,7 @@ -(BACancelToken *)done:(BAPromiseOnFulfilledBlock)onFulfilled
if (onRejected) {
wrappedRejectedBlock = ^(id obj) {
if (thread) {
[thread performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
[weakSelf performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
if (!cancellationToken.cancelled) {
onRejected(obj);
}
Expand All @@ -180,7 +209,7 @@ -(BACancelToken *)done:(BAPromiseOnFulfilledBlock)onFulfilled
if (onFinally) {
wrappedFinallyBlock = ^{
if (thread) {
[thread performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
[weakSelf performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
if (!cancellationToken.cancelled) {
onFinally();
}
Expand All @@ -194,91 +223,49 @@ -(BACancelToken *)done:(BAPromiseOnFulfilledBlock)onFulfilled
}
};
}
BAPromiseBlocks *blocks = BAPromiseBlocks.new;
blocks.done = wrappedDoneBlock;
blocks.observed = wrappedObservedBlock;
blocks.rejected = wrappedRejectedBlock;
blocks.finally = wrappedFinallyBlock;

[cancellationToken cancelled:^{
dispatch_async(self.queue, ^{
if (onFulfilled) {
[self.doneBlocks removeObjectIdenticalTo:wrappedDoneBlock];
}

if (onObserved) {
[self.observerBlocks removeObjectIdenticalTo:wrappedObservedBlock];
}

if (onRejected) {
[self.rejectedBlocks removeObjectIdenticalTo:wrappedRejectedBlock];
}

if (onFinally) {
[self.finallyBlocks removeObjectIdenticalTo:wrappedFinallyBlock];
}

if (self.doneBlocks.count == 0
&& self.finallyBlocks.count == 0) {
[self cancel];
}
});
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
dispatch_async(strongSelf.queue, ^{
@autoreleasepool {
[strongSelf.blocks removeObjectIdenticalTo:blocks];

BOOL strongCount = NO;
for (BAPromiseBlocks *block in strongSelf.blocks) {
if ([block shouldKeepPromise]) {
strongCount = YES;
break;
}
}
if (!strongCount) {
[strongSelf cancel];
}
}
});
}
}];


dispatch_async(self.queue, ^{
switch(self.promiseState) {
case BAPromise_Unfulfilled:
// save the blocks for later
if (wrappedDoneBlock) {
if (!_doneBlocks) {
_doneBlocks = [[NSMutableArray alloc] init];
}
[_doneBlocks addObject:wrappedDoneBlock];
}

if (wrappedObservedBlock) {
if (!_observerBlocks) {
_observerBlocks = [[NSMutableArray alloc] init];
}
[_observerBlocks addObject:wrappedObservedBlock];
}

if (wrappedRejectedBlock) {
if (!_rejectedBlocks) {
_rejectedBlocks = [[NSMutableArray alloc] init];
}
[_rejectedBlocks addObject:wrappedRejectedBlock];
}

if (wrappedFinallyBlock) {
if (!_finallyBlocks) {
_finallyBlocks = [[NSMutableArray alloc] init];
}
[_finallyBlocks addObject:wrappedFinallyBlock];
// save the blocks for later
if (!self.blocks) {
self.blocks = NSMutableArray.new;
}
[self.blocks addObject:blocks];
break;

case BAPromise_Fulfilled:
if (wrappedDoneBlock) {
// it was already fulfilled then call it now
wrappedDoneBlock(_fulfilledObject);
}

if (wrappedObservedBlock) {
wrappedObservedBlock(_fulfilledObject);
}

if (wrappedFinallyBlock) {
wrappedFinallyBlock();
}
break;

case BAPromise_Rejected:
case BAPromise_Canceled:
// if it was already rejected, but no failureBlock was set, then call it now
if (wrappedRejectedBlock) {
wrappedRejectedBlock(_fulfilledObject);
}

if (wrappedFinallyBlock) {
wrappedFinallyBlock();
}
[blocks callBlocksWithObject:self.fulfilledObject];
break;
}
});
Expand Down Expand Up @@ -504,23 +491,11 @@ -(void)fulfillWithObject:(id)obj
self.promiseState = BAPromise_Fulfilled;
self.fulfilledObject = obj;

// remove references we'll never call now
self.rejectedBlocks = nil;

for (BAPromiseOnFulfilledBlock done in self.doneBlocks) {
done(obj);
}
self.doneBlocks = nil;

for (BAPromiseOnFulfilledBlock done in self.observerBlocks) {
done(obj);
for (BAPromiseBlocks *blocks in self.blocks) {
[blocks callBlocksWithObject:obj];
}
self.observerBlocks = nil;

for (BAPromiseFinallyBlock finally in self.finallyBlocks) {
finally();
}
self.finallyBlocks = nil;
// remove references we'll never call now
self.blocks = nil;
}
});
}
Expand All @@ -537,18 +512,11 @@ -(void)rejectWithError:(NSError *)error
if (self.promiseState == BAPromise_Unfulfilled) {
self.promiseState = BAPromise_Rejected;
self.fulfilledObject = error;
// remove references we'll never call now
self.doneBlocks = nil;
self.onCancel = nil;
for (BAPromiseOnRejectedBlock rejected in self.rejectedBlocks) {
rejected(error);
for (BAPromiseBlocks *blocks in self.blocks) {
[blocks callBlocksWithObject:error];
}
self.rejectedBlocks = nil;

for (BAPromiseFinallyBlock finally in self.finallyBlocks) {
finally();
}
self.finallyBlocks = nil;
// remove references we'll never call now
self.blocks = nil;
}
});
}
Expand All @@ -557,6 +525,7 @@ -(void)reject
{
[self rejectWithError:[[NSError alloc] init]];
}

@end

@implementation NSArray (BAPromiseJoin)
Expand Down
Loading

0 comments on commit 54fb04a

Please sign in to comment.