Skip to content

Commit ecdc529

Browse files
committed
Fix up potential race conditions with class mocks.
Partial fix for #501. Wraps up a bunch of locations with synchronization blocks to attempt to make sure that the class is coherent before it starts receiving messages.
1 parent b9c7fb9 commit ecdc529

File tree

1 file changed

+49
-42
lines changed

1 file changed

+49
-42
lines changed

Source/OCMock/OCClassMockObject.m

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ - (void)stopMocking
8787

8888
- (void)stopMockingClassMethods
8989
{
90-
OCMSetAssociatedMockForClass(nil, mockedClass);
91-
object_setClass(mockedClass, originalMetaClass);
90+
@synchronized(mockedClass)
91+
{
92+
OCMSetAssociatedMockForClass(nil, mockedClass);
93+
object_setClass(mockedClass, originalMetaClass);
94+
}
9295
originalMetaClass = nil;
9396
/* created meta class will be disposed later because partial mocks create another subclass depending on it */
9497
}
@@ -119,48 +122,49 @@ - (void)prepareClassForClassMethodMocking
119122
if(otherMock != nil)
120123
[otherMock stopMockingClassMethods];
121124

122-
OCMSetAssociatedMockForClass(self, mockedClass);
123125

124126
/* dynamically create a subclass and use its meta class as the meta class for the mocked class */
125127
classCreatedForNewMetaClass = OCMCreateSubclass(mockedClass, mockedClass);
126128
originalMetaClass = object_getClass(mockedClass);
127129
id newMetaClass = object_getClass(classCreatedForNewMetaClass);
128-
129130
/* create a dummy initialize method */
130131
Method myDummyInitializeMethod = class_getInstanceMethod([self mockObjectClass], @selector(initializeForClassObject));
131132
const char *initializeTypes = method_getTypeEncoding(myDummyInitializeMethod);
132133
IMP myDummyInitializeIMP = method_getImplementation(myDummyInitializeMethod);
133134
class_addMethod(newMetaClass, @selector(initialize), myDummyInitializeIMP, initializeTypes);
134-
135-
object_setClass(mockedClass, newMetaClass); // only after dummy initialize is installed (iOS9)
136-
137-
/* point forwardInvocation: of the object to the implementation in the mock */
138-
Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForClassObject:));
139-
IMP myForwardIMP = method_getImplementation(myForwardMethod);
140-
class_addMethod(newMetaClass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod));
141-
142-
/* adding forwarder for most class methods (instance methods on meta class) to allow for verify after run */
143-
NSArray *methodBlackList = @[
144-
@"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", @"isBlock",
145-
@"instanceMethodForwarderForSelector:", @"instanceMethodSignatureForSelector:", @"resolveClassMethod:"
146-
];
147-
void (^setupForwarderFiltered)(Class, SEL) = ^(Class cls, SEL sel) {
148-
if((cls == object_getClass([NSObject class])) || (cls == [NSObject class]) || (cls == object_getClass(cls)))
149-
return;
150-
if(OCMIsApplePrivateMethod(cls, sel))
151-
return;
152-
if([methodBlackList containsObject:NSStringFromSelector(sel)])
153-
return;
154-
@try
155-
{
156-
[self setupForwarderForClassMethodSelector:sel];
157-
}
158-
@catch(NSException *e)
159-
{
160-
// ignore for now
161-
}
162-
};
163-
[NSObject enumerateMethodsInClass:originalMetaClass usingBlock:setupForwarderFiltered];
135+
@synchronized(mockedClass)
136+
{
137+
object_setClass(mockedClass, newMetaClass); // only after dummy initialize is installed (iOS9)
138+
OCMSetAssociatedMockForClass(self, mockedClass);
139+
140+
/* point forwardInvocation: of the object to the implementation in the mock */
141+
Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForClassObject:));
142+
IMP myForwardIMP = method_getImplementation(myForwardMethod);
143+
class_addMethod(newMetaClass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod));
144+
145+
/* adding forwarder for most class methods (instance methods on meta class) to allow for verify after run */
146+
NSArray *methodBlackList = @[
147+
@"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", @"isBlock",
148+
@"instanceMethodForwarderForSelector:", @"instanceMethodSignatureForSelector:", @"resolveClassMethod:"
149+
];
150+
void (^setupForwarderFiltered)(Class, SEL) = ^(Class cls, SEL sel) {
151+
if((cls == object_getClass([NSObject class])) || (cls == [NSObject class]) || (cls == object_getClass(cls)))
152+
return;
153+
if(OCMIsApplePrivateMethod(cls, sel))
154+
return;
155+
if([methodBlackList containsObject:NSStringFromSelector(sel)])
156+
return;
157+
@try
158+
{
159+
[self setupForwarderForClassMethodSelector:sel];
160+
}
161+
@catch(NSException *e)
162+
{
163+
// ignore for now
164+
}
165+
};
166+
[NSObject enumerateMethodsInClass:originalMetaClass usingBlock:setupForwarderFiltered];
167+
}
164168
}
165169

166170

@@ -184,15 +188,18 @@ - (void)setupForwarderForClassMethodSelector:(SEL)selector
184188
- (void)forwardInvocationForClassObject:(NSInvocation *)anInvocation
185189
{
186190
// in here "self" is a reference to the real class, not the mock
187-
OCClassMockObject *mock = OCMGetAssociatedMockForClass((Class)self, YES);
188-
if(mock == nil)
189-
{
190-
[NSException raise:NSInternalInconsistencyException format:@"No mock for class %@", NSStringFromClass((Class)self)];
191-
}
192-
if([mock handleInvocation:anInvocation] == NO)
191+
@synchronized(self)
193192
{
194-
[anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])];
195-
[anInvocation invoke];
193+
OCClassMockObject *mock = OCMGetAssociatedMockForClass((Class)self, YES);
194+
if(mock == nil)
195+
{
196+
[anInvocation invoke];
197+
}
198+
else if([mock handleInvocation:anInvocation] == NO)
199+
{
200+
[anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])];
201+
[anInvocation invoke];
202+
}
196203
}
197204
}
198205

0 commit comments

Comments
 (0)