diff --git a/Source/OCMock.xcodeproj/project.pbxproj b/Source/OCMock.xcodeproj/project.pbxproj index 55f31141..8865ae3c 100644 --- a/Source/OCMock.xcodeproj/project.pbxproj +++ b/Source/OCMock.xcodeproj/project.pbxproj @@ -181,6 +181,8 @@ 2FA28FEAEF9333D2C214DF53 /* NSValue+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28896E5EEFD7C2F12C2F8 /* NSValue+OCMAdditions.m */; }; 3C0FF06A1BAA3FD10021AD20 /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; }; 3C0FF06B1BAA3FD20021AD20 /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; }; + 3C76716C1BB3EBC500FDC9F4 /* TestClassWithCustomReferenceCounting.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3CFBDD771BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 817EB1171BD765130047E85A /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; }; 817EB1181BD765130047E85A /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; }; 817EB1191BD765130047E85A /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; }; @@ -444,6 +446,8 @@ 2FA28EC49F6C59B940AE6D00 /* OCMFunctions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMFunctions.h; sourceTree = ""; }; 2FA28EDBF243639C57F88A1B /* OCMArgTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMArgTests.m; sourceTree = ""; }; 2FA28EE3142412BD601026EF /* OCMockObjectDynamicPropertyMockingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectDynamicPropertyMockingTests.m; sourceTree = ""; }; + 3CFBDD751BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestClassWithCustomReferenceCounting.h; sourceTree = ""; }; + 3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestClassWithCustomReferenceCounting.m; sourceTree = ""; }; 817EB1621BD765130047E85A /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D31108AD1828DB8700737925 /* OCMockLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCMockLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D31108B71828DB8700737925 /* OCMockLibTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OCMockLibTests-Info.plist"; sourceTree = ""; }; @@ -612,6 +616,8 @@ 03B316291463350E0052CD09 /* OCObserverMockObjectTests.m */, 03B3161F1463350E0052CD09 /* NSInvocationOCMAdditionsTests.m */, 2FA28DEDB9163597B7C49F3D /* NSMethodSignatureOCMAdditionsTests.m */, + 3CFBDD751BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.h */, + 3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */, 03565A3418F0566F003AE91E /* Supporting Files */, ); path = OCMockTests; @@ -1244,6 +1250,7 @@ 03565A4418F05721003AE91E /* OCMockObjectProtocolMocksTests.m in Sources */, 03565A4B18F05721003AE91E /* NSInvocationOCMAdditionsTests.m in Sources */, 03565A4618F05721003AE91E /* OCMockObjectHamcrestTests.mm in Sources */, + 3CFBDD771BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m in Sources */, 03E98D5018F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */, 03565A4A18F05721003AE91E /* OCObserverMockObjectTests.m in Sources */, 03565A4318F05721003AE91E /* OCMockObjectClassMethodMockingTests.m in Sources */, @@ -1305,6 +1312,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3C76716C1BB3EBC500FDC9F4 /* TestClassWithCustomReferenceCounting.m in Sources */, 031E50591BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */, 03C9CA1E18F05A84006DF94D /* OCMArgTests.m in Sources */, 0322DA66191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m in Sources */, diff --git a/Source/OCMock/NSInvocation+OCMAdditions.h b/Source/OCMock/NSInvocation+OCMAdditions.h index 3fbd9255..f33c3b83 100644 --- a/Source/OCMock/NSInvocation+OCMAdditions.h +++ b/Source/OCMock/NSInvocation+OCMAdditions.h @@ -20,7 +20,7 @@ + (NSInvocation *)invocationForBlock:(id)block withArguments:(NSArray *)arguments; -- (BOOL)hasCharPointerArgument; +- (void)retainObjectArgumentsExcluding:(id)objectToExclude; - (id)getArgumentAtIndexAsObject:(NSInteger)argIndex; diff --git a/Source/OCMock/NSInvocation+OCMAdditions.m b/Source/OCMock/NSInvocation+OCMAdditions.m index b82b4460..119c09b9 100644 --- a/Source/OCMock/NSInvocation+OCMAdditions.m +++ b/Source/OCMock/NSInvocation+OCMAdditions.m @@ -14,7 +14,9 @@ * under the License. */ +#import #import "NSInvocation+OCMAdditions.h" +#import "OCMFunctions.h" #import "OCMFunctionsPrivate.h" #import "NSMethodSignature+OCMAdditions.h" @@ -41,16 +43,74 @@ + (NSInvocation *)invocationForBlock:(id)block withArguments:(NSArray *)argument } -- (BOOL)hasCharPointerArgument +static NSString *const OCMRetainedObjectArgumentsKey = @"OCMRetainedObjectArgumentsKey"; + +- (void)retainObjectArgumentsExcluding:(id)objectToExclude { - NSMethodSignature *signature = [self methodSignature]; - for(NSUInteger i = 0; i < [signature numberOfArguments]; i++) + if(objc_getAssociatedObject(self, OCMRetainedObjectArgumentsKey) == nil) { - const char *argType = OCMTypeWithoutQualifiers([signature getArgumentTypeAtIndex:i]); - if(strcmp(argType, "*") == 0) - return YES; + NSMutableArray *retainedArguments = [[NSMutableArray alloc] init]; + NSMethodSignature *methodSignature = self.methodSignature; + + id target = self.target; + if((target != nil) && (target != objectToExclude) && !object_isClass(target)) + { + // Bad things will happen if the target is a block since it's not being + // copied. There isn't a very good way to tell if an invocation's target + // is a block though (the argument type at index 0 is always "@" even if + // the target is a Class or block), and in practice it's OK since you + // can't mock a block. + [retainedArguments addObject:target]; + } + + NSUInteger numberOfArguments = methodSignature.numberOfArguments; + for(NSUInteger index = 2; index < numberOfArguments; index++) + { + const char *argumentType = [methodSignature getArgumentTypeAtIndex:index]; + if(OCMIsObjectType(argumentType) && !OCMIsClassType(argumentType)) + { + id argument; + [self getArgument:&argument atIndex:index]; + if((argument != nil) && (argument != objectToExclude)) + { + if(OCMIsBlockType(argumentType)) + { + // block types need to be copied in case they're stack blocks + id blockArgument = [argument copy]; + [retainedArguments addObject:blockArgument]; + [blockArgument release]; + } + else + { + [retainedArguments addObject:argument]; + } + } + } + } + + const char *returnType = methodSignature.methodReturnType; + if(OCMIsObjectType(returnType) && !OCMIsClassType(returnType)) + { + id returnValue; + [self getReturnValue:&returnValue]; + if((returnValue != nil) && (returnValue != objectToExclude)) + { + if(OCMIsBlockType(returnType)) + { + id blockReturnValue = [returnValue copy]; + [retainedArguments addObject:blockReturnValue]; + [blockReturnValue release]; + } + else + { + [retainedArguments addObject:returnValue]; + } + } + } + + objc_setAssociatedObject(self, OCMRetainedObjectArgumentsKey, retainedArguments, OBJC_ASSOCIATION_RETAIN); + [retainedArguments release]; } - return NO; } diff --git a/Source/OCMock/OCMFunctions.m b/Source/OCMock/OCMFunctions.m index 8b97d5d9..b07ee10f 100644 --- a/Source/OCMock/OCMFunctions.m +++ b/Source/OCMock/OCMFunctions.m @@ -35,15 +35,36 @@ - (void)failWithException:(NSException *)exception; #pragma mark Functions related to ObjC type system +static BOOL OCMIsUnQualifiedClassType(const char *unqualifiedObjCType) +{ + return (strcmp(unqualifiedObjCType, @encode(Class)) == 0); +} + + +static BOOL OCMIsUnQualifiedBlockType(const char *unqualifiedObjCType) +{ + char blockType[] = @encode(void(^)()); + if(strcmp(unqualifiedObjCType, blockType) == 0) + return YES; + + // sometimes block argument/return types are tacked onto the type, in angle brackets + if(strncmp(unqualifiedObjCType, blockType, sizeof(blockType) - 1) == 0 && unqualifiedObjCType[sizeof(blockType) - 1] == '<') + return YES; + + return NO; +} + + BOOL OCMIsObjectType(const char *objCType) { objCType = OCMTypeWithoutQualifiers(objCType); - if(strcmp(objCType, @encode(id)) == 0 || strcmp(objCType, @encode(Class)) == 0) + char objectType[] = @encode(id); + if(strcmp(objCType, objectType) == 0 || OCMIsUnQualifiedClassType(objCType)) return YES; // sometimes the name of an object's class is tacked onto the type, in double quotes - if(strncmp(objCType, @encode(id), 1) == 0 && objCType[1] == '\"') + if(strncmp(objCType, objectType, sizeof(objectType) - 1) == 0 && objCType[sizeof(objectType) - 1] == '"') return YES; // if the returnType is a typedef to an object, it has the form ^{OriginClass=#} @@ -54,11 +75,19 @@ BOOL OCMIsObjectType(const char *objCType) return YES; // if the return type is a block we treat it like an object - // TODO: if the runtime were to encode the block's argument and/or return types, this test would not be sufficient - if(strcmp(objCType, @encode(void(^)())) == 0) - return YES; + return OCMIsUnQualifiedBlockType(objCType); +} - return NO; + +BOOL OCMIsClassType(const char *objCType) +{ + return OCMIsUnQualifiedClassType(OCMTypeWithoutQualifiers(objCType)); +} + + +BOOL OCMIsBlockType(const char *objCType) +{ + return OCMIsUnQualifiedBlockType(OCMTypeWithoutQualifiers(objCType)); } diff --git a/Source/OCMock/OCMFunctionsPrivate.h b/Source/OCMock/OCMFunctionsPrivate.h index 06924ee8..3288885b 100644 --- a/Source/OCMock/OCMFunctionsPrivate.h +++ b/Source/OCMock/OCMFunctionsPrivate.h @@ -28,7 +28,8 @@ @class OCPartialMockObject; -OCMOCK_EXTERN BOOL OCMIsObjectType(const char *objCType); +OCMOCK_EXTERN BOOL OCMIsClassType(const char *objCType); +OCMOCK_EXTERN BOOL OCMIsBlockType(const char *objCType); const char *OCMTypeWithoutQualifiers(const char *objCType); BOOL OCMEqualTypesAllowingOpaqueStructs(const char *type1, const char *type2); CFNumberType OCMNumberTypeForObjCType(const char *objcType); diff --git a/Source/OCMock/OCMInvocationMatcher.m b/Source/OCMock/OCMInvocationMatcher.m index 416cb1df..91561e1f 100644 --- a/Source/OCMock/OCMInvocationMatcher.m +++ b/Source/OCMock/OCMInvocationMatcher.m @@ -21,6 +21,7 @@ #import "NSInvocation+OCMAdditions.h" #import "OCMInvocationMatcher.h" #import "OCClassMockObject.h" +#import "OCMFunctions.h" #import "OCMFunctionsPrivate.h" #import "OCMBlockArgCaller.h" @@ -41,11 +42,11 @@ - (void)dealloc - (void)setInvocation:(NSInvocation *)anInvocation { [recordedInvocation release]; - // When the method has a char* argument we do not retain the arguments. This makes it possible - // to match char* args literally and with anyPointer. Not retaining the argument means that - // in these cases tests that use their own autorelease pools may fail unexpectedly. - if(![anInvocation hasCharPointerArgument]) - [anInvocation retainArguments]; + // Don't do a regular -retainArguments on the invocation that we use for matching. NSInvocation + // effectively does an strcpy on char* arguments which messes up matching them literally and blows + // up with anyPointer (in strlen since it's not actually a C string). Also on the off-chance that + // anInvocation contains self as an argument, -retainArguments would create a retain cycle. + [anInvocation retainObjectArgumentsExcluding:self]; recordedInvocation = [anInvocation retain]; } diff --git a/Source/OCMock/OCMMacroState.m b/Source/OCMock/OCMMacroState.m index 9ebdc88c..13a8df0c 100644 --- a/Source/OCMock/OCMMacroState.m +++ b/Source/OCMock/OCMMacroState.m @@ -24,28 +24,34 @@ @implementation OCMMacroState -static OCMMacroState *globalState; +static NSString *const OCMGlobalStateKey = @"OCMGlobalStateKey"; #pragma mark Methods to begin/end macros + (void)beginStubMacro { OCMStubRecorder *recorder = [[[OCMStubRecorder alloc] init] autorelease]; - globalState = [[[OCMMacroState alloc] initWithRecorder:recorder] autorelease]; + OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; + [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; + [macroState release]; } + (OCMStubRecorder *)endStubMacro { - OCMStubRecorder *recorder = (OCMStubRecorder *)[globalState recorder]; - globalState = nil; - return recorder; + NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; + OCMMacroState *globalState = threadDictionary[OCMGlobalStateKey]; + OCMStubRecorder *recorder = [(OCMStubRecorder *)[globalState recorder] retain]; + [threadDictionary removeObjectForKey:OCMGlobalStateKey]; + return [recorder autorelease]; } + (void)beginExpectMacro { OCMExpectationRecorder *recorder = [[[OCMExpectationRecorder alloc] init] autorelease]; - globalState = [[[OCMMacroState alloc] initWithRecorder:recorder] autorelease]; + OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; + [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; + [macroState release]; } + (OCMStubRecorder *)endExpectMacro @@ -58,12 +64,14 @@ + (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation { OCMVerifier *recorder = [[[OCMVerifier alloc] init] autorelease]; [recorder setLocation:aLocation]; - globalState = [[[OCMMacroState alloc] initWithRecorder:recorder] autorelease]; + OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; + [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; + [macroState release]; } + (void)endVerifyMacro { - globalState = nil; + [[NSThread currentThread].threadDictionary removeObjectForKey:OCMGlobalStateKey]; } @@ -71,7 +79,7 @@ + (void)endVerifyMacro + (OCMMacroState *)globalState { - return globalState; + return [NSThread currentThread].threadDictionary[OCMGlobalStateKey]; } @@ -90,8 +98,7 @@ - (id)initWithRecorder:(OCMRecorder *)aRecorder - (void)dealloc { [recorder release]; - if(globalState == self) - globalState = nil; + NSAssert([NSThread currentThread].threadDictionary[OCMGlobalStateKey] != self, @"Unexpected dealloc while set as the global state"); [super dealloc]; } diff --git a/Source/OCMock/OCMock.h b/Source/OCMock/OCMock.h index 2d48cf64..40a5e40b 100644 --- a/Source/OCMock/OCMock.h +++ b/Source/OCMock/OCMock.h @@ -42,8 +42,13 @@ ({ \ _OCMSilenceWarnings( \ [OCMMacroState beginStubMacro]; \ - invocation; \ - [OCMMacroState endStubMacro]; \ + OCMStubRecorder *recorder = nil; \ + @try{ \ + invocation; \ + }@finally{ \ + recorder = [OCMMacroState endStubMacro]; \ + } \ + recorder; \ ); \ }) @@ -51,8 +56,13 @@ ({ \ _OCMSilenceWarnings( \ [OCMMacroState beginExpectMacro]; \ - invocation; \ - [OCMMacroState endExpectMacro]; \ + OCMStubRecorder *recorder = nil; \ + @try{ \ + invocation; \ + }@finally{ \ + recorder = [OCMMacroState endExpectMacro]; \ + } \ + recorder; \ ); \ }) @@ -71,8 +81,11 @@ ({ \ _OCMSilenceWarnings( \ [OCMMacroState beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)]; \ - invocation; \ - [OCMMacroState endVerifyMacro]; \ + @try{ \ + invocation; \ + }@finally{ \ + [OCMMacroState endVerifyMacro]; \ + } \ ); \ }) diff --git a/Source/OCMock/OCMockObject.m b/Source/OCMock/OCMockObject.m index 556a7ac3..14be3120 100644 --- a/Source/OCMock/OCMockObject.m +++ b/Source/OCMock/OCMockObject.m @@ -113,12 +113,18 @@ - (NSString *)description - (void)addStub:(OCMInvocationStub *)aStub { - [stubs addObject:aStub]; + @synchronized(stubs) + { + [stubs addObject:aStub]; + } } - (void)addExpectation:(OCMInvocationExpectation *)anExpectation { - [expectations addObject:anExpectation]; + @synchronized(expectations) + { + [expectations addObject:anExpectation]; + } } @@ -159,10 +165,13 @@ - (id)verify - (id)verifyAtLocation:(OCMLocation *)location { NSMutableArray *unsatisfiedExpectations = [NSMutableArray array]; - for(OCMInvocationExpectation *e in expectations) + @synchronized(expectations) { - if(![e isSatisfied]) - [unsatisfiedExpectations addObject:e]; + for(OCMInvocationExpectation *e in expectations) + { + if(![e isSatisfied]) + [unsatisfiedExpectations addObject:e]; + } } if([unsatisfiedExpectations count] == 1) @@ -178,12 +187,18 @@ - (id)verifyAtLocation:(OCMLocation *)location OCMReportFailure(location, description); } - if([exceptions count] > 0) + OCMInvocationExpectation *firstException = nil; + @synchronized(exceptions) + { + firstException = [exceptions.firstObject retain]; + } + if(firstException) { NSString *description = [NSString stringWithFormat:@"%@: %@ (This is a strict mock failure that was ignored when it actually occured.)", - [self description], [[exceptions objectAtIndex:0] description]]; + [self description], [firstException description]]; OCMReportFailure(location, description); } + [firstException release]; return [[[OCMVerifier alloc] initWithMockObject:self] autorelease]; } @@ -199,8 +214,11 @@ - (void)verifyWithDelay:(NSTimeInterval)delay atLocation:(OCMLocation *)location NSTimeInterval step = 0.01; while(delay > 0) { - if([expectations count] == 0) - break; + @synchronized(expectations) + { + if([expectations count] == 0) + break; + } [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:MIN(step, delay)]]; delay -= step; step *= 2; @@ -218,10 +236,13 @@ - (void)verifyInvocation:(OCMInvocationMatcher *)matcher - (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation *)location { - for(NSInvocation *invocation in invocations) + @synchronized(invocations) { - if([matcher matchesInvocation:invocation]) - return; + for(NSInvocation *invocation in invocations) + { + if([matcher matchesInvocation:invocation]) + return; + } } NSString *description = [NSString stringWithFormat:@"%@: Method %@ was not invoked.", [self description], [matcher description]]; @@ -246,10 +267,12 @@ - (id)forwardingTargetForSelector:(SEL)aSelector - (BOOL)handleSelector:(SEL)sel { - for(OCMInvocationStub *recorder in stubs) - if([recorder matchesSelector:sel]) - return YES; - + @synchronized(stubs) + { + for(OCMInvocationStub *recorder in stubs) + if([recorder matchesSelector:sel]) + return YES; + } return NO; } @@ -269,7 +292,10 @@ - (void)forwardInvocation:(NSInvocation *)anInvocation else { // add non-stubbed method to list of exceptions to be re-raised in verify - [exceptions addObject:e]; + @synchronized(exceptions) + { + [exceptions addObject:e]; + } } [e raise]; } @@ -277,43 +303,65 @@ - (void)forwardInvocation:(NSInvocation *)anInvocation - (BOOL)handleInvocation:(NSInvocation *)anInvocation { - [invocations addObject:anInvocation]; - + @synchronized(invocations) + { + // We can't do a normal retain arguments on anInvocation because its target/arguments/return + // value could be self. That would produce a retain cycle self->invocations->anInvocation->self. + // However we need to retain everything on anInvocation that isn't self because we expect them to + // stick around after this method returns. Use our special method to retain just what's needed. + [anInvocation retainObjectArgumentsExcluding:self]; + [invocations addObject:anInvocation]; + } + OCMInvocationStub *stub = nil; - for(stub in stubs) + @synchronized(stubs) { - // If the stub forwards its invocation to the real object, then we don't want to do handleInvocation: yet, since forwarding the invocation to the real object could call a method that is expected to happen after this one, which is bad if expectationOrderMatters is YES - if([stub matchesInvocation:anInvocation]) - break; + for(stub in stubs) + { + // If the stub forwards its invocation to the real object, then we don't want to do handleInvocation: yet, since forwarding the invocation to the real object could call a method that is expected to happen after this one, which is bad if expectationOrderMatters is YES + if([stub matchesInvocation:anInvocation]) + break; + } + // Retain the stub in case it ends up being removed from stubs and expectations, since we still have to call handleInvocation on the stub at the end + [stub retain]; } - // Retain the stub in case it ends up being removed from stubs and expectations, since we still have to call handleInvocation on the stub at the end - [stub retain]; if(stub == nil) return NO; - - if([expectations containsObject:stub]) - { - OCMInvocationExpectation *expectation = [self _nextExpectedInvocation]; - if(expectationOrderMatters && (expectation != stub)) - { - [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@\n\texpected:\t%@", - [self description], [stub description], [[expectations objectAtIndex:0] description]]; - } - - // We can't check isSatisfied yet, since the stub won't be satisfied until we call handleInvocation:, and we don't want to call handleInvocation: yes for the reason in the comment above, since we'll still have the current expectation in the expectations array, which will cause an exception if expectationOrderMatters is YES and we're not ready for any future expected methods to be called yet - if(![(OCMInvocationExpectation *)stub isMatchAndReject]) - { - [expectations removeObject:stub]; - [stubs removeObject:stub]; - } - } - [stub handleInvocation:anInvocation]; - [stub release]; - - return YES; + + BOOL removeStub = NO; + @synchronized(expectations) + { + if([expectations containsObject:stub]) + { + OCMInvocationExpectation *expectation = [self _nextExpectedInvocation]; + if(expectationOrderMatters && (expectation != stub)) + { + [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@\n\texpected:\t%@", + [self description], [stub description], [[expectations objectAtIndex:0] description]]; + } + + // We can't check isSatisfied yet, since the stub won't be satisfied until we call handleInvocation:, and we don't want to call handleInvocation: yes for the reason in the comment above, since we'll still have the current expectation in the expectations array, which will cause an exception if expectationOrderMatters is YES and we're not ready for any future expected methods to be called yet + if(![(OCMInvocationExpectation *)stub isMatchAndReject]) + { + [expectations removeObject:stub]; + removeStub = YES; + } + } + } + if(removeStub) + { + @synchronized(stubs) + { + [stubs removeObject:stub]; + } + } + [stub handleInvocation:anInvocation]; + [stub release]; + + return YES; } - +// Must be synchronized on expectations when calling this method. - (OCMInvocationExpectation *)_nextExpectedInvocation { for(OCMInvocationExpectation *expectation in expectations) @@ -352,24 +400,36 @@ - (void)doesNotRecognizeSelector:(SEL)aSelector __unused - (NSString *)_stubDescriptions:(BOOL)onlyExpectations { NSMutableString *outputString = [NSMutableString string]; - for(OCMStubRecorder *stub in stubs) + NSArray *stubsCopy = nil; + @synchronized(stubs) { + stubsCopy = [stubs copy]; + } + for(OCMStubRecorder *stub in stubsCopy) + { + BOOL expectationsContainStub = NO; + @synchronized(expectations) + { + expectationsContainStub = [expectations containsObject:stub]; + } + NSString *prefix = @""; if(onlyExpectations) { - if([expectations containsObject:stub] == NO) + if(expectationsContainStub == NO) continue; } else { - if([expectations containsObject:stub]) + if(expectationsContainStub) prefix = @"expected:\t"; else prefix = @"stubbed:\t"; } [outputString appendFormat:@"\n\t%@%@", prefix, [stub description]]; } + [stubsCopy release]; return outputString; } diff --git a/Source/OCMock/OCObserverMockObject.m b/Source/OCMock/OCObserverMockObject.m index c764ccd4..03db0d34 100644 --- a/Source/OCMock/OCObserverMockObject.m +++ b/Source/OCMock/OCObserverMockObject.m @@ -61,7 +61,10 @@ - (void)setExpectationOrderMatters:(BOOL)flag - (void)autoRemoveFromCenter:(NSNotificationCenter *)aCenter { - [centers addObject:aCenter]; + @synchronized(centers) + { + [centers addObject:aCenter]; + } } @@ -70,7 +73,10 @@ - (void)autoRemoveFromCenter:(NSNotificationCenter *)aCenter - (id)expect { OCMObserverRecorder *recorder = [[[OCMObserverRecorder alloc] init] autorelease]; - [recorders addObject:recorder]; + @synchronized(recorders) + { + [recorders addObject:recorder]; + } return recorder; } @@ -81,17 +87,20 @@ - (void)verify - (void)verifyAtLocation:(OCMLocation *)location { - if([recorders count] == 1) + @synchronized(recorders) { - NSString *description = [NSString stringWithFormat:@"%@: expected notification was not observed: %@", - [self description], [[recorders lastObject] description]]; - OCMReportFailure(location, description); - } - else if([recorders count] > 0) - { - NSString *description = [NSString stringWithFormat:@"%@ : %@ expected notifications were not observed.", - [self description], @([recorders count])]; - OCMReportFailure(location, description); + if([recorders count] == 1) + { + NSString *description = [NSString stringWithFormat:@"%@: expected notification was not observed: %@", + [self description], [[recorders lastObject] description]]; + OCMReportFailure(location, description); + } + else if([recorders count] > 0) + { + NSString *description = [NSString stringWithFormat:@"%@ : %@ expected notifications were not observed.", + [self description], @([recorders count])]; + OCMReportFailure(location, description); + } } } @@ -108,17 +117,20 @@ - (NSNotification *)notificationWithName:(NSString *)name object:(id)sender - (void)handleNotification:(NSNotification *)aNotification { - NSUInteger i, limit; - - limit = expectationOrderMatters ? 1 : [recorders count]; - for(i = 0; i < limit; i++) - { - if([[recorders objectAtIndex:i] matchesNotification:aNotification]) - { - [recorders removeObjectAtIndex:i]; - return; - } - } + @synchronized(recorders) + { + NSUInteger i, limit; + + limit = expectationOrderMatters ? 1 : [recorders count]; + for(i = 0; i < limit; i++) + { + if([[recorders objectAtIndex:i] matchesNotification:aNotification]) + { + [recorders removeObjectAtIndex:i]; + return; + } + } + } [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected notification observed: %@", [self description], [aNotification description]]; } diff --git a/Source/OCMock/OCPartialMockObject.m b/Source/OCMock/OCPartialMockObject.m index 7bce03db..991e994f 100644 --- a/Source/OCMock/OCPartialMockObject.m +++ b/Source/OCMock/OCPartialMockObject.m @@ -129,7 +129,7 @@ - (void)prepareObjectForInstanceMethodMocking /* Adding forwarder for most instance methods to allow for verify after run */ NSArray *methodBlackList = @[@"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", - @"allowsWeakReference", @"retainWeakReference", @"isBlock"]; + @"allowsWeakReference", @"retainWeakReference", @"isBlock", @"retainCount", @"retain", @"release", @"autorelease"]; [NSObject enumerateMethodsInClass:mockedClass usingBlock:^(Class cls, SEL sel) { if((cls == [NSObject class]) || (cls == [NSProxy class])) return; diff --git a/Source/OCMockTests/OCMockObjectPartialMocksTests.m b/Source/OCMockTests/OCMockObjectPartialMocksTests.m index 92f931f8..dec7d22a 100644 --- a/Source/OCMockTests/OCMockObjectPartialMocksTests.m +++ b/Source/OCMockTests/OCMockObjectPartialMocksTests.m @@ -17,6 +17,7 @@ #import #import #import +#import "TestClassWithCustomReferenceCounting.h" #if TARGET_OS_IPHONE #define NSRect CGRect @@ -264,6 +265,16 @@ - (void)testRefusesToCreatePartialMockForNilObject XCTAssertThrows(OCMPartialMock(nil)); } +- (void)testPartialMockOfCustomReferenceCountingObject +{ + /* The point of using an object that implements its own reference counting methods is to force + -retain to be called even though the test is compiled with ARC. (Normally ARC does some magic + that bypasses dispatching to -retain.) Issue #245 turned up a recursive crash when partial + mocks used a forwarder for -retain. */ + TestClassWithCustomReferenceCounting *realObject = [TestClassWithCustomReferenceCounting new]; + id partialMock = OCMPartialMock(realObject); + XCTAssertNotNil(partialMock); +} #pragma mark Tests for KVO interaction with mocks diff --git a/Source/OCMockTests/TestClassWithCustomReferenceCounting.h b/Source/OCMockTests/TestClassWithCustomReferenceCounting.h new file mode 100644 index 00000000..20c8ac48 --- /dev/null +++ b/Source/OCMockTests/TestClassWithCustomReferenceCounting.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2015 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface TestClassWithCustomReferenceCounting : NSObject +@end diff --git a/Source/OCMockTests/TestClassWithCustomReferenceCounting.m b/Source/OCMockTests/TestClassWithCustomReferenceCounting.m new file mode 100644 index 00000000..08c1c529 --- /dev/null +++ b/Source/OCMockTests/TestClassWithCustomReferenceCounting.m @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "TestClassWithCustomReferenceCounting.h" + +@implementation TestClassWithCustomReferenceCounting +{ +#if __LP64__ + int64_t retainCount; +#else + int32_t retainCount; +#endif +} + +- (NSUInteger)retainCount +{ + return retainCount + 1; +} + +- (instancetype)retain +{ +#if __LP64__ + OSAtomicIncrement64(&retainCount); +#else + OSAtomicIncrement32(&retainCount); +#endif + return self; +} + +- (oneway void)release +{ +#if __LP64__ + int64_t newRetainCount = OSAtomicDecrement64(&retainCount); +#else + int32_t newRetainCount = OSAtomicDecrement32(&retainCount); +#endif + if (newRetainCount == -1) + [self dealloc]; +} + +@end