Created
December 31, 2012 16:07
-
-
Save nevyn/4420931 to your computer and use it in GitHub Desktop.
Code to accompany "Methods of Concurrency" on my blog nevyn.tumblr.com.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
@interface SPInvocationGrabber : NSObject | |
-(id)initWithObject:(id)obj; | |
-(id)initWithObject:(id)obj stacktraceSaving:(BOOL)saveStack; | |
@property (readonly, retain, nonatomic) id object; | |
@property (readonly, retain, nonatomic) NSInvocation *invocation; | |
@property (nonatomic, copy) void(^afterForwardInvocation)(); | |
@property BOOL backgroundAfterForward; | |
@property BOOL onMainAfterForward; | |
@property BOOL waitUntilDone; | |
-(void)invoke; // will release object and invocation | |
-(void)printBacktrace; | |
-(void)saveBacktrace; | |
@end | |
@interface NSObject (SPInvocationGrabbing) | |
-(id)grab; | |
-(id)grabWithoutStacktrace; | |
-(id)invokeAfter:(NSTimeInterval)delta; | |
-(id)nextRunloop; | |
-(id)inBackground; | |
-(id)onMainAsync:(BOOL)async; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "NSObject+SPInvocationGrabbing.h" | |
#import <execinfo.h> | |
#pragma mark Invocation grabbing | |
@interface SPInvocationGrabber () { | |
int frameCount; | |
char **frameStrings; | |
BOOL backgroundAfterForward; | |
BOOL onMainAfterForward; | |
BOOL waitUntilDone; | |
} | |
@property (readwrite, retain, nonatomic) id object; | |
@property (readwrite, retain, nonatomic) NSInvocation *invocation; | |
@end | |
@implementation SPInvocationGrabber | |
- (id)initWithObject:(id)obj; | |
{ | |
return [self initWithObject:obj stacktraceSaving:YES]; | |
} | |
-(id)initWithObject:(id)obj stacktraceSaving:(BOOL)saveStack; | |
{ | |
self.object = obj; | |
if(saveStack) | |
[self saveBacktrace]; | |
return self; | |
} | |
-(void)dealloc; | |
{ | |
free(frameStrings); | |
} | |
@synthesize backgroundAfterForward, onMainAfterForward, waitUntilDone; | |
- (void)runInBackground; | |
{ | |
@autoreleasepool { | |
[self invoke]; | |
} | |
} | |
- (void)forwardInvocation:(NSInvocation *)anInvocation { | |
[anInvocation retainArguments]; | |
anInvocation.target = _object; | |
self.invocation = anInvocation; | |
if(backgroundAfterForward) | |
[NSThread detachNewThreadSelector:@selector(runInBackground) toTarget:self withObject:nil]; | |
else if(onMainAfterForward) | |
[self performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:waitUntilDone]; | |
else if(_afterForwardInvocation) | |
_afterForwardInvocation(); | |
} | |
- (NSMethodSignature *)methodSignatureForSelector:(SEL)inSelector { | |
NSMethodSignature *signature = [super methodSignatureForSelector:inSelector]; | |
if (signature == NULL) | |
signature = [_object methodSignatureForSelector:inSelector]; | |
return signature; | |
} | |
- (void)invoke; | |
{ | |
@try { | |
[_invocation invoke]; | |
} | |
@catch (NSException * e) { | |
NSLog(@"SPInvocationGrabber's target raised %@:\n\t%@\nInvocation was originally scheduled at:", e.name, e); | |
[self printBacktrace]; | |
printf("\n"); | |
[e raise]; | |
} | |
self.invocation = nil; | |
self.object = nil; | |
self.afterForwardInvocation = nil; | |
} | |
-(void)saveBacktrace; | |
{ | |
void *backtraceFrames[128]; | |
frameCount = backtrace(&backtraceFrames[0], 128); | |
frameStrings = backtrace_symbols(&backtraceFrames[0], frameCount); | |
} | |
-(void)printBacktrace; | |
{ | |
for(int x = 3; x < frameCount; x++) { | |
if(frameStrings[x] == NULL) { break; } | |
printf("%s\n", frameStrings[x]); | |
} | |
} | |
@end | |
@implementation NSObject (SPInvocationGrabbing) | |
-(id)grab; | |
{ | |
return [[SPInvocationGrabber alloc] initWithObject:self]; | |
} | |
-(id)grabWithoutStacktrace | |
{ | |
return [[SPInvocationGrabber alloc] initWithObject:self stacktraceSaving:NO]; | |
} | |
-(id)invokeAfter:(NSTimeInterval)delta; | |
{ | |
id grabber = [self grab]; | |
[NSTimer scheduledTimerWithTimeInterval:delta target:grabber selector:@selector(invoke) userInfo:nil repeats:NO]; | |
return grabber; | |
} | |
- (id)nextRunloop; | |
{ | |
return [self invokeAfter:0]; | |
} | |
-(id)inBackground; | |
{ | |
SPInvocationGrabber *grabber = [self grab]; | |
grabber.backgroundAfterForward = YES; | |
return grabber; | |
} | |
-(id)onMainAsync:(BOOL)async; | |
{ | |
SPInvocationGrabber *grabber = [self grab]; | |
grabber.onMainAfterForward = YES; | |
grabber.waitUntilDone = !async; | |
return grabber; | |
} | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
/** | |
Experimental multithreading primitive: An object conforming to SPAgent is not thread safe, | |
but it runs in its own thread. To perform any of the methods on it, you must first dispatch to its | |
workQueue. | |
*/ | |
@protocol SPAgent <NSObject> | |
@property(nonatomic,readonly) dispatch_queue_t workQueue; | |
@end | |
/// Returns invocation grabber; resulting invocation will be performed on workQueue. Proxied invocation returns an SPTask. | |
@interface NSObject (SPAgentDo) | |
- (instancetype)sp_agentAsync; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Moriarty/SPAgent.h> | |
#import "NSObject+SPInvocationGrabbing.h" | |
#import "SPTask.h" | |
@implementation NSObject (SPAgentDo) | |
- (instancetype)sp_agentAsync | |
{ | |
SPInvocationGrabber *grabber = [self grabWithoutStacktrace]; | |
__weak SPInvocationGrabber *weakGrabber = grabber; | |
SPTaskCompletionSource *completionSource = [SPTaskCompletionSource new]; | |
SPTask *task = completionSource.task; | |
__block void *unsafeTask = (__bridge void *)(task); | |
grabber.afterForwardInvocation = ^{ | |
NSInvocation *invocation = [weakGrabber invocation]; | |
// Let the caller get the result of the invocation as a task. | |
// Block guarantees lifetime of 'task', so just bridge it here. | |
BOOL hasObjectReturn = strcmp([invocation.methodSignature methodReturnType], @encode(id)) == 0; | |
if(hasObjectReturn) | |
[invocation setReturnValue:&unsafeTask]; | |
dispatch_async([(id)self workQueue], ^{ | |
#pragma clang diagnostic push | |
#pragma clang diagnostic ignored "-Warc-retain-cycles" | |
// "invoke" will break the cycle, and the block must hold on to grabber | |
// until this moment so that it survives (nothing else is holding the grabber) | |
[grabber invoke]; | |
#pragma clang diagnostic pop | |
if(hasObjectReturn) { | |
__unsafe_unretained id result = nil; | |
[invocation getReturnValue:&result]; | |
[completionSource completeWithValue:result]; | |
} | |
}); | |
}; | |
return grabber; | |
} | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SPAgentTest.h | |
// Moriarty | |
// | |
// Created by Joachim Bengtsson on 2012-12-26. | |
// | |
// | |
#import <SenTestingKit/SenTestingKit.h> | |
@interface SPAgentTest : SenTestCase | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SPAgentTest.m | |
// Moriarty | |
// | |
// Created by Joachim Bengtsson on 2012-12-26. | |
// | |
// | |
#import "SPAgentTest.h" | |
#import <Moriarty/SPAgent.h> | |
#import "SPTask.h" | |
@interface TestAgent : NSObject <SPAgent> | |
- (id)leet; | |
@end | |
@implementation SPAgentTest | |
- (void)testAgentAsyncTask | |
{ | |
TestAgent *agent = [TestAgent new]; | |
SPTask *leetTask = [[agent sp_agentAsync] leet]; | |
__block BOOL gotLeet = NO; | |
[leetTask addCallback:^(id value) { | |
STAssertEqualObjects(value, @(1337), @"Got an unexpected value"); | |
gotLeet = YES; | |
} on:dispatch_get_main_queue()]; | |
// Spin the runloop | |
while(!gotLeet) | |
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; | |
STAssertEquals(gotLeet, YES, @"Expected to have gotten leet by now"); | |
} | |
@end | |
@implementation TestAgent | |
{ | |
dispatch_queue_t _workQueue; | |
} | |
- (id)init | |
{ | |
if(!(self = [super init])) | |
return nil; | |
_workQueue = dispatch_queue_create("moriarty.testworkqueue", DISPATCH_QUEUE_SERIAL); | |
return self; | |
} | |
- (void)dealloc | |
{ | |
dispatch_release(_workQueue); | |
} | |
- (dispatch_queue_t)workQueue | |
{ | |
return _workQueue; | |
} | |
- (id)leet | |
{ | |
NSAssert(_workQueue == dispatch_get_current_queue(), @"Expected getter to be called on work queue"); | |
return @(1337); | |
} | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SPTask.h | |
// Moriarty | |
// | |
// Created by Joachim Bengtsson on 2012-12-26. | |
// | |
// | |
#import <Foundation/Foundation.h> | |
@class SPTask; | |
typedef void(^SPTaskCallback)(id value); | |
typedef void(^SPTaskErrback)(NSError *error); | |
typedef id(^SPTaskThenCallback)(id value); | |
typedef SPTask*(^SPTaskChainCallback)(id value); | |
/** @class SPTask | |
@abstract Any asynchronous operation that someone might want to know the result of. | |
*/ | |
@interface SPTask : NSObject | |
/** @method addCallback:on: | |
Add a callback to be called async when this task finishes, including the queue to | |
call it on. If the task has already finished, the callback will be called immediately | |
(but still asynchronously) | |
@return self, in case you want to add more call/errbacks on the same task */ | |
- (instancetype)addCallback:(SPTaskCallback)callback on:(dispatch_queue_t)queue; | |
/** @method addErrback:on: | |
Like callback, but for when the task fails | |
@return self, in case you want to add more call/errbacks on the same task */ | |
- (instancetype)addErrback:(SPTaskErrback)errback on:(dispatch_queue_t)queue; | |
/** @method then:on: | |
Add a callback, and return a task that represents the return value of that | |
callback. Useful for doing background work with the result of some other task. | |
This task will fail if the parent task fails, chaining them together. | |
@return A new task to be executed when 'self' completes, representing | |
the work in 'worker' | |
*/ | |
- (instancetype)then:(SPTaskThenCallback)worker on:(dispatch_queue_t)queue; | |
/** @method chain:on: | |
Add a callback that will be used to provide further work to be done. The | |
returned SPTask represents this work-to-be-provided. | |
@return A new task to be executed when 'self' completes, representing | |
the work provided by 'worker' | |
*/ | |
- (instancetype)chain:(SPTaskChainCallback)chainer on:(dispatch_queue_t)queue; | |
/** @method chain | |
@abstract Convenience for asynchronously waiting on a task that returns a task. | |
@discussion Equivalent to [task chain:^SPTask*(SPTask *task) { return task; } ...] | |
@example sp_agentAsync returns a task. When run on a method that returns a task, | |
you want to wait on the latter, rather than the former. Thus, you chain: | |
[[[[foo sp_agentAsync] fetchSomething] chain] addCallback:^(id something) {}]; | |
... to first convert `Task<Task<Thing>>` into `Task<Thing>` through chain, | |
then into `Thing` through addCallback. | |
*/ | |
- (instancetype)chain; | |
@end | |
/** @class SPTaskCompletionSource | |
Task factory for a single task that the caller knows how to complete/fail. | |
*/ | |
@interface SPTaskCompletionSource : NSObject | |
/** The task that this source can mark as completed. */ | |
- (SPTask*)task; | |
/** Signal successful completion of the task to all callbacks */ | |
- (void)completeWithValue:(id)value; | |
/** Signal failed completion of the task to all errbacks */ | |
- (void)failWithError:(NSError*)error; | |
@end | |
/** Convenience holder of a callback and the queue that the callback should be called on */ | |
@interface SPCallbackHolder : NSObject | |
- (id)initWithCallback:(SPTaskCallback)callback onQueue:(dispatch_queue_t)callbackQueue; | |
@property(nonatomic,assign) dispatch_queue_t callbackQueue; | |
@property(nonatomic,copy) SPTaskCallback callback; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SPTask.m | |
// Moriarty | |
// | |
// Created by Joachim Bengtsson on 2012-12-26. | |
// | |
// | |
#import "SPTask.h" | |
@interface SPTask () | |
{ | |
NSMutableArray *_callbacks; | |
NSMutableArray *_errbacks; | |
BOOL _isCompleted; | |
id _completedValue; | |
NSError *_completedError; | |
} | |
@end | |
@implementation SPTask | |
- (id)init | |
{ | |
if(!(self = [super init])) | |
return nil; | |
_callbacks = [NSMutableArray new]; | |
_errbacks = [NSMutableArray new]; | |
return self; | |
} | |
- (instancetype)addCallback:(SPTaskCallback)callback on:(dispatch_queue_t)queue | |
{ | |
@synchronized(_callbacks) { | |
if(_isCompleted) { | |
if(!_completedValue) { | |
dispatch_async(queue, ^{ | |
callback(_completedValue); | |
}); | |
} | |
} else { | |
[_callbacks addObject:[[SPCallbackHolder alloc] initWithCallback:callback onQueue:queue]]; | |
} | |
} | |
return self; | |
} | |
- (instancetype)addErrback:(SPTaskErrback)errback on:(dispatch_queue_t)queue | |
{ | |
@synchronized(_errbacks) { | |
if(_isCompleted) { | |
if(_completedError) { | |
dispatch_async(queue, ^{ | |
errback(_completedError); | |
}); | |
} | |
} else { | |
[_errbacks addObject:[[SPCallbackHolder alloc] initWithCallback:errback onQueue:queue]]; | |
} | |
} | |
return self; | |
} | |
- (instancetype)then:(SPTaskThenCallback)worker on:(dispatch_queue_t)queue | |
{ | |
SPTask *then = [SPTask new]; | |
[self addCallback:^(id value) { | |
id result = worker(value); | |
[then completeWithValue:result]; | |
} on:queue]; | |
[self addErrback:^(NSError *error) { | |
[then failWithError:error]; | |
} on:queue]; | |
return then; | |
} | |
- (instancetype)chain:(SPTaskChainCallback)chainer on:(dispatch_queue_t)queue | |
{ | |
SPTask *chain = [SPTask new]; | |
[self addCallback:^(id value) { | |
SPTask *workToBeProvided = chainer(value); | |
[workToBeProvided addCallback:^(id value) { | |
[chain completeWithValue:value]; | |
} on:queue]; | |
[workToBeProvided addErrback:^(NSError *error) { | |
[chain failWithError:error]; | |
} on:queue]; | |
} on:queue]; | |
[self addErrback:^(NSError *error) { | |
[chain failWithError:error]; | |
} on:queue]; | |
return chain; | |
} | |
- (instancetype)chain | |
{ | |
return [self chain:^SPTask *(id value) { | |
return value; | |
} on:dispatch_get_global_queue(0, 0)]; | |
} | |
- (void)completeWithValue:(id)value | |
{ | |
@synchronized(_callbacks) { | |
_isCompleted = YES; | |
_completedValue = value; | |
for(SPCallbackHolder *holder in _callbacks) { | |
dispatch_async(holder.callbackQueue, ^{ | |
holder.callback(value); | |
}); | |
} | |
[_callbacks removeAllObjects]; | |
[_errbacks removeAllObjects]; | |
} | |
} | |
- (void)failWithError:(NSError*)error | |
{ | |
@synchronized(_errbacks) { | |
_isCompleted = YES; | |
_completedError = error; | |
for(SPCallbackHolder *holder in _errbacks) { | |
dispatch_async(holder.callbackQueue, ^{ | |
holder.callback(error); | |
}); | |
} | |
[_callbacks removeAllObjects]; | |
[_errbacks removeAllObjects]; | |
} | |
} | |
@end | |
@implementation SPTaskCompletionSource | |
{ | |
SPTask *_task; | |
} | |
- (SPTask*)task | |
{ | |
if(!_task) | |
_task = [SPTask new]; | |
return _task; | |
} | |
- (void)completeWithValue:(id)value | |
{ | |
[self.task completeWithValue:value]; | |
} | |
- (void)failWithError:(NSError*)error | |
{ | |
[self.task failWithError:error]; | |
} | |
@end | |
@implementation SPCallbackHolder | |
- (id)initWithCallback:(SPTaskCallback)callback onQueue:(dispatch_queue_t)callbackQueue | |
{ | |
if(!(self = [super init])) | |
return nil; | |
self.callback = callback; | |
self.callbackQueue = callbackQueue; | |
return self; | |
} | |
- (void)dealloc | |
{ | |
self.callbackQueue = nil; | |
} | |
- (void)setCallbackQueue:(dispatch_queue_t)callbackQueue | |
{ | |
if(callbackQueue) | |
dispatch_retain(callbackQueue); | |
if(_callbackQueue) | |
dispatch_release(_callbackQueue); | |
_callbackQueue = callbackQueue; | |
} | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SPTaskTest.h | |
// Moriarty | |
// | |
// Created by Joachim Bengtsson on 2012-12-26. | |
// | |
// | |
#import <SenTestingKit/SenTestingKit.h> | |
@interface SPTaskTest : SenTestCase | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SPTaskTest.m | |
// Moriarty | |
// | |
// Created by Joachim Bengtsson on 2012-12-26. | |
// | |
// | |
#import "SPTaskTest.h" | |
#import "SPTask.h" | |
@implementation SPTaskTest | |
- (void)testCallback | |
{ | |
SPTaskCompletionSource *source = [SPTaskCompletionSource new]; | |
SPTask *task = source.task; | |
dispatch_queue_t callbackQueue = dispatch_get_main_queue(); | |
__block BOOL firstCallbackTriggered = NO; | |
__block BOOL secondCallbackTriggered = NO; | |
[task addCallback:^(id value) { | |
STAssertEqualObjects(value, @(1337), @"Unexpected value"); | |
STAssertEquals(firstCallbackTriggered, NO, @"Callback should only trigger once"); | |
firstCallbackTriggered = YES; | |
} on:callbackQueue]; | |
[task addErrback:^(id value) { | |
STAssertTrue(NO, @"Error should not have triggered"); | |
} on:callbackQueue]; | |
[task addCallback:^(id value) { | |
STAssertEqualObjects(value, @(1337), @"Unexpected value"); | |
STAssertEquals(firstCallbackTriggered, YES, @"First callback should have triggered before the second"); | |
secondCallbackTriggered = YES; | |
} on:callbackQueue]; | |
[source completeWithValue:@(1337)]; | |
// Spin the runloop | |
while(!secondCallbackTriggered) | |
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; | |
STAssertEquals(firstCallbackTriggered, YES, @"First callback should have triggered"); | |
STAssertEquals(secondCallbackTriggered, YES, @"Second callback should have triggered"); | |
} | |
- (void)testErrback | |
{ | |
SPTaskCompletionSource *source = [SPTaskCompletionSource new]; | |
SPTask *task = source.task; | |
dispatch_queue_t callbackQueue = dispatch_get_main_queue(); | |
__block BOOL firstErrbackTriggered = NO; | |
__block BOOL secondErrbackTriggered = NO; | |
[task addErrback:^(NSError *error) { | |
STAssertEquals(error.code, (NSInteger)1337, @"Unexpected error code"); | |
STAssertEquals(firstErrbackTriggered, NO, @"Errback should only trigger once"); | |
firstErrbackTriggered = YES; | |
} on:callbackQueue]; | |
[task addCallback:^(id value) { | |
STAssertTrue(NO, @"Callback should not have triggered"); | |
} on:callbackQueue]; | |
[task addErrback:^(NSError *error) { | |
STAssertEquals(error.code, (NSInteger)1337, @"Unexpected error code"); | |
STAssertEquals(firstErrbackTriggered, YES, @"First errback should have triggered before the second"); | |
secondErrbackTriggered = YES; | |
} on:callbackQueue]; | |
[source failWithError:[NSError errorWithDomain:@"test" code:1337 userInfo:nil]]; | |
// Spin the runloop | |
while(!secondErrbackTriggered) | |
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; | |
STAssertEquals(firstErrbackTriggered, YES, @"First errback should have triggered"); | |
STAssertEquals(secondErrbackTriggered, YES, @"Second errback should have triggered"); | |
} | |
- (void)testLateCallback | |
{ | |
SPTaskCompletionSource *source = [SPTaskCompletionSource new]; | |
SPTask *task = source.task; | |
dispatch_queue_t callbackQueue = dispatch_get_main_queue(); | |
__block BOOL firstCallbackTriggered = NO; | |
__block BOOL secondCallbackTriggered = NO; | |
[task addCallback:^(id value) { | |
STAssertEqualObjects(value, @(1337), @"Unexpected value"); | |
STAssertEquals(firstCallbackTriggered, NO, @"Callback should only trigger once"); | |
firstCallbackTriggered = YES; | |
} on:callbackQueue]; | |
[source completeWithValue:@(1337)]; | |
[task addCallback:^(id value) { | |
STAssertEqualObjects(value, @(1337), @"Unexpected value"); | |
secondCallbackTriggered = YES; | |
} on:callbackQueue]; | |
// Spin the runloop | |
while(!secondCallbackTriggered) | |
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; | |
STAssertEquals(firstCallbackTriggered, YES, @"First callback should have triggered"); | |
STAssertEquals(secondCallbackTriggered, YES, @"Second callback should have triggered"); | |
} | |
- (void)testLateErrback | |
{ | |
SPTaskCompletionSource *source = [SPTaskCompletionSource new]; | |
SPTask *task = source.task; | |
dispatch_queue_t callbackQueue = dispatch_get_main_queue(); | |
__block BOOL firstErrbackTriggered = NO; | |
__block BOOL secondErrbackTriggered = NO; | |
[task addErrback:^(NSError *error) { | |
STAssertEquals(error.code, (NSInteger)1337, @"Unexpected value"); | |
STAssertEquals(firstErrbackTriggered, NO, @"Callback should only trigger once"); | |
firstErrbackTriggered = YES; | |
} on:callbackQueue]; | |
[source failWithError:[NSError errorWithDomain:@"test" code:1337 userInfo:nil]]; | |
[task addErrback:^(NSError *error) { | |
STAssertEquals(error.code, (NSInteger)1337, @"Unexpected value"); | |
secondErrbackTriggered = YES; | |
} on:callbackQueue]; | |
// Spin the runloop | |
while(!secondErrbackTriggered) | |
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; | |
STAssertEquals(firstErrbackTriggered, YES, @"First callback should have triggered"); | |
STAssertEquals(secondErrbackTriggered, YES, @"Second callback should have triggered"); | |
} | |
- (void)testThen | |
{ | |
SPTaskCompletionSource *source = [SPTaskCompletionSource new]; | |
[source completeWithValue:@(10)]; | |
__block BOOL done = NO; | |
[[[source.task then:^id(id value) { | |
return @([value intValue]*20); | |
} on:dispatch_get_main_queue()] then:^id(id value) { | |
return @([value intValue]*30); | |
} on:dispatch_get_main_queue()] addCallback:^(id value) { | |
STAssertEqualObjects(value, @(6000), @"Chain didn't chain as expected"); | |
done = YES; | |
} on:dispatch_get_main_queue()]; | |
while(!done) | |
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment