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

va_arg() causes EXC_BAD_ACCESS in mocked objects #524

Open
karolszafranski opened this issue Aug 5, 2022 · 5 comments
Open

va_arg() causes EXC_BAD_ACCESS in mocked objects #524

karolszafranski opened this issue Aug 5, 2022 · 5 comments
Labels
M1 Requires Apple silicon to address

Comments

@karolszafranski
Copy link

Executing a variadic method on a mocked object results in an "EXC_BAD_ACCESS" crash.

The variadic method does not have to be executed with OCMock (#191). It occurs even if called directly.

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

@interface BaseClass: NSObject
@end

@implementation BaseClass

- (void)logLevel:(NSUInteger)messageLogLevel message:(NSString *)format, ... {
    va_list args;
    va_start(args, format);
    NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
    va_end(args);
    NSLog(@"%li: %@", messageLogLevel, message);
}

@end

@interface OCMockFailureTestTests : XCTestCase
@end

@implementation OCMockFailureTestTests

- (void)testExample {
    BaseClass* obj = [BaseClass new];
    id mock = [OCMockObject partialMockForObject:obj];

    [obj logLevel:3 message:@"%@", @"abc"]; // crash
}

@end

Executed on an iOS Simulator running on a Macbook Pro with M1, not reproducible with i5.

OCMock v3.9.1

@JeromeTonnelierOgury
Copy link

Same here! Any news ?

@erikdoe
Copy link
Owner

erikdoe commented Feb 27, 2023

Still don't have access to a Mac with Apple Silicon.

@erikdoe erikdoe added the M1 Requires Apple silicon to address label Feb 27, 2023
@marchv
Copy link

marchv commented Dec 18, 2023

I've looked a little at this and it seems like NSInvocation is used. I then skimmed the documentation and found

Screenshot 2023-12-18 at 14 25 50

I have no experience with NSInvocation but I am wondering if those if those two sections are fulfilled for OCMock's implementation or if OCMock just doesn't support variable arguments for the time being?

@smorr
Copy link

smorr commented May 1, 2024

The best workaround I have is to get OCPartialMock to ignore (or "demock") specific selectors.

When mocking an object, OCM adds all the methods of the real object to call objc_msgForward. This allows OCM to handle the stubs. but because _objc_msgForward wraps the message into an NSInvocation, it will fail for variadic args.

The thing I do is redirect specific methods on the mockObject back to the original implementation of the real object. so that any calls to the variadic argument method is calling the actual method, rather than forwarding using an NSInvocation. While it means there is no test mocking for that method 😔, it also means that things won't crash 😃

I have written a category on OCPartialMockObject to do this and a macro "OCMIgnore" for convenience:

and added it to the top of my XCTTest Class.

#define OCMIgnore(object,method) do { if (object_getClass(object) == OCPartialMockObject.class) [(OCPartialMockObject*)object _ocm_ignoreMethod:method];} while (0)

@interface  OCPartialMockObject : NSProxy  @end // unimplemented declaration so the category implementation doesn't fail

@implementation OCPartialMockObject(SC)
-(void)_ocm_ignoreMethod:(SEL)methodSelector{
    Ivar ivar = class_getInstanceVariable(OCPartialMockObject.class,"realObject");
    if (ivar){
        id mockObject = object_getIvar(self, ivar); // despite ivar name, the value is the created Mock object
        if (mockObject){
            Method mockMethod = class_getInstanceMethod(object_getClass(mockObject), methodSelector);
            Method realMethod = class_getInstanceMethod(self.class, methodSelector);
            if (realMethod && mockMethod){
                IMP realIMP = method_getImplementation(realMethod);
                IMP mockIMP = method_getImplementation(mockMethod);
                if (mockIMP && realIMP && mockIMP == _objc_msgForward){
                    method_setImplementation(mockMethod, realIMP);
                }
            }
        }
    }
}
@end


Usage:

@interface MyObject : NSObject
-(void)log:(NSString*)format , ...;
@end

// Partial mock a new object
MYObject * mockObject = OCMPartialMock([MyObject.alloc init]);

// ignore (undo the mock) of the following selector:
OCMIgnore(mockObject,@selector(log:));

BTW this can be used for any method, not just ones that have variadic arguments.

@smorr
Copy link

smorr commented May 2, 2024

Actually in further use it is more complicated than this because stubbing new methods will rewire implementation pointers after an ignore :(

Wince my comment yesterday, I reworked things so that you can ignore mocking a specific selector for a class (and its descendent classes).

OCMIgnore([MyClass class],@selector(log:));

now registers the log: selector for the class as to be ignored. When mocking, stubbing and message forwarding, OCM mock will lookup the selector to by forwarded and ensure it is not forwarded in the mocking process. an that the real object gets the message:

The changes are multifold but can be found in pull request #539

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
M1 Requires Apple silicon to address
Projects
None yet
Development

No branches or pull requests

5 participants