Skip to content

Commit 0b3faf2

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 0b3faf2

File tree

1 file changed

+61
-41
lines changed

1 file changed

+61
-41
lines changed

Source/OCMock/OCClassMockObject.m

Lines changed: 61 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,15 @@ - (void)stopMocking
8787

8888
- (void)stopMockingClassMethods
8989
{
90-
OCMSetAssociatedMockForClass(nil, mockedClass);
91-
object_setClass(mockedClass, originalMetaClass);
90+
// Synchronize around mockedClass to try and prevent class methods on other
91+
// threads being called while the class is being torn down.
92+
// See prepareClassForClassMethodMocking and forwardInvocationForClassObject
93+
// for other locations that are synchronized on this.
94+
@synchronized(mockedClass)
95+
{
96+
OCMSetAssociatedMockForClass(nil, mockedClass);
97+
object_setClass(mockedClass, originalMetaClass);
98+
}
9299
originalMetaClass = nil;
93100
/* created meta class will be disposed later because partial mocks create another subclass depending on it */
94101
}
@@ -119,48 +126,54 @@ - (void)prepareClassForClassMethodMocking
119126
if(otherMock != nil)
120127
[otherMock stopMockingClassMethods];
121128

122-
OCMSetAssociatedMockForClass(self, mockedClass);
123129

124130
/* dynamically create a subclass and use its meta class as the meta class for the mocked class */
125131
classCreatedForNewMetaClass = OCMCreateSubclass(mockedClass, mockedClass);
126132
originalMetaClass = object_getClass(mockedClass);
127133
id newMetaClass = object_getClass(classCreatedForNewMetaClass);
128-
129134
/* create a dummy initialize method */
130135
Method myDummyInitializeMethod = class_getInstanceMethod([self mockObjectClass], @selector(initializeForClassObject));
131136
const char *initializeTypes = method_getTypeEncoding(myDummyInitializeMethod);
132137
IMP myDummyInitializeIMP = method_getImplementation(myDummyInitializeMethod);
133138
class_addMethod(newMetaClass, @selector(initialize), myDummyInitializeIMP, initializeTypes);
134139

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];
140+
// Synchronize around mockedClass to try and prevent class methods on other
141+
// threads being called while the class is being set up.
142+
// See forwardInvocationForClassObject and stopMockingClassMethods for other
143+
// locations that are synchronized on this.
144+
@synchronized(mockedClass)
145+
{
146+
object_setClass(mockedClass, newMetaClass); // only after dummy initialize is installed (iOS9)
147+
OCMSetAssociatedMockForClass(self, mockedClass);
148+
149+
/* point forwardInvocation: of the object to the implementation in the mock */
150+
Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForClassObject:));
151+
IMP myForwardIMP = method_getImplementation(myForwardMethod);
152+
class_addMethod(newMetaClass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod));
153+
154+
/* adding forwarder for most class methods (instance methods on meta class) to allow for verify after run */
155+
NSArray *methodBlackList = @[
156+
@"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", @"isBlock",
157+
@"instanceMethodForwarderForSelector:", @"instanceMethodSignatureForSelector:", @"resolveClassMethod:"
158+
];
159+
void (^setupForwarderFiltered)(Class, SEL) = ^(Class cls, SEL sel) {
160+
if((cls == object_getClass([NSObject class])) || (cls == [NSObject class]) || (cls == object_getClass(cls)))
161+
return;
162+
if(OCMIsApplePrivateMethod(cls, sel))
163+
return;
164+
if([methodBlackList containsObject:NSStringFromSelector(sel)])
165+
return;
166+
@try
167+
{
168+
[self setupForwarderForClassMethodSelector:sel];
169+
}
170+
@catch(NSException *e)
171+
{
172+
// ignore for now
173+
}
174+
};
175+
[NSObject enumerateMethodsInClass:originalMetaClass usingBlock:setupForwarderFiltered];
176+
}
164177
}
165178

166179

@@ -184,15 +197,22 @@ - (void)setupForwarderForClassMethodSelector:(SEL)selector
184197
- (void)forwardInvocationForClassObject:(NSInvocation *)anInvocation
185198
{
186199
// 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)
200+
// Synchronize around self to try and prevent the class from being torn
201+
// down while a method is being called on it.
202+
// See prepareClassForClassMethodMocking and stopMockingClassMethods for
203+
// other locations that are synchronized on this.
204+
@synchronized(self)
193205
{
194-
[anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])];
195-
[anInvocation invoke];
206+
OCClassMockObject *mock = OCMGetAssociatedMockForClass((Class)self, YES);
207+
if(mock == nil)
208+
{
209+
[anInvocation invoke];
210+
}
211+
else if([mock handleInvocation:anInvocation] == NO)
212+
{
213+
[anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])];
214+
[anInvocation invoke];
215+
}
196216
}
197217
}
198218

0 commit comments

Comments
 (0)