Pregunta

I'm trying to write unit tests for some gui components that use grand central dispatch. I'd like to call threaded code from the test, wait for it to finish, and then check the results on the gui object.

dispatch_queue_t myQueue = dispatch_queue_create();

- (void)refreshGui {
    [self.button setEnabled:NO];
    dispatch_async(myQueue, ^{
        //operation of undetermined length
        sleep(1); 

        dispatch_sync(dispatch_get_main_queue(), ^{
            // GUI stuff that must be on the main thread,
            // I want this to be done before I check results in my tests.
            [self.button setEnabled:YES];
        });
    });
}

In my tests, I want to do something like this:

-(void)testRefreshGui {
    [object refreshGui];
    [object blockUntilThreadedOperationIsDone];
    STAssertTrue([object isRefreshedProperly], @"did not refresh");
}

My first idea was to call something synchronously on the relevant queue, like this. Unfortunately, this results in deadlock when called from the main queue (because there is a dispatch_sync() to the main queue in the gui code, and the test is also running on the main thread):

-(void)blockOnQueue:(dispatch_queue_t)q {
    dispatch_sync(q, ^{});
}

Using a dispatch group with dispatch_group_wait(group, DISPATCH_TIME_FOREVER) also results in deadlock for the same reason.

A hack solution I came up with was this:

- (void)waitOnQueue:(dispatch_queue_t)q {
    __block BOOL blocking = YES;
    while (blocking) {
        [NSRunLoop.mainRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
            dispatch_sync(q, ^{});
            blocking = NO;
        });
    }
}

Unfortunately, this 'solution' has the problem of pumping the main run loop and causing other tests to run, which breaks a number of things for me.

I also do not want to change the GUI code's dispatch_sync() to dispatch_async() because that's not the right behavior for this queue, and in the tests, the GUI code wouldn't be guaranteed to complete before the test checks the result either.

Thanks for any ideas!

¿Fue útil?

Solución

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top