diff --git a/Source/OCMock.xcodeproj/project.pbxproj b/Source/OCMock.xcodeproj/project.pbxproj index 1056a514..b455a5d7 100644 --- a/Source/OCMock.xcodeproj/project.pbxproj +++ b/Source/OCMock.xcodeproj/project.pbxproj @@ -7,8 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 031E50581BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 031E50571BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m */; }; - 031E50591BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 031E50571BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m */; }; 0322DA65191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0322DA64191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m */; }; 0322DA66191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0322DA64191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m */; }; 0322DA6919118B4600CACAF1 /* OCMVerifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0322DA6719118B4600CACAF1 /* OCMVerifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -477,7 +475,6 @@ 030EF0B814632FD000B04273 /* OCMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCMock.h; sourceTree = ""; }; 030EF0DC14632FF700B04273 /* libOCMock.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libOCMock.a; sourceTree = BUILT_PRODUCTS_DIR; }; 030EF0E114632FF700B04273 /* OCMockLib-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCMockLib-Prefix.pch"; sourceTree = ""; }; - 031E50571BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBoxedReturnValueProviderTests.m; sourceTree = ""; }; 0322DA64191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectVerifyAfterRunTests.m; sourceTree = ""; }; 0322DA6719118B4600CACAF1 /* OCMVerifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMVerifier.h; sourceTree = ""; }; 0322DA6819118B4600CACAF1 /* OCMVerifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMVerifier.m; sourceTree = ""; }; @@ -765,7 +762,6 @@ 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */, 03B316271463350E0052CD09 /* OCMStubRecorderTests.m */, 037ECD5318FAD84100AF0E4C /* OCMInvocationMatcherTests.m */, - 031E50571BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m */, 03B316211463350E0052CD09 /* OCMConstraintTests.m */, 8B11D4B62448E2E900247BE2 /* OCMCPlusPlus98Tests.mm */, 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */, @@ -1544,7 +1540,6 @@ 8BF73E53246CA75E00B9A52C /* OCMNoEscapeBlockTests.m in Sources */, 8B11D4B72448E2E900247BE2 /* OCMCPlusPlus98Tests.mm in Sources */, 2FA28AB33F01A7D980F2C705 /* OCMockObjectDynamicPropertyMockingTests.m in Sources */, - 031E50581BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1637,7 +1632,6 @@ buildActionMask = 2147483647; files = ( 3C76716C1BB3EBC500FDC9F4 /* TestClassWithCustomReferenceCounting.m in Sources */, - 031E50591BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */, 8B3786A924E5BD6400FD1B5B /* OCMFunctionsTests.m in Sources */, 03C9CA1E18F05A84006DF94D /* OCMArgTests.m in Sources */, 036865651D3571A8005E6BEE /* OCMQuantifierTests.m in Sources */, diff --git a/Source/OCMock/NSMethodSignature+OCMAdditions.h b/Source/OCMock/NSMethodSignature+OCMAdditions.h index 6049cae5..b73e696d 100644 --- a/Source/OCMock/NSMethodSignature+OCMAdditions.h +++ b/Source/OCMock/NSMethodSignature+OCMAdditions.h @@ -26,4 +26,8 @@ - (NSString *)fullTypeString; - (const char *)fullObjCTypes; +// True if the return type of the method is "compatible" with the valueType and value. +// Compatible is defined the same as `OCMIsObjCTypeCompatibleWithValueType`. +- (BOOL)isMethodReturnTypeCompatibleWithValueType:(const char *)valueType value:(const void *)value valueSize:(size_t)valueSize; + @end diff --git a/Source/OCMock/NSMethodSignature+OCMAdditions.m b/Source/OCMock/NSMethodSignature+OCMAdditions.m index a3dc24d1..99f4a1df 100644 --- a/Source/OCMock/NSMethodSignature+OCMAdditions.m +++ b/Source/OCMock/NSMethodSignature+OCMAdditions.m @@ -178,4 +178,9 @@ - (const char *)fullObjCTypes return [[self fullTypeString] UTF8String]; } +- (BOOL)isMethodReturnTypeCompatibleWithValueType:(const char *)valueType value:(const void *)value valueSize:(size_t)valueSize +{ + return OCMIsObjCTypeCompatibleWithValueType([self methodReturnType], valueType, value, valueSize); +} + @end diff --git a/Source/OCMock/OCMBlockCaller.h b/Source/OCMock/OCMBlockCaller.h index d5c16512..1153e1e7 100644 --- a/Source/OCMock/OCMBlockCaller.h +++ b/Source/OCMock/OCMBlockCaller.h @@ -18,10 +18,24 @@ @interface OCMBlockCaller : NSObject { - void (^block)(NSInvocation *); + id block; } -- (id)initWithCallBlock:(void (^)(NSInvocation *))theBlock; +/* + * Call blocks can have one of four types: + * a) A simple block ^{ NSLog(@"hi"); }. + * b) The new style ^(id localSelf, type0 arg0, type1 arg1) { ... } + * where types and args match the arguments passed to the selector we are + * stubbing. + * c) The deprecated style ^(NSInvocation *anInvocation) { ... }. This case + * cannot have a return value. If a return value is desired it must be set + * on `anInvocation`. + * d) nil + * + * If it is (a) or (b) and there is a return value it must match the return type + * of the selector. If there is no return value then the method will return 0. + */ +- (id)initWithCallBlock:(id)theBlock; - (void)handleInvocation:(NSInvocation *)anInvocation; diff --git a/Source/OCMock/OCMBlockCaller.m b/Source/OCMock/OCMBlockCaller.m index b83f3a00..70533d28 100644 --- a/Source/OCMock/OCMBlockCaller.m +++ b/Source/OCMock/OCMBlockCaller.m @@ -15,11 +15,13 @@ */ #import "OCMBlockCaller.h" - +#import "NSMethodSignature+OCMAdditions.h" +#import "OCMFunctionsPrivate.h" +#import "NSInvocation+OCMAdditions.h" @implementation OCMBlockCaller -- (id)initWithCallBlock:(void (^)(NSInvocation *))theBlock +-(id)initWithCallBlock:(id)theBlock { if((self = [super init])) { @@ -37,9 +39,79 @@ - (void)dealloc - (void)handleInvocation:(NSInvocation *)anInvocation { - if(block != nil) + if(!block) + { + return; + } + NSMethodSignature *blockMethodSignature = [NSMethodSignature signatureForBlock:block]; + NSUInteger blockNumberOfArguments = [blockMethodSignature numberOfArguments]; + if(blockNumberOfArguments == 2 && strcmp([blockMethodSignature getArgumentTypeAtIndex:1], "@\"NSInvocation\"") == 0) + { + // This is the deprecated ^(NSInvocation *) {} case. + if([blockMethodSignature methodReturnLength] != 0) + { + [NSException raise:NSInvalidArgumentException format:@"NSInvocation style `andDo:` block for `-%@` cannot have return value.", NSStringFromSelector([anInvocation selector])]; + } + + void (^theBlock)(NSInvocation *) = block; + theBlock(anInvocation); + NSLog(@"Warning: Replace `^(NSInvocation *invocation) { ... }` with `%@`.", OCMBlockDeclarationForInvocation(anInvocation)); + return; + } + + // This handles both the ^{} case and the ^(SelfType *a, Arg1Type b, ...) case. + NSMethodSignature *invocationMethodSignature = [anInvocation methodSignature]; + NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:blockMethodSignature]; + NSUInteger invocationNumberOfArguments = [invocationMethodSignature numberOfArguments]; + if(blockNumberOfArguments != 1 && blockNumberOfArguments != invocationNumberOfArguments) + { + [NSException raise:NSInvalidArgumentException format:@"Block style `andDo:` block signature has wrong number of arguments. %d vs %d", (int)invocationNumberOfArguments, (int)blockNumberOfArguments]; + } + id target = [anInvocation target]; + + // In the ^{} case, blockNumberOfArguments will be 1, so we will just skip the whole for block. + for(NSUInteger argIndex = 1; argIndex < blockNumberOfArguments; ++argIndex) + { + // Set arg1 to be "localSelf". + // Note that in a standard NSInvocation this would be SEL, but blocks don't have SEL args. + if(argIndex == 1) + { + [blockInvocation setArgument:&target atIndex:1]; + continue; + } + const char *blockArgType = [blockMethodSignature getArgumentTypeAtIndex:argIndex]; + const char *invocationArgType = [invocationMethodSignature getArgumentTypeAtIndex:argIndex]; + NSUInteger argSize; + NSGetSizeAndAlignment(blockArgType, &argSize, NULL); + NSMutableData *argSpace = [NSMutableData dataWithLength:argSize]; + void *argBytes = [argSpace mutableBytes]; + [anInvocation getArgument:argBytes atIndex:argIndex]; + if(!OCMIsObjCTypeCompatibleWithValueType(invocationArgType, blockArgType, argBytes, argSize) && !OCMEqualTypesAllowingOpaqueStructs(blockArgType, invocationArgType)) + { + [NSException raise:NSInvalidArgumentException format:@"Block style `andDo:` block signature does not match selector signature. Arg %d is `%@` vs `%@`.", (int)argIndex, OCMObjCTypeForArgumentType(blockArgType), OCMObjCTypeForArgumentType(invocationArgType)]; + } + [blockInvocation setArgument:argBytes atIndex:argIndex]; + } + [blockInvocation invokeWithTarget:block]; + NSUInteger blockReturnLength = [blockMethodSignature methodReturnLength]; + if(blockReturnLength > 0) { - block(anInvocation); + // If there is a return value, make sure that it matches the expected return type. + const char *blockReturnType = [blockMethodSignature methodReturnType]; + NSUInteger invocationReturnLength = [invocationMethodSignature methodReturnLength]; + const char *invocationReturnType = [invocationMethodSignature methodReturnType]; + if(invocationReturnLength != blockReturnLength) + { + [NSException raise:NSInvalidArgumentException format:@"Block style `andDo:` block signature does not match selector signature. Return type is `%@` vs `%@`.", OCMObjCTypeForArgumentType(blockReturnType), OCMObjCTypeForArgumentType(invocationReturnType)]; + } + NSMutableData *argSpace = [NSMutableData dataWithLength:invocationReturnLength]; + void *argBytes = [argSpace mutableBytes]; + [blockInvocation getReturnValue:argBytes]; + if(!OCMIsObjCTypeCompatibleWithValueType(invocationReturnType, blockReturnType, argBytes, invocationReturnLength) && !OCMEqualTypesAllowingOpaqueStructs(blockReturnType, invocationReturnType)) + { + [NSException raise:NSInvalidArgumentException format:@"Block style `andDo:` block signature does not match selector signature. Return type is `%@` vs `%@`.", OCMObjCTypeForArgumentType(blockReturnType), OCMObjCTypeForArgumentType(invocationReturnType)]; + } + [anInvocation setReturnValue:argBytes]; } } diff --git a/Source/OCMock/OCMBoxedReturnValueProvider.m b/Source/OCMock/OCMBoxedReturnValueProvider.m index 2907f49a..1b997aa8 100644 --- a/Source/OCMock/OCMBoxedReturnValueProvider.m +++ b/Source/OCMock/OCMBoxedReturnValueProvider.m @@ -16,8 +16,7 @@ #import "OCMBoxedReturnValueProvider.h" #import "NSValue+OCMAdditions.h" -#import "OCMFunctionsPrivate.h" - +#import "NSMethodSignature+OCMAdditions.h" @implementation OCMBoxedReturnValueProvider @@ -28,11 +27,11 @@ - (void)handleInvocation:(NSInvocation *)anInvocation NSGetSizeAndAlignment([returnValueAsNSValue objCType], &valueSize, NULL); char valueBuffer[valueSize]; [returnValueAsNSValue getValue:valueBuffer]; + NSMethodSignature *signature = [anInvocation methodSignature]; + const char *returnType = [signature methodReturnType]; + const char *returnValueType = [returnValueAsNSValue objCType]; - const char *returnType = [[anInvocation methodSignature] methodReturnType]; - - if([self isMethodReturnType:returnType compatibleWithValueType:[returnValueAsNSValue objCType] - value:valueBuffer valueSize:valueSize]) + if([signature isMethodReturnTypeCompatibleWithValueType:returnValueType value:valueBuffer valueSize:valueSize]) { [anInvocation setReturnValue:valueBuffer]; } @@ -43,21 +42,8 @@ - (void)handleInvocation:(NSInvocation *)anInvocation else { [NSException raise:NSInvalidArgumentException - format:@"Return value cannot be used for method; method signature declares '%s' but value is '%s'.", returnType, [returnValueAsNSValue objCType]]; + format:@"Return value cannot be used for method; method signature declares '%s' but value is '%s'.", returnType, returnValueType]; } } -- (BOOL)isMethodReturnType:(const char *)returnType compatibleWithValueType:(const char *)valueType value:(const void *)value valueSize:(size_t)valueSize -{ - /* Same types are obviously compatible */ - if(strcmp(returnType, valueType) == 0) - return YES; - - /* Special treatment for nil and Nil */ - if(strcmp(returnType, @encode(id)) == 0 || strcmp(returnType, @encode(Class)) == 0) - return OCMIsNilValue(valueType, value, valueSize); - - return OCMEqualTypesAllowingOpaqueStructs(returnType, valueType); -} - @end diff --git a/Source/OCMock/OCMFunctions.m b/Source/OCMock/OCMFunctions.m index eb8465f6..9b9ac910 100644 --- a/Source/OCMock/OCMFunctions.m +++ b/Source/OCMock/OCMFunctions.m @@ -21,8 +21,8 @@ #import "OCClassMockObject.h" #import "OCMFunctionsPrivate.h" #import "OCMLocation.h" -#import "OCPartialMockObject.h" - +#import "OCProtocolMockObject.h" +#import "NSInvocation+OCMAdditions.h" #pragma mark Known private API @@ -512,3 +512,329 @@ void OCMReportFailure(OCMLocation *loc, NSString *description) [[NSException exceptionWithName:@"OCMockTestFailure" reason:description userInfo:nil] raise]; } } + +NSString *OCMObjCTypeForArgumentType(const char *argType) +{ + switch(argType[0]) + { + case '@': return @"id"; + case 'B': return @"BOOL"; + case 'c': return @"char"; + case 'C': return @"unsigned char"; + case 's': return @"short"; + case 'S': return @"unsigned short"; +#ifdef __LP64__ + case 'i': return @"int"; + case 'I': return @"unsigned int"; + case 'l': return @"NSInteger"; + case 'L': return @"NSUInteger"; + case 'q': return @"NSInteger"; + case 'Q': return @"NSUInteger"; +#else + case 'i': return @"NSInteger"; + case 'I': return @"NSUInteger"; + case 'l': return @"NSInteger"; + case 'L': return @"NSUInteger"; + case 'q': return @"long long"; + case 'Q': return @"unsigned long long"; +#endif + case 'd': return @"double"; + case 'f': return @"float"; + case 'D': return @"long double"; + case '{': return @"struct"; + case '^': + { + NSString *type = OCMObjCTypeForArgumentType(&argType[1]); + return [type stringByAppendingString:@"*"]; + } + case '*': return @"char *"; + case 'v': return @"void"; + case '#': return @"Class"; + case ':': return @"SEL"; + default: return @""; // avoid confusion with trigraphs... + } +} + +BOOL OCMIsObjCTypeCompatibleWithValueType(const char *objcType, const char *valueType, const void *value, size_t valueSize) +{ + /* Same types are obviously compatible */ + if(strcmp(objcType, valueType) == 0) + return YES; + + /* Object types are deemed compatible with other objects or nil/Nil */ + if(OCMIsObjectType(objcType)) + { + return OCMIsObjectType(valueType) || OCMIsNilValue(valueType, value, valueSize); + } + + return OCMEqualTypesAllowingOpaqueStructs(objcType, valueType); +} + +NSArray *OCMSplitSelectorSegmentIntoWords(NSString *string) +{ + // Breaks up a camelcase string fooIsBarIsBam into individual words [foo, Is, Bar, Is, Bam] + // Note attempted special case at plurals (words ending with 's' only) + string = [string stringByReplacingOccurrencesOfString:@"([A-Z](?=[A-Z][a-rt-z][a-z0-9]{0,1})|[^A-Z](?=[A-Z])|[a-zA-Z](?=[^a-zA-Z0-9]))" + withString:@"$1 " + options:NSRegularExpressionSearch + range:NSMakeRange(0, [string length])]; + return [string componentsSeparatedByString:@" "]; +} + +static NSString *OCMParameterNameFromWords(NSArray *words) +{ + NSString *name = [words componentsJoinedByString:@""]; + NSUInteger length = [name length]; + if(length == 0) + { + return nil; + } + BOOL lower = length == 1; + if(!lower) + { + unichar secondChar = [name characterAtIndex:1]; + lower = secondChar >= 'a' && secondChar <= 'z'; + } + if(lower) + { + return [NSString stringWithFormat:@"%@%@", [[name substringToIndex:1] lowercaseString], [name substringFromIndex:1]]; + } + return name; + +} + +NSString *OCMTurnSelectorSegmentIntoParameterName(NSString *selectorSegment) +{ + // Trim off any foo_ prefixes for categories and other odd things. + // So `gtm_updateWithFoo` becomes `updateWithFoo`. + NSRange underscoreRange = [selectorSegment rangeOfString:@"_" options:NSBackwardsSearch]; + if(underscoreRange.length != 0) + { + selectorSegment = [selectorSegment substringFromIndex:underscoreRange.location + 1]; + } + NSArray *originalSubStrings = OCMSplitSelectorSegmentIntoWords(selectorSegment); + NSUInteger count = [originalSubStrings count]; + if(count == 0) + { + return nil; + } + NSString *firstItem = [originalSubStrings firstObject]; + if(count == 1) + { + return firstItem; + } + // In the case we have a selector segment like `atIndex:`, remove the `prefix` + NSArray *subStrings = originalSubStrings; + NSArray *prefixesToRemove = @[ @"with", @"from", @"and", @"set", @"for", @"get", @"at", @"of", @"add", @"replace", @"remove", @"to", @"on", @"perform" ]; + if([prefixesToRemove containsObject:firstItem]) + { + subStrings = [subStrings subarrayWithRange:NSMakeRange(1, [subStrings count] - 1)]; + } + if([subStrings count] == 1) + { + return OCMParameterNameFromWords(subStrings); + } + + // In the case we have textViewWillUpdate (very common for delegate patterns) break off the WillUpdate. + NSArray *prefixSplitWords = @[ @"Did", @"Will" ]; + NSUInteger index = [subStrings indexOfObjectWithOptions:0 passingTest:^BOOL(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + return [prefixSplitWords containsObject:obj]; + }]; + if(index != NSNotFound) + { + subStrings = [subStrings subarrayWithRange:NSMakeRange(0, index)]; + } + + // In the case we have `updateWithFoo` we usually only want the subject (in this case `Foo` + NSArray *suffixSplitWords = @[ @"And", @"With", @"Of", @"At", @"By", @"For", @"From", @"Mutable", @"To", @"That", @"In", @"On", @"Perform" ]; + index = [subStrings indexOfObjectWithOptions:NSEnumerationReverse passingTest:^BOOL(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + return [suffixSplitWords containsObject:obj]; + }]; + if(index != NSNotFound) + { + subStrings = [subStrings subarrayWithRange:NSMakeRange(index + 1, [subStrings count] - (index + 1))]; + } + + if([subStrings count] == 0) + { + subStrings = originalSubStrings; + } + else if([subStrings count] == 1) + { + return OCMParameterNameFromWords(subStrings); + } + + // Finally if we have `updateWithExtendedViewRect` we usually chop it down to two words that + // are usually an adjective and the subject (so viewRect). + NSCAssert([subStrings count] > 1, @"Substrings count too low: `%@`", subStrings); + subStrings = [subStrings subarrayWithRange:NSMakeRange([subStrings count] - 2, 2)]; + return OCMParameterNameFromWords(subStrings); +} + +NSArray *OCMParameterNamesFromSelector(SEL selector) +{ + NSArray *selStrings = [NSStringFromSelector(selector) componentsSeparatedByString:@":"]; + NSUInteger count = [selStrings count]; + if(count <= 1) + { + return nil; + } + NSMutableArray *paramStrings = [NSMutableArray arrayWithCapacity:count]; + NSMutableDictionary *argMap = [NSMutableDictionary dictionaryWithCapacity:count]; + [selStrings enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if(idx == count - 1) + { + return; + } + NSString *paramName = OCMTurnSelectorSegmentIntoParameterName(obj); + if([paramName length] == 0) + { + paramName = @"arg"; + } + NSNumber *number = argMap[paramName]; + if(!number) + { + argMap[paramName] = @0; + } + else + { + if([number isEqual:@0]) + { + NSString *newName = [paramName stringByAppendingString:@"0"]; + [paramStrings replaceObjectAtIndex:[paramStrings indexOfObject:paramName] withObject:newName]; + } + number = @([number intValue] + 1); + argMap[paramName] = number; + paramName = [paramName stringByAppendingString:[number stringValue]]; + } + [paramStrings addObject:paramName]; + }]; + return paramStrings; +} + +NSString *OCMTypeOfObject(id obj) +{ + @try + { + if(!obj) + { + return @"id"; + } + if(object_isClass(obj)) + { + return @"Class"; + } + Class class = [obj class]; + if(class == [OCProtocolMockObject class]) + { + return [NSString stringWithFormat:@"id<%@>", NSStringFromProtocol([(OCProtocolMockObject *)obj mockedProtocol])]; + } + if(OCMIsBlock(obj)) + { + return @"BlockType"; + } + + NSDictionary, NSString *> *kindOfClassMap = @{ + (id)[NSArray class]: @"NSArray *", + (id)[NSAttributedString class]: @"NSAttributedString *", + (id)[NSData class]: @"NSData *", + (id)[NSDate class]: @"NSDate *", + (id)[NSDictionary class]: @"NSDictionary *", + (id)[NSNumber class]: @"NSNumber *", + (id)[NSOrderedSet class]: @"NSOrderedSet *", + (id)[NSSet class]: @"NSSet *", + (id)[NSString class]: @"NSString *", + (id)[NSTimer class]: @"NSTimer *", + (id)[NSTimeZone class]: @"NSTimeZone *", + (id)[NSURLRequest class]: @"NSURLRequest *", + }; + for(Class key in kindOfClassMap) + { + if([obj isKindOfClass:key]) + { + return kindOfClassMap[(id)key]; + } + } + NSDictionary *conformsToProtocolMap = @{ + @"OS_dispatch_queue": @"dispatch_queue_t", + @"OS_dispatch_data": @"dispatch_data_t", + @"OS_dispatch_group": @"dispatch_group_t", + @"OS_dispatch_io": @"dispatch_io_t", + @"OS_dispatch_queue_attr": @"dispatch_queue_attr_t", + @"OS_dispatch_semaphore": @"dispatch_semaphore_t", + @"OS_dispatch_source": @"dispatch_source_t", + }; + for(NSString *key in conformsToProtocolMap) + { + Protocol *protocol = NSProtocolFromString(key); + NSCAssert(protocol, @"Unknown protocol %@", key); + if([obj conformsToProtocol:protocol]) + { + return conformsToProtocolMap[key]; + } + } + return [NSStringFromClass(class) stringByAppendingString:@" *"]; + } + @catch(...) { + // Our attempt at introspection caused an exception to be thrown (occurs sometimes with weird + // NSProxy set ups). Just return the most generic thing possible, and let the developer sort + // this out. + return @"id"; + } + } + +static NSString *OCMArgSeparator(NSString *arg) +{ + NSUInteger length = [arg length]; + if(length > 0 && [arg characterAtIndex:length - 1] == '*') + { + // Pointers don't get a space after them. + return @""; + } + return @" "; +} + +NSString *OCMBlockDeclarationForInvocation(NSInvocation *invocation) +{ + NSMethodSignature *methodSignature = [invocation methodSignature]; + NSUInteger numberOfArgs = [methodSignature numberOfArguments]; + id target = [invocation target]; + NSString *localSelfType = OCMTypeOfObject(target); + const char *returnType = [methodSignature methodReturnType]; + NSString *convertedReturnType; + if([invocation methodIsInCreateFamily] || [invocation methodIsInInitFamily]) + { + convertedReturnType = @"id"; + } + else if(OCMIsObjectType(returnType)) + { + id value; + [invocation getReturnValue:&value]; + convertedReturnType = OCMTypeOfObject(value); + } + else + { + convertedReturnType = OCMObjCTypeForArgumentType(returnType); + } + NSMutableString *declaration = [NSMutableString stringWithFormat:@"^%@(%@%@localSelf", convertedReturnType, localSelfType, OCMArgSeparator(localSelfType)]; + NSArray *parts = OCMParameterNamesFromSelector([invocation selector]); + for(NSUInteger i = 2; i < numberOfArgs; i++) + { + const char *argType = [methodSignature getArgumentTypeAtIndex:i]; + NSString *argName = parts[i - 2]; + NSString *convertedArgType; + if(OCMIsObjectType(argType)) + { + id value; + [invocation getArgument:&value atIndex:i]; + convertedArgType = OCMTypeOfObject(value); + } + else + { + convertedArgType = OCMObjCTypeForArgumentType(argType); + } + [declaration appendFormat:@", %@%@%@", convertedArgType, OCMArgSeparator(convertedArgType), argName]; + } + [declaration appendFormat:@") { %@ }", [methodSignature methodReturnLength] > 0 ? @"return ..." : @"..."]; + return declaration; +} diff --git a/Source/OCMock/OCMFunctionsPrivate.h b/Source/OCMock/OCMFunctionsPrivate.h index d895c3a4..60e319ea 100644 --- a/Source/OCMock/OCMFunctionsPrivate.h +++ b/Source/OCMock/OCMFunctionsPrivate.h @@ -51,6 +51,8 @@ void OCMReportFailure(OCMLocation *loc, NSString *description); BOOL OCMIsBlock(id potentialBlock); BOOL OCMIsNonEscapingBlock(id block); +NSString *OCMObjCTypeForArgumentType(const char *argType); +BOOL OCMIsObjCTypeCompatibleWithValueType(const char *objcType, const char *valueType, const void *value, size_t valueSize); struct OCMBlockDef { @@ -75,3 +77,43 @@ enum OCMBlockDescriptionFlagsHasCopyDispose = (1 << 25), OCMBlockDescriptionFlagsHasSignature = (1 << 30) }; + +/* + * This attempts to generate a decent replacement string for old style ^(NSInvocation *) {} blocks. + * It analyzes the method signature and the arguments passed to the invocation to attempt to + * determine argument types and good names for the arguments. + */ +NSString *OCMBlockDeclarationForInvocation(NSInvocation *invocation); + +/* + * Return a suggested list of parameter names for a selector. + * For example if the selector was `initWithName:forDog:` it would return `@[ @"name", @"dog" ]`. + * If a good name cannot be determined, it will be `arg`. If there are multiple names that are the + * same, they will be suffixed with a number, so selector `initWithThings::::` would return + * `@[ @"things", @"arg0", @"arg1", @"arg2" ]` + * Note this all works on heuristics based on standard Apple naming conventions. It doesn't + * guarantee perfection. + */ +NSArray *OCMParameterNamesFromSelector(SEL selector); + +/* + * Given a selector segment like `initWithName`, splits it into words: `@[ @"init", @"With", @"Name" ]` + * Normally splits on capital letters, but attemps to keep acronyms together, and plurals correct. + * So `initWithURLsStartingWithHTTPAssumingTheyAreWebBased:` becomes + * @[ @"init", @"With, @"URLs", @"Starting", @"With", @"HTTP", @"Assuming", @"They", @"Are", @"Web", @"Based"] + * Doesn't have to be perfect, as it is a best effort to generate good API names. + */ +NSArray *OCMSplitSelectorSegmentIntoWords(NSString *string); + +/* + * Given a selector segment such as `initWithBigName:` attempts to deduce an + * Objective C style parameter name for the segment (in this case `bigName`). + * If it can't deal with it, returns an empty string. + */ +NSString *OCMTurnSelectorSegmentIntoParameterName(NSString *); + +/* + * Attempts to deduce a type for `obj` that could be put in a declaration + * for a method containing obj. This is a best effort. + */ +NSString *OCMTypeOfObject(id obj); diff --git a/Source/OCMock/OCMInvocationStub.m b/Source/OCMock/OCMInvocationStub.m index f418ec90..52fb6a98 100644 --- a/Source/OCMock/OCMInvocationStub.m +++ b/Source/OCMock/OCMInvocationStub.m @@ -19,8 +19,6 @@ #import "OCMArg.h" #import "OCMArgAction.h" -#define UNSET_RETURN_VALUE_MARKER ((id)0x01234567) - @implementation OCMInvocationStub @@ -58,13 +56,18 @@ - (void)handleInvocation:(NSInvocation *)anInvocation BOOL isInCreateFamily = isInInitFamily ? NO : [anInvocation methodIsInCreateFamily]; if(isInInitFamily || isInCreateFamily) { - id returnVal = UNSET_RETURN_VALUE_MARKER; - [anInvocation setReturnValue:&returnVal]; + // Put a unique value in the invocation so that we can verify that the invocation + // changed it. + id unsetReturnMarker = [NSUUID UUID]; + [anInvocation setReturnValue:&unsetReturnMarker]; [self invokeActionsForInvocation:anInvocation]; + id returnVal; [anInvocation getReturnValue:&returnVal]; - if(returnVal == UNSET_RETURN_VALUE_MARKER) + + // Intentional Pointer comparison being done here. + if(returnVal == unsetReturnMarker) [NSException raise:NSInvalidArgumentException format:@"%@ was stubbed but no return value set. A return value is required for all alloc/copy/new/mutablecopy/init methods. If you intended to return nil, make this explicit with .andReturn(nil)", NSStringFromSelector([anInvocation selector])]; if(isInCreateFamily) diff --git a/Source/OCMock/OCMStubRecorder.h b/Source/OCMock/OCMStubRecorder.h index 383d9b45..a7fa23bc 100644 --- a/Source/OCMock/OCMStubRecorder.h +++ b/Source/OCMock/OCMStubRecorder.h @@ -30,7 +30,7 @@ - (id)andThrow:(NSException *)anException; - (id)andPost:(NSNotification *)aNotification; - (id)andCall:(SEL)selector onObject:(id)anObject; -- (id)andDo:(void (^)(NSInvocation *invocation))block; +- (id)andDo:(id)block; - (id)andForwardToRealObject; #if !TARGET_OS_WATCH @@ -61,8 +61,9 @@ #define andCall(anObject, aSelector) _andCall(anObject, aSelector) @property (nonatomic, readonly) OCMStubRecorder *(^ _andCall)(id, SEL); +// See [OCMBlockCaller initWithCallBlock] declaration for description of what `aBlock` can be. #define andDo(aBlock) _andDo(aBlock) -@property (nonatomic, readonly) OCMStubRecorder *(^ _andDo)(void (^)(NSInvocation *)); +@property (nonatomic, readonly) OCMStubRecorder *(^ _andDo)(id); #define andForwardToRealObject() _andForwardToRealObject() @property (nonatomic, readonly) OCMStubRecorder *(^ _andForwardToRealObject)(void); diff --git a/Source/OCMock/OCMStubRecorder.m b/Source/OCMock/OCMStubRecorder.m index 2c2448b1..cc1d2aa3 100644 --- a/Source/OCMock/OCMStubRecorder.m +++ b/Source/OCMock/OCMStubRecorder.m @@ -89,7 +89,7 @@ - (id)andCall:(SEL)selector onObject:(id)anObject return self; } -- (id)andDo:(void (^)(NSInvocation *))aBlock +- (id)andDo:(id)aBlock { [[self stub] addInvocationAction:[[[OCMBlockCaller alloc] initWithCallBlock:aBlock] autorelease]]; return self; @@ -179,12 +179,12 @@ @implementation OCMStubRecorder (Properties) @dynamic _andDo; -- (OCMStubRecorder * (^)(void (^)(NSInvocation *)))_andDo +- (OCMStubRecorder *(^)(id))_andDo { - id (^theBlock)(void (^)(NSInvocation *)) = ^(void (^blockToCall)(NSInvocation *)) { + id (^theBlock)(id) = ^ (id blockToCall) { return [self andDo:blockToCall]; }; - return (id)[[theBlock copy] autorelease]; + return [[theBlock copy] autorelease]; } diff --git a/Source/OCMock/OCProtocolMockObject.h b/Source/OCMock/OCProtocolMockObject.h index a6bac51f..c0ec57a0 100644 --- a/Source/OCMock/OCProtocolMockObject.h +++ b/Source/OCMock/OCProtocolMockObject.h @@ -23,4 +23,6 @@ - (id)initWithProtocol:(Protocol *)aProtocol; +- (Protocol *)mockedProtocol; + @end diff --git a/Source/OCMock/OCProtocolMockObject.m b/Source/OCMock/OCProtocolMockObject.m index 76f44f15..d49b2f2d 100644 --- a/Source/OCMock/OCProtocolMockObject.m +++ b/Source/OCMock/OCProtocolMockObject.m @@ -62,4 +62,10 @@ - (BOOL)respondsToSelector:(SEL)selector return ([self methodSignatureForSelector:selector] != nil); } + +- (Protocol *)mockedProtocol +{ + return mockedProtocol; +} + @end diff --git a/Source/OCMockTests/OCMFunctionsTests.m b/Source/OCMockTests/OCMFunctionsTests.m index 85f9f2c3..2a371a86 100644 --- a/Source/OCMockTests/OCMFunctionsTests.m +++ b/Source/OCMockTests/OCMFunctionsTests.m @@ -14,12 +14,15 @@ * under the License. */ -#import -#import #import +#import +#import "OCMockObject.h" +#import "OCMockMacros.h" #import "OCMFunctions.h" #import "OCMFunctionsPrivate.h" +#pragma mark Helper classes + @interface TestClassForFunctions : NSObject - (void)setFoo:(NSString *)aString; @end @@ -32,10 +35,78 @@ - (void)setFoo:(NSString *)aString; @end +// Exists solely to supply method signatures to InvocationRecorder. +@interface InvocationImplementor : NSObject +@end + +@implementation InvocationImplementor + +- (instancetype)foo_initWithCount:(NSNumber *)number ofObjectsInSet:(NSSet*)set +{ + return nil; +} + +- (instancetype)initWithCount:(NSNumber *)number ofObjectsInSet:(NSSet*)set +{ + return self; +} + +- (NSUInteger)numberFromNameString:(NSString *)nameString inEnglish:(BOOL)inEnglish +{ + return 0; +} + +- (void)performBlock:(id(^)(int))block onQueue:(dispatch_queue_t)queue +{ +} + +- (BOOL)stringWillBeginFormatting:(NSString *)string +{ + return NO; +} + +- (NSString *)stringDidEndFormatting:(NSString *)string +{ + return [NSMutableString string]; +} + +- (void)didFinishPlaybackAndWillInternallyTransitionToNextPlayback:(BOOL)value +{ +} + +@end + +// Records invocations. +@interface InvocationRecorder : NSProxy +@property NSMutableArray *invocations; +@property InvocationImplementor *implementor; +@end + +@implementation InvocationRecorder +- (instancetype)init +{ + _invocations = [NSMutableArray array]; + _implementor = [[InvocationImplementor alloc] init]; + return self; +} + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + [invocation retainArguments]; + [self.invocations addObject:invocation]; + [invocation invokeWithTarget:_implementor]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector +{ + return [self.implementor methodSignatureForSelector:selector]; +} +@end @interface OCMFunctionsTests : XCTestCase @end + @implementation OCMFunctionsTests - (void)testIsBlockReturnsFalseForClass @@ -93,4 +164,263 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N { } +- (void)testOCMIsBlock +{ + XCTAssertFalse(OCMIsBlock([NSString class])); + XCTAssertFalse(OCMIsBlock(@"")); + XCTAssertFalse(OCMIsBlock([NSString stringWithFormat:@"%d", 42])); + XCTAssertFalse(OCMIsBlock(nil)); + XCTAssertTrue(OCMIsBlock(^{})); +} + +- (void)testCorrectEqualityForCppProperty +{ + // see https://github.com/erikdoe/ocmock/issues/96 + const char *type1 = + "r^{GURL={basic_string, std::__1::alloca" + "tor >={__compressed_pair, std::__1::allocator >::__rep, std::__1::allocator >={__rep}}}B{Parsed={Component=ii}{Component=ii}{Component=ii}{Compo" + "nent=ii}{Component=ii}{Component=ii}{Component=ii}{Component=ii}^{Parsed}" + "}{scoped_ptr >={scoped_ptr_impl >={Data=^{GURL}}}}}"; + + const char *type2 = + "r^{GURL={basic_string, std::__1::alloca" + "tor >={__compressed_pair, std::__1::allocator >::__rep, std::__1::allocator >={__rep=(?={__long=II*}{__short=(?=Cc)[11c]}{__raw=[3L]})}}}B{Parse" + "d={Component=ii}{Component=ii}{Component=ii}{Component=ii}{Component=ii}{" + "Component=ii}{Component=ii}{Component=ii}^{Parsed}}{scoped_ptr >={scoped_ptr_impl >={Data=^{GURL}}}}}"; + + const char *type3 = + "r^{GURL}"; + + XCTAssertTrue(OCMIsObjCTypeCompatibleWithValueType(type1, type2, NULL, 0)); + XCTAssertTrue(OCMIsObjCTypeCompatibleWithValueType(type1, type3, NULL, 0)); + XCTAssertTrue(OCMIsObjCTypeCompatibleWithValueType(type2, type1, NULL, 0)); + XCTAssertTrue(OCMIsObjCTypeCompatibleWithValueType(type2, type3, NULL, 0)); + XCTAssertTrue(OCMIsObjCTypeCompatibleWithValueType(type3, type1, NULL, 0)); + XCTAssertTrue(OCMIsObjCTypeCompatibleWithValueType(type3, type2, NULL, 0)); +} + + +- (void)testCorrectEqualityForCppReturnTypesWithVtables +{ + // see https://github.com/erikdoe/ocmock/issues/247 + const char *type1 = + "^{S=^^?{basic_string, std::__1::allocat" + "or >={__compressed_pair, std::__1::allocator >::__rep, std::__1::allocator >={__rep}}}}"; + + const char *type2 = + "^{S=^^?{basic_string, std::__1::allocat" + "or >={__compressed_pair, std::__1::allocator >::__rep, std::__1::allocator >={__rep=(?={__long=QQ*}{__short=(?=Cc)[23c]}{__raw=[3Q]})}}}}"; + + XCTAssertTrue(OCMIsObjCTypeCompatibleWithValueType(type1, type2, NULL, 0)); +} + + +- (void)testCorrectEqualityForStructureWithUnknownName +{ + // see https://github.com/erikdoe/ocmock/issues/333 + const char *type1 = "{?=dd}"; + const char *type2 = "{CLLocationCoordinate2D=dd}"; + + XCTAssertTrue(OCMIsObjCTypeCompatibleWithValueType(type1, type2, NULL, 0)); +} + + +- (void)testCorrectEqualityForStructureWithoutName +{ + // see https://github.com/erikdoe/ocmock/issues/342 + const char *type1 = "r^{GURL={basic_string, std::__1::allocator >={__compressed_pair, std::__1::allocator >::__r" + "ep, std::__1::allocator >={__rep}}}B{Parsed={Component=ii}{Compo" + "nent=ii}{Component=ii}{Component=ii}{Component=ii}{Component=ii}{Compo" + "nent=ii}{Component=ii}B^{}}{unique_ptr >={__compressed_pair >=^{" + "}}}}"; + const char *type2 = "r^{GURL={basic_string, std::__1::allocator >={__compressed_pair, std::__1::allocator >::__r" + "ep, std::__1::allocator >={__rep=(?={__long=QQ*}{__short=(?=Cc)[" + "23c]}{__raw=[3Q]})}}}B{Parsed={Component=ii}{Component=ii}{Component=i" + "i}{Component=ii}{Component=ii}{Component=ii}{Component=ii}{Component=i" + "i}B^{Parsed}}{unique_ptr >={__com" + "pressed_pair >=^{GURL}}}}"; + + XCTAssertTrue(OCMIsObjCTypeCompatibleWithValueType(type1, type2, NULL, 0)); +} +- (void)testSplitSelectorSegmentIntoWords +{ + NSArray *array = OCMSplitSelectorSegmentIntoWords(@"ASongAboutThe26ABCsIsOfTheEssenceButAPersonalIDCardForUser456InRoom26ContainingABC26TimesIsNotAsEasyAs123"); + NSArray *expected = @[ @"A", + @"Song", + @"About", + @"The26", + @"ABCs", + @"Is", + @"Of", + @"The", + @"Essence", + @"But", + @"A", + @"Personal", + @"ID", + @"Card", + @"For", + @"User456", + @"In", + @"Room26", + @"Containing", + @"ABC26", + @"Times", + @"Is", + @"Not", + @"As", + @"Easy", + @"As123" ]; + XCTAssertEqualObjects(array, expected); + array = OCMSplitSelectorSegmentIntoWords(@"initWithURLsStartingWithHTTPAssumingTheyAreWebBased"); + expected = @[ @"init", + @"With", + @"URLs", + @"Starting", + @"With", + @"HTTPAssuming", + @"They", + @"Are", + @"Web", + @"Based"]; + XCTAssertEqualObjects(array, expected); +} + +- (void)testParameterNamesFromSelector +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + SEL selectors[6]; + selectors[0] = @selector(gtm_initWithWindow:); + selectors[1] = @selector(_initWithLikesCount:commentDate:firstCommentGUID:toAssetWithUUID:photosBatchID:mainAssetIsMine:mainAssetIsVideo:inAlbumWithTitle:albumUUID:assetUUIDs:placeholderAssetUUIDs:lowResThumbAssetUUIDs:senderNames:forMultipleAsset:allMultipleAssetIsMine:isMixedType:); + selectors[2] = @selector(initWithBitmapDataPlanes:pixelsWide:pixelsHigh:bitsPerSample:samplesPerPixel:hasAlpha:isPlanar:colorSpaceName:bitmapFormat:bytesPerRow:bitsPerPixel:); + selectors[3] = @selector(getPixel:atX:y:); + selectors[4] = @selector(doSomething:::); + selectors[5] = @selector(under_score_this:and_:_while:_also_:_:); +#pragma clang diagnostic pop + + NSArray*> *expecteds = @[ + @[ + @"window", + ], + @[ + @"likesCount", + @"commentDate", + @"commentGUID", + @"UUID", + @"batchID", + @"isMine0", + @"isVideo", + @"title", + @"albumUUID", + @"assetUUIDs0", + @"assetUUIDs1", + @"assetUUIDs2", + @"senderNames", + @"multipleAsset", + @"isMine1", + @"mixedType", + ], + @[ + @"dataPlanes", + @"pixelsWide", + @"pixelsHigh", + @"perSample", + @"perPixel0", + @"hasAlpha", + @"isPlanar", + @"spaceName", + @"bitmapFormat", + @"perRow", + @"perPixel1", + ], + @[ + @"pixel", + @"x", + @"y", + ], + @[ + @"doSomething", + @"arg0", + @"arg1" + ], + @[ + @"this", + @"arg0", + @"while", + @"arg1", + @"arg2", + ], + ]; + for(size_t i = 0; i < sizeof(selectors) / sizeof(*selectors); i++) + { + XCTAssertEqualObjects(OCMParameterNamesFromSelector(selectors[i]), expecteds[i], @"Failing case %@", NSStringFromSelector(selectors[i])); + } +} + +- (void)testOCMTypeOfObject { + XCTAssertEqualObjects(OCMTypeOfObject(self), @"OCMFunctionsTests *"); + XCTAssertEqualObjects(OCMTypeOfObject([NSProxy alloc]), @"id"); + XCTAssertEqualObjects(OCMTypeOfObject(@"foo"), @"NSString *"); + XCTAssertEqualObjects(OCMTypeOfObject(nil), @"id"); + XCTAssertEqualObjects(OCMTypeOfObject(Nil), @"id"); + XCTAssertEqualObjects(OCMTypeOfObject(@1), @"NSNumber *"); + XCTAssertEqualObjects(OCMTypeOfObject(@YES), @"NSNumber *"); + XCTAssertEqualObjects(OCMTypeOfObject(@[ ]), @"NSArray *"); + XCTAssertEqualObjects(OCMTypeOfObject(@[ @"Foo"]), @"NSArray *"); + XCTAssertEqualObjects(OCMTypeOfObject(@[ @"Foo", @"Bar"]), @"NSArray *"); + XCTAssertEqualObjects(OCMTypeOfObject(@{}), @"NSDictionary *"); + XCTAssertEqualObjects(OCMTypeOfObject(@{ @"Foo": @1}), @"NSDictionary *"); + XCTAssertEqualObjects(OCMTypeOfObject(@{ @"Foo": @1, @"Bar": @2}), @"NSDictionary *"); + XCTAssertEqualObjects(OCMTypeOfObject([[NSSet alloc] init]), @"NSSet *"); + XCTAssertEqualObjects(OCMTypeOfObject([NSSet setWithObject:@"foo"]), @"NSSet *"); + XCTAssertEqualObjects(OCMTypeOfObject(^{}), @"BlockType"); + XCTAssertEqualObjects(OCMTypeOfObject(OCMProtocolMock(@protocol(NSObject))), @"id"); + XCTAssertEqualObjects(OCMTypeOfObject([NSObject class]), @"Class"); + XCTAssertEqualObjects(OCMTypeOfObject(dispatch_get_main_queue()), @"dispatch_queue_t"); +} + +- (void)testBlockDeclarationForInvocation { + id recorder = [[InvocationRecorder alloc] init]; + [recorder foo_initWithCount:@2 ofObjectsInSet:[NSSet setWithObject:@"foo"]]; + (void)[recorder initWithCount:@2 ofObjectsInSet:[NSSet setWithObject:@"foo"]]; + [recorder numberFromNameString:@"twoCows" inEnglish:YES]; + [recorder performBlock:^(int a) { return @"foo"; } onQueue:dispatch_get_main_queue()]; + [recorder stringWillBeginFormatting:@"foo"]; + [recorder stringDidEndFormatting:@"foo"]; + [recorder didFinishPlaybackAndWillInternallyTransitionToNextPlayback:YES]; + NSArray *expectedResults = @[ + @"^id(InvocationImplementor *localSelf, NSNumber *count, NSSet *set) { return ... }", + @"^id(InvocationImplementor *localSelf, NSNumber *count, NSSet *set) { return ... }", + @"^NSUInteger(InvocationImplementor *localSelf, NSString *nameString, BOOL inEnglish) { return ... }", + @"^void(InvocationImplementor *localSelf, BlockType block, dispatch_queue_t queue) { ... }", + @"^BOOL(InvocationImplementor *localSelf, NSString *string) { return ... }", + @"^NSString *(InvocationImplementor *localSelf, NSString *string) { return ... }", + @"^void(InvocationImplementor *localSelf, BOOL nextPlayback) { ... }", + ]; + + int i = 0; + for(NSInvocation *invocation in [recorder invocations]) + { + XCTAssertEqualObjects(OCMBlockDeclarationForInvocation(invocation), expectedResults[i]); + ++i; + } +} + @end diff --git a/Source/OCMockTests/OCMockObjectTests.m b/Source/OCMockTests/OCMockObjectTests.m index 27543673..39e24c57 100644 --- a/Source/OCMockTests/OCMockObjectTests.m +++ b/Source/OCMockTests/OCMockObjectTests.m @@ -716,8 +716,7 @@ - (void)testThrowsWhenTryingToUseForwardToRealObjectOnNonPartialMock XCTAssertThrows([[[mock expect] andForwardToRealObject] name], @"Should have raised and exception."); } - -#pragma mark returning values in pass-by-reference arguments +#pragma mark returning values in pass-by-reference argument - (void)testReturnsValuesInPassByReferenceArguments { @@ -1158,4 +1157,120 @@ - (void)testTryingToCreateAnInstanceOfOCMockObjectRaisesAnException } +#pragma mark andDo Tests + +- (void)testAndDoBlockNilArgument +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(nil); + XCTAssertNil([mock stringByAppendingString:@"foo"]); +} + +- (void)testAndDoSimpleBlockWithNoReturnValue +{ + __block int counter = 0; + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^{ counter = 1; }); + XCTAssertNil([mock stringByAppendingString:@"foo"]); + XCTAssertEqual(counter, 1); +} + +- (void)testAndDoSimpleBlockWithGoodReturnValue +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^{ return @"bar"; }); + XCTAssertEqualObjects([mock stringByAppendingString:@"foo"], @"bar"); +} + +- (void)testAndDoSimpleBlockWithNilReturnValue +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^{ return nil; }); + XCTAssertNil([mock stringByAppendingString:@"foo"]); +} + +- (void)testAndDoSimpleBlockWithBadReturnValue +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^{ return 1; }); + XCTAssertThrows([mock stringByAppendingString:@"foo"]); +} + +- (void)testAndDoNSInvocationBlockWithReturnValue +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + return nil; + }); + XCTAssertThrows([mock stringByAppendingString:@"foo"]); +} + +- (void)testAndDoNSInvocationBlockWithNoReturnValue +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + }); + XCTAssertNil([mock stringByAppendingString:@"foo"]); +} + +- (void)testAndDoNSInvocationBlockWithSetReturnValue +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __autoreleasing NSString *returnValue = @"bar"; + [invocation setReturnValue:&returnValue]; + }); + XCTAssertEqualObjects([mock stringByAppendingString:@"foo"], @"bar"); +} + +- (void)testAndDoBlockWithReturnValue +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^(NSString *localSelf, NSString *append) { + return @"bar"; + }); + XCTAssertEqualObjects([mock stringByAppendingString:@"foo"], @"bar"); +} + +- (void)testAndDoBlockWithNilReturnValue +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^(NSString *localSelf, NSString *append) { + XCTAssertEqual(localSelf, self->mock); + XCTAssertEqualObjects(append, @"foo"); + return nil; + }); + XCTAssertNil([mock stringByAppendingString:@"foo"]); +} + +- (void)testAndDoBlockWithNoReturnValue +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^(NSString *localSelf, NSString *append) { + XCTAssertEqual(localSelf, self->mock); + XCTAssertEqualObjects(append, @"foo"); + }); + XCTAssertNil([mock stringByAppendingString:@"foo"]); +} + +- (void)testAndDoBlockWithWrongReturnType +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^(NSString *localSelf, NSString *append) { + XCTAssertEqual(localSelf, self->mock); + XCTAssertEqualObjects(append, @"foo"); + return 2; + }); + XCTAssertThrows([mock stringByAppendingString:@"foo"]); +} + +- (void)testAndDoBlockWithWrongArgumentType +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^(NSString *localSelf, int append) { + }); + XCTAssertThrows([mock stringByAppendingString:@"foo"]); +} + +- (void)testAndDoBlockWithTooManyArguments +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^(NSString *localSelf, NSString *append, int anInt) { + }); + XCTAssertThrows([mock stringByAppendingString:@"foo"]); +} + +- (void)testAndDoBlockWithNotEnoughArguments +{ + OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andDo(^(NSString *localSelf) { + }); + XCTAssertThrows([mock stringByAppendingString:@"foo"]); +} + @end