Skip to content

Commit 8e078ea

Browse files
committed
Leak tests around cancelation
1 parent c656f50 commit 8e078ea

File tree

6 files changed

+518
-164
lines changed

6 files changed

+518
-164
lines changed

BAPromise.xcodeproj/project.pbxproj

+248-42
Large diffs are not rendered by default.

BAPromiseTests/CancelTests.m

+39
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,45 @@ -(void)testCancelAsyncARC
142142
[self waitForExpectationsWithTimeout:0.5 handler:nil];
143143
}
144144

145+
-(void)testCancelTokenReject
146+
{
147+
XCTestExpectation *expectation = [self expectationWithDescription:@"Promise Resolution"];
148+
BAPromiseClient *promise = [[BAPromiseClient alloc] init];
149+
[promise cancelled:^{
150+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
151+
[expectation fulfill];
152+
});
153+
}];
154+
[promise rejectWithError:[NSError errorWithDomain:@"org.benski" code:0 userInfo:nil]];
155+
BACancelToken *token = [promise rejected:^(NSError *obj) {
156+
XCTFail(@"unepected rejection");
157+
}];
158+
[token cancel];
159+
[self waitForExpectationsWithTimeout:0.5 handler:nil];
160+
161+
}
162+
163+
-(void)testCancelAsyncARCReject
164+
{
165+
XCTestExpectation *expectation = [self expectationWithDescription:@"Promise Resolution"];
166+
BAPromiseClient *promise = [[BAPromiseClient alloc] init];
167+
[promise cancelled:^{
168+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
169+
[expectation fulfill];
170+
});
171+
}];
172+
dispatch_async(dispatch_get_main_queue(), ^{
173+
[promise rejectWithError:[NSError errorWithDomain:@"org.benski" code:0 userInfo:nil]];
174+
BACancelToken *token = [promise rejected:^(NSError *error) {
175+
XCTFail(@"unepected rejection");
176+
}];
177+
[token cancel];
178+
179+
});
180+
181+
[self waitForExpectationsWithTimeout:0.5 handler:nil];
182+
}
183+
145184
-(void)testCancelThen
146185
{
147186
XCTestExpectation *expectation1 = [self expectationWithDescription:@"Promise reached inside of then block"];

BAPromiseTests/ChainTests.m

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ -(void)testSimpleWhenFail
8484
{
8585
XCTestExpectation *expectation = [self expectationWithDescription:@"Promise Resolution"];
8686
BAPromiseClient *promise = [[BAPromiseClient alloc] init];
87-
BAPromise *anotherPromise = [BAPromiseClient rejectedPromise:nil];
87+
BAPromise *anotherPromise = [BAPromiseClient rejectedPromise:[NSError errorWithDomain:@"org.benski" code:0 userInfo:nil]];
8888

8989
[promise fulfillWithObject:anotherPromise];
9090
[promise rejected:^(id obj) {

Classes/BAPromise.m

+90-112
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ @interface BACancelToken ()
3030
@property (nonatomic, strong) dispatch_queue_t queue;
3131
@property (nonatomic) BAPromiseState promiseState;
3232
@property (atomic) BOOL cancelled;
33-
@property (nonatomic, strong) dispatch_block_t onCancel;
33+
@property (nonatomic, copy) dispatch_block_t onCancel;
3434
@end
3535

3636
@implementation BACancelToken
@@ -59,7 +59,7 @@ -(void)cancelled:(dispatch_block_t)onCancel
5959
if (self.cancelled) {
6060
wrappedCancelBlock();
6161
} else {
62-
_onCancel = wrappedCancelBlock;
62+
self.onCancel = wrappedCancelBlock;
6363
}
6464
}
6565
});
@@ -69,7 +69,6 @@ -(void)cancel
6969
{
7070
self.cancelled = YES;
7171
dispatch_async(self.queue, ^{
72-
7372
if (self.onCancel) {
7473
self.onCancel();
7574
self.onCancel=nil;
@@ -78,11 +77,42 @@ -(void)cancel
7877
}
7978
@end
8079

80+
@interface BAPromiseBlocks : NSObject
81+
@property (nonatomic, copy) BAPromiseOnFulfilledBlock done;
82+
@property (nonatomic, copy) BAPromiseOnFulfilledBlock observed;
83+
@property (nonatomic, copy) BAPromiseOnRejectedBlock rejected;
84+
@property (nonatomic, copy) BAPromiseFinallyBlock finally;
85+
@end
86+
87+
@implementation BAPromiseBlocks
88+
- (BOOL)shouldKeepPromise
89+
{
90+
return self.done != nil || self.finally != nil;
91+
}
92+
93+
- (void)callBlocksWithObject:(id)object
94+
{
95+
if ([object isKindOfClass:NSError.class]) {
96+
if (self.rejected) {
97+
self.rejected(object);
98+
}
99+
} else {
100+
if (self.done) {
101+
self.done(object);
102+
}
103+
if (self.observed) {
104+
self.observed(object);
105+
}
106+
}
107+
if (self.finally) {
108+
self.finally();
109+
}
110+
}
111+
@end
112+
113+
81114
@interface BAPromise ()
82-
@property (nonatomic, strong) NSMutableArray *doneBlocks;
83-
@property (nonatomic, strong) NSMutableArray *observerBlocks;
84-
@property (nonatomic, strong) NSMutableArray *rejectedBlocks;
85-
@property (nonatomic, strong) NSMutableArray *finallyBlocks;
115+
@property (nonatomic, strong) NSMutableArray<BAPromiseBlocks *> *blocks;
86116
@property (nonatomic, strong) id fulfilledObject;
87117
@end
88118

@@ -118,15 +148,14 @@ -(BACancelToken *)done:(BAPromiseOnFulfilledBlock)onFulfilled
118148
BAPromiseOnRejectedBlock wrappedRejectedBlock;
119149
BAPromiseFinallyBlock wrappedFinallyBlock;
120150

121-
BACancelToken *cancellationToken;
122-
123-
cancellationToken = [BACancelToken new];
151+
BACancelToken *cancellationToken = [BACancelToken new];
124152

153+
__weak typeof(self) weakSelf = self;
125154
// wrap the passed in blocks to dispatch to the appropriate queue and check for cancellaltion
126155
if (onFulfilled) {
127156
wrappedDoneBlock = ^(id obj) {
128157
if (thread) {
129-
[self performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
158+
[weakSelf performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
130159
if (!cancellationToken.cancelled) {
131160
onFulfilled(obj);
132161
}
@@ -144,7 +173,7 @@ -(BACancelToken *)done:(BAPromiseOnFulfilledBlock)onFulfilled
144173
if (onObserved) {
145174
wrappedObservedBlock = ^(id obj) {
146175
if (thread) {
147-
[thread performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
176+
[weakSelf performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
148177
if (!cancellationToken.cancelled) {
149178
onObserved(obj);
150179
}
@@ -162,7 +191,7 @@ -(BACancelToken *)done:(BAPromiseOnFulfilledBlock)onFulfilled
162191
if (onRejected) {
163192
wrappedRejectedBlock = ^(id obj) {
164193
if (thread) {
165-
[thread performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
194+
[weakSelf performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
166195
if (!cancellationToken.cancelled) {
167196
onRejected(obj);
168197
}
@@ -180,7 +209,7 @@ -(BACancelToken *)done:(BAPromiseOnFulfilledBlock)onFulfilled
180209
if (onFinally) {
181210
wrappedFinallyBlock = ^{
182211
if (thread) {
183-
[thread performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
212+
[weakSelf performSelector:@selector(ba_runBlock:) onThread:thread withObject:^{
184213
if (!cancellationToken.cancelled) {
185214
onFinally();
186215
}
@@ -194,91 +223,49 @@ -(BACancelToken *)done:(BAPromiseOnFulfilledBlock)onFulfilled
194223
}
195224
};
196225
}
226+
BAPromiseBlocks *blocks = BAPromiseBlocks.new;
227+
blocks.done = wrappedDoneBlock;
228+
blocks.observed = wrappedObservedBlock;
229+
blocks.rejected = wrappedRejectedBlock;
230+
blocks.finally = wrappedFinallyBlock;
197231

198232
[cancellationToken cancelled:^{
199-
dispatch_async(self.queue, ^{
200-
if (onFulfilled) {
201-
[self.doneBlocks removeObjectIdenticalTo:wrappedDoneBlock];
202-
}
203-
204-
if (onObserved) {
205-
[self.observerBlocks removeObjectIdenticalTo:wrappedObservedBlock];
206-
}
207-
208-
if (onRejected) {
209-
[self.rejectedBlocks removeObjectIdenticalTo:wrappedRejectedBlock];
210-
}
211-
212-
if (onFinally) {
213-
[self.finallyBlocks removeObjectIdenticalTo:wrappedFinallyBlock];
214-
}
215-
216-
if (self.doneBlocks.count == 0
217-
&& self.finallyBlocks.count == 0) {
218-
[self cancel];
219-
}
220-
});
233+
typeof(self) strongSelf = weakSelf;
234+
if (strongSelf) {
235+
dispatch_async(strongSelf.queue, ^{
236+
@autoreleasepool {
237+
[strongSelf.blocks removeObjectIdenticalTo:blocks];
238+
239+
BOOL strongCount = NO;
240+
for (BAPromiseBlocks *block in strongSelf.blocks) {
241+
if ([block shouldKeepPromise]) {
242+
strongCount = YES;
243+
break;
244+
}
245+
}
246+
if (!strongCount) {
247+
[strongSelf cancel];
248+
}
249+
}
250+
});
251+
}
221252
}];
222253

223254

224255
dispatch_async(self.queue, ^{
225256
switch(self.promiseState) {
226257
case BAPromise_Unfulfilled:
227-
// save the blocks for later
228-
if (wrappedDoneBlock) {
229-
if (!_doneBlocks) {
230-
_doneBlocks = [[NSMutableArray alloc] init];
231-
}
232-
[_doneBlocks addObject:wrappedDoneBlock];
233-
}
234-
235-
if (wrappedObservedBlock) {
236-
if (!_observerBlocks) {
237-
_observerBlocks = [[NSMutableArray alloc] init];
238-
}
239-
[_observerBlocks addObject:wrappedObservedBlock];
240-
}
241-
242-
if (wrappedRejectedBlock) {
243-
if (!_rejectedBlocks) {
244-
_rejectedBlocks = [[NSMutableArray alloc] init];
245-
}
246-
[_rejectedBlocks addObject:wrappedRejectedBlock];
247-
}
248-
249-
if (wrappedFinallyBlock) {
250-
if (!_finallyBlocks) {
251-
_finallyBlocks = [[NSMutableArray alloc] init];
252-
}
253-
[_finallyBlocks addObject:wrappedFinallyBlock];
258+
// save the blocks for later
259+
if (!self.blocks) {
260+
self.blocks = NSMutableArray.new;
254261
}
262+
[self.blocks addObject:blocks];
255263
break;
256264

257265
case BAPromise_Fulfilled:
258-
if (wrappedDoneBlock) {
259-
// it was already fulfilled then call it now
260-
wrappedDoneBlock(_fulfilledObject);
261-
}
262-
263-
if (wrappedObservedBlock) {
264-
wrappedObservedBlock(_fulfilledObject);
265-
}
266-
267-
if (wrappedFinallyBlock) {
268-
wrappedFinallyBlock();
269-
}
270-
break;
271-
272266
case BAPromise_Rejected:
273267
case BAPromise_Canceled:
274-
// if it was already rejected, but no failureBlock was set, then call it now
275-
if (wrappedRejectedBlock) {
276-
wrappedRejectedBlock(_fulfilledObject);
277-
}
278-
279-
if (wrappedFinallyBlock) {
280-
wrappedFinallyBlock();
281-
}
268+
[blocks callBlocksWithObject:self.fulfilledObject];
282269
break;
283270
}
284271
});
@@ -504,23 +491,11 @@ -(void)fulfillWithObject:(id)obj
504491
self.promiseState = BAPromise_Fulfilled;
505492
self.fulfilledObject = obj;
506493

507-
// remove references we'll never call now
508-
self.rejectedBlocks = nil;
509-
510-
for (BAPromiseOnFulfilledBlock done in self.doneBlocks) {
511-
done(obj);
512-
}
513-
self.doneBlocks = nil;
514-
515-
for (BAPromiseOnFulfilledBlock done in self.observerBlocks) {
516-
done(obj);
517-
}
518-
self.observerBlocks = nil;
519-
520-
for (BAPromiseFinallyBlock finally in self.finallyBlocks) {
521-
finally();
494+
for (BAPromiseBlocks *blocks in self.blocks) {
495+
[blocks callBlocksWithObject:obj];
522496
}
523-
self.finallyBlocks = nil;
497+
// remove references we'll never call now
498+
self.blocks = nil;
524499
}
525500
});
526501
}
@@ -537,18 +512,11 @@ -(void)rejectWithError:(NSError *)error
537512
if (self.promiseState == BAPromise_Unfulfilled) {
538513
self.promiseState = BAPromise_Rejected;
539514
self.fulfilledObject = error;
540-
// remove references we'll never call now
541-
self.doneBlocks = nil;
542-
self.onCancel = nil;
543-
for (BAPromiseOnRejectedBlock rejected in self.rejectedBlocks) {
544-
rejected(error);
545-
}
546-
self.rejectedBlocks = nil;
547-
548-
for (BAPromiseFinallyBlock finally in self.finallyBlocks) {
549-
finally();
515+
for (BAPromiseBlocks *blocks in self.blocks) {
516+
[blocks callBlocksWithObject:error];
550517
}
551-
self.finallyBlocks = nil;
518+
// remove references we'll never call now
519+
self.blocks = nil;
552520
}
553521
});
554522
}
@@ -557,6 +525,16 @@ -(void)reject
557525
{
558526
[self rejectWithError:[[NSError alloc] init]];
559527
}
528+
529+
530+
-(void)cancel
531+
{
532+
[super cancel];
533+
dispatch_async(self.queue, ^{
534+
[self rejectWithError:[NSError errorWithDomain:@"org.benski.cancelled" code:0 userInfo:nil]];
535+
});
536+
}
537+
560538
@end
561539

562540
@implementation NSArray (BAPromiseJoin)

0 commit comments

Comments
 (0)