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

Fix up potential race conditions with class mocks. #502

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
102 changes: 61 additions & 41 deletions Source/OCMock/OCClassMockObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,15 @@ - (void)stopMocking

- (void)stopMockingClassMethods
{
OCMSetAssociatedMockForClass(nil, mockedClass);
object_setClass(mockedClass, originalMetaClass);
// Synchronize around mockedClass to try and prevent class methods on other
// threads being called while the class is being torn down.
// See prepareClassForClassMethodMocking and forwardInvocationForClassObject
// for other locations that are synchronized on this.
@synchronized(mockedClass)
{
OCMSetAssociatedMockForClass(nil, mockedClass);
object_setClass(mockedClass, originalMetaClass);
}
originalMetaClass = nil;
/* created meta class will be disposed later because partial mocks create another subclass depending on it */
}
Expand Down Expand Up @@ -119,48 +126,54 @@ - (void)prepareClassForClassMethodMocking
if(otherMock != nil)
[otherMock stopMockingClassMethods];

OCMSetAssociatedMockForClass(self, mockedClass);

/* dynamically create a subclass and use its meta class as the meta class for the mocked class */
classCreatedForNewMetaClass = OCMCreateSubclass(mockedClass, mockedClass);
originalMetaClass = object_getClass(mockedClass);
id newMetaClass = object_getClass(classCreatedForNewMetaClass);

/* create a dummy initialize method */
Method myDummyInitializeMethod = class_getInstanceMethod([self mockObjectClass], @selector(initializeForClassObject));
const char *initializeTypes = method_getTypeEncoding(myDummyInitializeMethod);
IMP myDummyInitializeIMP = method_getImplementation(myDummyInitializeMethod);
class_addMethod(newMetaClass, @selector(initialize), myDummyInitializeIMP, initializeTypes);

object_setClass(mockedClass, newMetaClass); // only after dummy initialize is installed (iOS9)

/* point forwardInvocation: of the object to the implementation in the mock */
Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForClassObject:));
IMP myForwardIMP = method_getImplementation(myForwardMethod);
class_addMethod(newMetaClass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod));

/* adding forwarder for most class methods (instance methods on meta class) to allow for verify after run */
NSArray *methodBlackList = @[
@"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", @"isBlock",
@"instanceMethodForwarderForSelector:", @"instanceMethodSignatureForSelector:", @"resolveClassMethod:"
];
void (^setupForwarderFiltered)(Class, SEL) = ^(Class cls, SEL sel) {
if((cls == object_getClass([NSObject class])) || (cls == [NSObject class]) || (cls == object_getClass(cls)))
return;
if(OCMIsApplePrivateMethod(cls, sel))
return;
if([methodBlackList containsObject:NSStringFromSelector(sel)])
return;
@try
{
[self setupForwarderForClassMethodSelector:sel];
}
@catch(NSException *e)
{
// ignore for now
}
};
[NSObject enumerateMethodsInClass:originalMetaClass usingBlock:setupForwarderFiltered];
// Synchronize around mockedClass to try and prevent class methods on other
// threads being called while the class is being set up.
// See forwardInvocationForClassObject and stopMockingClassMethods for other
// locations that are synchronized on this.
@synchronized(mockedClass)
{
object_setClass(mockedClass, newMetaClass); // only after dummy initialize is installed (iOS9)
OCMSetAssociatedMockForClass(self, mockedClass);

/* point forwardInvocation: of the object to the implementation in the mock */
Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForClassObject:));
IMP myForwardIMP = method_getImplementation(myForwardMethod);
class_addMethod(newMetaClass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod));

/* adding forwarder for most class methods (instance methods on meta class) to allow for verify after run */
NSArray *methodBlackList = @[
@"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", @"isBlock",
@"instanceMethodForwarderForSelector:", @"instanceMethodSignatureForSelector:", @"resolveClassMethod:"
];
void (^setupForwarderFiltered)(Class, SEL) = ^(Class cls, SEL sel) {
if((cls == object_getClass([NSObject class])) || (cls == [NSObject class]) || (cls == object_getClass(cls)))
return;
if(OCMIsApplePrivateMethod(cls, sel))
return;
if([methodBlackList containsObject:NSStringFromSelector(sel)])
return;
@try
{
[self setupForwarderForClassMethodSelector:sel];
}
@catch(NSException *e)
{
// ignore for now
}
};
[NSObject enumerateMethodsInClass:originalMetaClass usingBlock:setupForwarderFiltered];
}
}


Expand All @@ -184,15 +197,22 @@ - (void)setupForwarderForClassMethodSelector:(SEL)selector
- (void)forwardInvocationForClassObject:(NSInvocation *)anInvocation
{
// in here "self" is a reference to the real class, not the mock
OCClassMockObject *mock = OCMGetAssociatedMockForClass((Class)self, YES);
if(mock == nil)
{
[NSException raise:NSInternalInconsistencyException format:@"No mock for class %@", NSStringFromClass((Class)self)];
}
if([mock handleInvocation:anInvocation] == NO)
// Synchronize around self to try and prevent the class from being torn
// down while a method is being called on it.
// See prepareClassForClassMethodMocking and stopMockingClassMethods for
// other locations that are synchronized on this.
@synchronized(self)
{
[anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])];
[anInvocation invoke];
OCClassMockObject *mock = OCMGetAssociatedMockForClass((Class)self, YES);
if(mock == nil)
{
[anInvocation invoke];
erikdoe marked this conversation as resolved.
Show resolved Hide resolved
}
else if([mock handleInvocation:anInvocation] == NO)
{
[anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])];
[anInvocation invoke];
}
}
}

Expand Down