Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCMVerify causing EXC_BAD_ACESS #147

Closed
foulkesjohn opened this issue Sep 12, 2014 · 17 comments
Closed

OCMVerify causing EXC_BAD_ACESS #147

foulkesjohn opened this issue Sep 12, 2014 · 17 comments

Comments

@foulkesjohn
Copy link

I've been using OCMock 2 for many months along side Specta without issue. Recently I upgraded to 3 and began using the OCMVerify macro for verify-after-running. In certain instances I am receiving EXC_BAD_ACCESS when the invocation matcher is matching arguments, specifically if(([recordedArg isEqual:passedArg] == NO). It seems the the passedArg is released by ARC at some point and by the time it is pulled out of the invocation it is gone.

I'm not 100% where the issue lies and I can't seem to be able to replicate this is a sample project to provide as an example for this issue. It seems to happen more on iOS 64bit simulator than 32bit, but it isn't everytime.

The layout of my tests look something like this:

SpecBegin(Spec)

describe(@"Spec", ^{

    __block TestSubject *subject;
    __block id mockObject;

    beforeAll(^{
        mockObject = OCMClassMock([MyObject class]);
        subject = [[TestSubject alloc] initWithObject:mockObject];
    });

    describe(@"when something happens", ^{

        beforeAll(^{
            MyInvocation successfulInvocation = [ArrangeInvocation successfulInvocation];

            OCMStub([mockObject anotherMethod]);
            .andDo(successfulInvocation);

            [subject doSomething];
        });

        it(@"does something", ^{
            OCMVerify([mockObject methodCalledWithParam:[SomeParam param] 
                                      completionHandler:OCMOCK_ANY]);
        });

    });

});

SpecEnd

I have been able to work around this issue by moving the expectation to before the tests run but ideally would like to use the new features of OCMock 3. I'm just not sure where to start diagnosing this and as I said can't seem to replicate the issue outside of my main codebase.

@HiveHicks
Copy link

I'm having the same issue when using [OCMArg checkWithBlock:] in OCMVerify(). It doesn't throw though if I first use OCMExpect() and then OCMVerifyAll()

- (void)testThatDoesntThrow
{
    [self.mainContext performBlockAndWait:^{

        GRDocumentVariant *documentVariant = [GRDocumentVariant insertInManagedObjectContext:self.mainContext];

        id documentVC = [OCMArg checkWithBlock:^BOOL(id obj) {
            return [obj isKindOfClass:[GRDocumentVC class]] && [(GRDocumentVC *) obj documentVariant] == documentVariant;
        }];

        OCMExpect([_navigationController pushViewController:documentVC animated:YES]);

        [_mockedVC showDocumentVariant:documentVariant];

        OCMVerifyAll((id)_navigationController);

    }];
}

- (void)testThatThrows_EXC_BAD_ACCESS
{
    [self.mainContext performBlockAndWait:^{

        GRDocumentVariant *documentVariant = [GRDocumentVariant insertInManagedObjectContext:self.mainContext];
        [_mockedVC showDocumentVariant:documentVariant];

        id documentVC = [OCMArg checkWithBlock:^BOOL(id obj) {
            return [obj isKindOfClass:[GRDocumentVC class]] && [(GRDocumentVC *) obj documentVariant] == documentVariant;
        }];

        OCMVerify([_navigationController pushViewController:documentVC animated:YES]);

    }];
}

@leszarna
Copy link

I have similar problem in OCMock code line:
if(([recordedArg isEqual:passedArg] == NO) &&
for :
OCMVerify([_api getObjectWithId:@16]);
Method is stubbed before:
[OCMStub([_api getObjectWithId:[OCMArg any]]) andReturn:someObject];

OCMVerify with [OCMArg any] doesn't cause problem.

@albsala
Copy link

albsala commented Oct 31, 2014

+1
Using OCMExpect() and OCMVerifyAll()solved the problem, but I don't know why.

@seaburg
Copy link

seaburg commented Jan 1, 2015

Hi!
@leszarna, perhaps it not a bug
Try this:

OCMStub([_api getObjectWithId:@16]).andDo(^(NSInvocation *inv) {
    [inv retainArguments];

    [inv setReturnValue:&someObject];
});

<...>
OCMVerify([_api getObjectWithId:@16]);

@foulkesjohn, you can add to beforeAll:

OCMStub([mockObject methodCalledWithParam:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *inv) {
    [inv retainArguments];
});

@iosdev-republicofapps
Copy link

I am also seeing the same EXC_BAD_ACCESS when I use [OCMArg checkWithBlock:] in OCMVerify(). This happens even if I keep strong references around to the things used in the block.

Something is going on .... Not sure what. Maybe the NSInvocation in OCM should retain is arguments?

@erikdoe
Copy link
Owner

erikdoe commented Feb 17, 2015

Finally found time to look into this. It seems that despite appearances these are separate issues.

@foulkesjohn Unfortunately, I can't diagnose this from the code you provided. I can only speculate that there is a memory problem somewhere around the return value of [ArrangeInvocation successfulInvocation] but at a minimum I'd have to see the code for that. Also, I'm not a Specta user but it seems that the code wouldn't even compile, particularly the following two lines:

OCMStub([mockObject anotherMethod]);
.andDo(successfulInvocation);

What is this test trying to show? That stubbing one method but calling another makes the verification of the other method fail?

@HiveHicks Only realised now that you probably just made a simple mistake. As far as I am aware you have to add __block to variables that will be accessed from a block. You're just "lucky" that in the first test the omission doesn't cause a problem.

@iosdev-republicofapps OCMock already retains the arguments on the recorded exception in almost all cases. For complicated reasons it can't retain the arguments when the method has at least one char* argument, but the methods discussed in this issue don't seem to have such arguments.

@foulkesjohn
Copy link
Author

The ArrangeInvocation is a helper class for creating the invocation to stub as in my codebase there are a lot of the same stubs happening. The implementation looks something like this:

typedef void (^Invocation)(NSInvocation *);
+ (Invocation)successfulInvocationWithParameter:(id)parameter atIndex:(NSInteger)index
{
  return ^(NSInvocation *invocation) {
    void (^completionHandler)(id, NSError *);
    [invocation getArgument:&completionHandler atIndex:index];
    completionHandler(param, nil);
  };
}

I also couldn't replicate the issue away from my main code base, which is frustrating. The code I provided probably doesn't compile as I put it as more of an example of how I was using it incase I was doing something wrong.

I've taken to just using OCMExpect and OCMVerifyAll which works around the issue, if I get time to look at it again soon I will.

@erikdoe
Copy link
Owner

erikdoe commented Feb 17, 2015

Looking at this code I think you're running into a know problem with ARC and NSInvocation that isn't specific to OCMock. Have a look at this comment, maybe this helps: #123 (comment)

@foulkesjohn
Copy link
Author

I did try the suggestions in that comment but they didn't seem to help. I think my issue is something with Specta, but it needs more investigation. Thanks for taking time to look into it

@aspyct
Copy link

aspyct commented Mar 5, 2015

Just had the same issue (OCMock 3.1.2):

NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"mailto:[email protected]"]];

OCMStub([self.delegate secureWebviewShouldOpenMailSheetWithAdress:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
    [invocation retainArguments];
});

[self.secureWebView webView:self.webView
                shouldStartLoadWithRequest:request
                            navigationType:UIWebViewNavigationTypeLinkClicked];

OCMVerify([self.delegate secureWebviewShouldOpenMailSheetWithAdress:@"[email protected]"]);

If I remove that [invocation retainArguments], the test randomly succeeds, fails or crashes with EXC_BAD_ACCESS.

@drekka
Copy link

drekka commented Jun 23, 2015

Came across this with 3.1.2 and found the solution.

In my code I was calling another class and passing it a mock.

That class create an object and set it on the mock via a setValue: setter.

The class did not keep a reference to the object it just set on the mock so when the thread of execution returned to the test, the created object was released. As OCMock's internal NSInvocation of the setVale: method was not retaining the object, it realloced.

When I then executed a OCMVerify I got an EXEC_BAD_ACCESS.

I found that when I created an internal variable in the class that created the object and set it with the new object so that it was retained independently of the mock object. That everything worked.

Therefore I think that the problem is that OCMock's internal NSInvocations are running with retainArguments as NO (default).

So I'd like a way to be able to turn this on for cases where an argument value passed to a mock is not retained anywhere else in the code.

@hirad
Copy link

hirad commented Jul 15, 2015

I believe I'm seeing something similar. This is my code:

id delegateMock = OCMProtocolMock(@protocol(DBFetchedObjectsControllerDelegate));
controller.delegate = delegateMock;
[controller loadObjects];

OCMVerify([delegateMock controller:[OCMArg any] didDetectAdditions:[OCMArg checkWithBlock:^BOOL(NSSet* addedObjects) {
    NSLog(@"Block arg is %@", addedObjects);
    return [addedObjects count] == 5;
}]]);

[controller loadObjects];

The test prints "Block arg is __NSBlockVariable__ ..." before throwing an exception (__NSBlockVariable__ doesn't respond to count). It gets fixed with OCMExpect and OCMVerifyAll.

@sryze
Copy link

sryze commented Nov 18, 2015

I'm having a similar problem with OCMVerify and checkWithBlock, the test always crashes with EXC_BAD_ACCESS. I managed to reduce my test code to the following:

#import <AFNetworking.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>

@interface ExampleTests : XCTestCase

@end

@implementation ExampleTests

- (void)sendRequesUsingSessionManager:(AFHTTPSessionManager *)sessionManager {
    NSDictionary *parameters = @{@"param": @"value"};
    [sessionManager POST:@"test" parameters:parameters success:nil failure:nil];
}

- (void)testExample {
    id sessionManagerMock = OCMClassMock([AFHTTPSessionManager class]);
    [self sendRequesUsingSessionManager:sessionManagerMock];
    OCMVerify([sessionManagerMock POST:@"test" parameters:[OCMArg checkWithBlock:^BOOL(NSDictionary *parameters) {
        return YES;
    }] success:[OCMArg any] failure:[OCMArg any]]);
}

@end

When I turn on Enable Zombie Objects in Xcode, test execution breaks with the following message:

*** -[__NSDictionaryI retain]: message sent to deallocated instance 0x7fbd8306f280

It seems that the parameters dictionary is released before it gets passed to the checkWithBlock block.

@skunkworks
Copy link

Just to throw my hat into the ring, I believe I'm seeing the same issue in 3.2. The argument for the invocation, which is an NSString, is not retained by the object being partially mocked. This causes a crash most of the time.

When I retain the argument in a contrived way -- by adding the argument to a mutable array that's retained by the object -- it no longer crashes.

@davertay
Copy link

Like @erikdoe mentioned, I think this is an ARC + NSInvocation issue, but I'm trying to find a workaround and having no luck. This example is contrived but representative of some real world cases we have that are causing the same crash. There is a nested block that needs to be invoked, but invoking it causes EXC_BAD_ACCESS.

@interface SomeTestClient: NSObject
- (void)processTheResponse:(void (^)())responseBlock;
- (void)checkWithCompletion:(void (^)())completion;
@end

@implementation SomeTestClient

- (void)processTheResponse:(void (^)())responseBlock {
    NSLog(@"THIS SHOULD NEVER GET CALLED");
}

- (void)checkWithCompletion:(void (^)())completion {
    [self processTheResponse:^() {
        completion();
    }];
}

@end

@interface MockCrashTestTests : XCTestCase
@end

@implementation MockCrashTestTests

- (void)testNestedBlockCallback {
    SomeTestClient *testClient = OCMPartialMock([SomeTestClient new]);

    OCMExpect([testClient processTheResponse:[OCMArg checkWithBlock:^BOOL (id (^completionBlock)()) {
        completionBlock();  // CRASHES HERE WITH EXC_BAD_ACCESS
        return YES;
    }]]);

    [testClient checkWithCompletion:^() {
        NSLog(@"Actual completionBlock invoked");
    }];

    OCMVerifyAll((id)testClient);
}

@end

@dmaclach
Copy link
Contributor

@davertay Pulling an ancient thread out here, but in your case I think the problem is that your "checkWithBlock" is taking a block that returns an id, but the blocks you are passing in do not return an id. If I change your code to

OCMExpect([testClient processTheResponse:[OCMArg checkWithBlock:^BOOL (void (^completionBlock)(void)) {
        completionBlock();  // CRASHES HERE WITH EXC_BAD_ACCESS
        return YES;
    }]]);

the problem goes away.

@erikdoe
Copy link
Owner

erikdoe commented May 11, 2021

Assuming @dmaclach's answer is correct.

@erikdoe erikdoe closed this as completed May 11, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests