1

I am trying to write a unit test for a method which itself creates and passes a block to another object (so it can be called later). This method is using socket.io-objc to send a request to a server. It passes a callback block to socketio's sendEvent:withData:andAcknowledge that will be invoked when it receives a response from the server). Here is the method i want to test:

typedef void(^MyResponseCallback)(NSDictionary * response);

...

-(void) sendCommand:(NSDictionary*)dict withCallback:(MyResponseCallback)callback andTimeoutAfter:(NSTimeInterval)delay
{
    __block BOOL didTimeout = FALSE;
    void (^timeoutBlock)() = nil;

    // create the block to invoke if request times out
    if (delay > 0)
    {
        timeoutBlock = ^
        {
            // set the flag so if we do get a response we can suppress it
            didTimeout = TRUE;

            // invoke original callback with no response argument (to indicate timeout)
            callback(nil);
        };
    }

    // create a callback/ack to be invoked when we get a response
    SocketIOCallback cb = ^(id argsData)
    {
        // if the callback was invoked after the UI timed out, ignore the response. otherwise, invoke
        // original callback
        if (!didTimeout)
        {
            if (timeoutBlock != nil)
            {
                // cancel the timeout timer
                [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onRequestTimeout:) object:timeoutBlock];
            }

            // invoke original callback
            NSDictionary * responseDict = argsData;
            callback(responseDict);
        }
    };

    // send the event to the server
    [_socketIO sendEvent:@"message" withData:dict andAcknowledge:cb];

    if (timeoutBlock != nil)
    {
        // if a timeout value was specified, set up timeout now
        [self performSelector:@selector(onRequestTimeout:) withObject:timeoutBlock afterDelay:delay];
    }
}

-(void) onRequestTimeout:(id)arg
{
    if (nil != arg)
    {
        // arg is a block, execute it
        void (^callback)() = (void (^)())arg;
        callback();
    }
}

This all appears to be working fine when running for real. My problem comes in when I run my unit test (which uses SenTestingKit and OCMock):

-(void)testSendRequestWithNoTimeout
{
    NSDictionary * cmd = [[NSDictionary alloc] initWithObjectsAndKeys:
                          @"TheCommand", @"msg", nil];
    __block BOOL callbackInvoked = FALSE;
    __block SocketIOCallback socketIoCallback = nil;
    MyResponseCallback requestCallback = ^(NSDictionary * response)
    {
        STAssertNotNil(response, @"Response dictionary is invalid");
        callbackInvoked = TRUE;
    };

    // expect controller to emit the message
    [[[_mockSocket expect] andDo:^(NSInvocation * invocation) {
        SocketIOCallback temp;
        [invocation getArgument:&temp atIndex:4];

        // THIS ISN'T WORKING AS I'D EXPECT
        socketIoCallback = [temp copy];

        STAssertNotNil(socketIoCallback, @"No callback was passed to socket.io sendEvent method");
    }] sendEvent:@"message" withData:cmd andAcknowledge:OCMOCK_ANY];

    // send command to dio
    [_ioController sendCommand:cmd withCallback:requestCallback];

    // make sure callback not invoked yet
    STAssertFalse(callbackInvoked, @"Response callback was invoked before receiving a response");

    // fake a response coming back
    socketIoCallback([[NSDictionary alloc] initWithObjectsAndKeys:@"msg", @"response", nil]);

    // make sure the original callback was invoked as a result
    STAssertTrue(callbackInvoked, @"Original requester did not get callback when msg recvd");
}

To simulate a response, I need to capture (and retain) the block that is created by the method i'm testing and is passed to sendEvent. By passing an 'andDo' argument to my expectation, I am able to access the block i'm looking for. However, it seems like it is not being retained. So, when sendEvent unwinds and I go to invoke the callback, all the values that should have been captured in the block show up as null. The result is that the test crashes when I invoke socketIoCallback and it goes to access the 'callback' value that was originally captured as part of the block (and is now nil).

I am using ARC and so I expect that "__block SocketIOCallback socketIoCallback" will retain values. I've tried to "-copy" the block into this variable but still it does not seem to retain past the end of sendCommand. What can I do to force this block to retain long enough for me to simulate a response?

Note: I've tried calling [invocation retainArguments] which works but then crashes somewhere in objc_release when cleaning up after the test is complete.

aeternusrahl
  • 52
  • 1
  • 6
  • Welcome to the lovely world of blocks! Would you like our premium deluxe package, which now includes 50% more crashes, absolutely free? – Richard J. Ross III May 02 '13 at 19:42
  • But seriously now, `retainArguments` will cause crashes if not all arguments are objects. Can you show us the signature of the method you are intercepting? – Richard J. Ross III May 02 '13 at 19:44
  • I am intercepting the sendEvent method defined as: `- (void) sendEvent:(NSString *)eventName withData:(id)data andAcknowledge:(SocketIOCallback)function;` – aeternusrahl May 02 '13 at 20:23
  • Very strange. I haven't used OCMock before, but in all probability it's just simply being released before the method is called. What happens if you copy the callback block *before* you pass it to the method? – Richard J. Ross III May 02 '13 at 20:25
  • If you add a `NSLog(@"%@",temp);` just after extracting the argument do you get a null value? – aLevelOfIndirection May 02 '13 at 20:28
  • @aLevelOfIndirection: temp is printed as `TEMP IS: <__NSMallocBlock__: 0x82c82c0>`. If i invoke the callback immediately after `getArgument` the block executes as i would expect. It seems to 'go bad' only when the stack unwinds. However, since that is the sequence it would follow in normal circumstances, i want to mimic that for the purposes of testing. – aeternusrahl May 02 '13 at 20:33
  • I was able to achieve my goal by changing the way I capture the callback argument from the OCMock expectation. It seems to work when I use replace the previous expectation call with: `[[_mockSocket expect] sendEvent:@"message" withData:cmd andAcknowledge:[OCMArg checkWithBlock:^BOOL(id value) { socketIoCallback = value; return value != nil; }]];` I'm not entirely sure why this works except that I suspect it is related to the use of NSInvocation. – aeternusrahl May 02 '13 at 21:26

2 Answers2

2

I was finally able to reproduce the problem and I suspect that the error is in this code section:

SocketIOCallback temp;
[invocation getArgument:&temp atIndex:4];

Extracting the block this way does not work correctly. I'm not exactly sure why but it may have to do with some of the magic ARC does in the background. If you change the code to the following it should work as you'd expect:

void *pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = (__brigde SocketIOCallback)pointer;
aLevelOfIndirection
  • 3,522
  • 14
  • 18
1

@aLevelOfIndirection is correct in that it is due to call to getArgument:atIndex:.

To understand why, remember that SocketIOCallback is an object pointer type (it is a typedef for a block pointer type), which is implicitly __strong in ARC. So &temp is type SocketIOCallback __strong *, i.e. it is a "pointer to strong". When you pass a "pointer to strong" to a function, the contract is that if the function replaces the value pointed to by the pointer (which is __strong), it must 1) release the existing value, and 2) retain the new value.

However, NSInvocation's getArgument:atIndex: does not know anything about the type of the thing pointed to. It takes a void * parameter, and simply copies the desired value binary-wise into the location pointed to by the pointer. So in simple terms, it does a pre-ARC non-retained assignment into temp. It does not retain the value assigned.

However, since temp is a strong reference, ARC will assume it was retained, and release it at the end of its scope. This is an over-release, and thus will crash.

Ideally, the compiler should disallow conversions between "pointer to strong" and void *, so this doesn't accidentally happen.

The solution is to not pass a "pointer to strong" to getArgument:atIndex:. You can pass either a void * as aLevelOfIndirection showed:

void *pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = (__brigde SocketIOCallback)pointer;

or a "pointer to unretained":

SocketIOCallback __unsafe_unretained pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = pointer;

and then assign back into a strong reference afterwards. (or in the latter case, you could use the unretained reference directly if you are careful)

newacct
  • 119,665
  • 29
  • 163
  • 224