You should decouple your need for the test to wait for GUI updates to run from how the main code path runs. In the first code block you posted, dispatch_sync
is almost certainly the wrong approach (vs. dispatch_async
) because you're going to block a background thread waiting on the main thread for no reason (there's no code after the dispatch_sync
) this can lead to thread starvation (in deployment that is). I'm guessing that you made it dispatch_sync
in an attempt to use the queue itself to interlock the two parallel tasks. If you are really committed to using that somewhat sub-optimal approach, you could do something like this:
- (void)testOne
{
SOAltUpdateView* view = [[SOAltUpdateView alloc] initWithFrame: NSMakeRect(0, 0, 100, 100)];
STAssertNotNil(view, @"View was nil");
STAssertEqualObjects(view.color, [NSColor redColor] , @"Initial color was wrong");
dispatch_queue_t q = dispatch_queue_create("test", 0);
dispatch_group_t group = dispatch_group_create();
view.queue = q;
// Run the operation
[view update];
// An operation we can wait on
dispatch_group_async(group, q, ^{ });
while (dispatch_group_wait(group, DISPATCH_TIME_NOW))
{
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES);
}
STAssertEqualObjects(view.color, [NSColor greenColor] , @"Updated color was wrong");
view.queue = nil;
[view release];
dispatch_release(group);
dispatch_release(q);
}
That was the approach that seemed closest to what you already had, but I came up with something that might be a little better/cleaner: A semaphore can do this interlocking for you, and with a little effort, you can make the intrusion on your actual GUI code pretty minimal. (Note: it will be effectively impossible to have no intrusion at all, because in order for two parallel tasks to interlock, they have to share something to interlock on -- something shared -- in your existing code it was the queue, here I'm using a semaphore.) Consider this contrived example: I've added a generic means for the test harness to push in a semaphore that can be used to notify it when the background operation completes. The "intrusion" on the code to be tested is limited to two macros.
NSObject+AsyncGUITestSupport.h:
@interface NSObject (AsyncGUITestSupport)
@property (nonatomic, readwrite, assign) dispatch_semaphore_t testCompletionSemaphore;
@end
#define OPERATION_BEGIN(...) do { dispatch_semaphore_t s = self.testCompletionSemaphore; if (s) dispatch_semaphore_wait(s, DISPATCH_TIME_NOW); } while(0)
#define OPERATION_END(...) do { dispatch_semaphore_t s = self.testCompletionSemaphore; if (s) dispatch_semaphore_signal(s); } while(0)
NSObject+AsyncGUITestSupport.m:
#import "NSObject+AsyncGUITestSupport.h"
#import <objc/runtime.h>
@implementation NSObject (AsyncGUITestSupport)
static void * const kTestingSemaphoreAssociatedStorageKey = (void*)&kTestingSemaphoreAssociatedStorageKey;
- (void)setTestCompletionSemaphore:(dispatch_semaphore_t)myProperty
{
objc_setAssociatedObject(self, kTestingSemaphoreAssociatedStorageKey, myProperty, OBJC_ASSOCIATION_ASSIGN);
}
- (dispatch_semaphore_t)testCompletionSemaphore
{
return objc_getAssociatedObject(self, kTestingSemaphoreAssociatedStorageKey);
}
@end
SOUpdateView.h
@interface SOUpdateView : NSView
@property (nonatomic, readonly, retain) NSColor* color;
- (void)update;
@end
SOUpdateView.m
#import "SOUpdateView.h"
#import "NSObject+AsyncGUITestSupport.h"
@implementation SOUpdateView
{
NSUInteger _count;
}
- (NSColor *)color
{
NSArray* colors = @[ [NSColor redColor], [NSColor greenColor], [NSColor blueColor] ];
@synchronized(self)
{
return colors[_count % colors.count];
}
}
- (void)drawRect:(NSRect)dirtyRect
{
[self.color set];
NSRectFill(dirtyRect);
}
- (void)update
{
OPERATION_BEGIN();
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(self)
{
_count++;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self setNeedsDisplay: YES];
OPERATION_END();
});
});
}
@end
And then the test harness:
#import "TestSOTestGUI.h"
#import "SOUpdateView.h"
#import "NSObject+AsyncGUITestSupport.h"
@implementation TestSOTestGUI
- (void)testOne
{
SOUpdateView* view = [[SOUpdateView alloc] initWithFrame: NSMakeRect(0, 0, 100, 100)];
STAssertNotNil(view, @"View was nil");
STAssertEqualObjects(view.color, [NSColor redColor] , @"Initial color was wrong");
// Push in a semaphore...
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
view.testCompletionSemaphore = sem;
// Run the operation
[view update];
// Wait for the operation to finish.
while (dispatch_semaphore_wait(sem, DISPATCH_TIME_NOW))
{
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES);
}
// Clear out the semaphore
view.testCompletionSemaphore = nil;
STAssertEqualObjects(view.color, [NSColor greenColor] , @"Updated color was wrong");
}
@end
Hope this helps.