diff --git a/Source/OCMock.xcodeproj/project.pbxproj b/Source/OCMock.xcodeproj/project.pbxproj index c4164646..0f94e920 100644 --- a/Source/OCMock.xcodeproj/project.pbxproj +++ b/Source/OCMock.xcodeproj/project.pbxproj @@ -7,8 +7,8 @@ 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 */; }; + 031E50581BB4A56300E257C3 /* OCMFunctionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 031E50571BB4A56300E257C3 /* OCMFunctionsTests.m */; }; + 031E50591BB4A56300E257C3 /* OCMFunctionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 031E50571BB4A56300E257C3 /* OCMFunctionsTests.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, ); }; }; @@ -471,7 +471,7 @@ 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 = ""; }; + 031E50571BB4A56300E257C3 /* OCMFunctionsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMFunctionsTests.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 = ""; }; @@ -752,7 +752,7 @@ 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */, 03B316271463350E0052CD09 /* OCMStubRecorderTests.m */, 037ECD5318FAD84100AF0E4C /* OCMInvocationMatcherTests.m */, - 031E50571BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m */, + 031E50571BB4A56300E257C3 /* OCMFunctionsTests.m */, 03B316211463350E0052CD09 /* OCMConstraintTests.m */, 8B11D4B62448E2E900247BE2 /* OCMCPlusPlus98Tests.mm */, 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */, @@ -1513,7 +1513,7 @@ 8BF73E53246CA75E00B9A52C /* OCMNoEscapeBlockTests.m in Sources */, 8B11D4B72448E2E900247BE2 /* OCMCPlusPlus98Tests.mm in Sources */, 2FA28AB33F01A7D980F2C705 /* OCMockObjectDynamicPropertyMockingTests.m in Sources */, - 031E50581BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */, + 031E50581BB4A56300E257C3 /* OCMFunctionsTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1606,7 +1606,7 @@ buildActionMask = 2147483647; files = ( 3C76716C1BB3EBC500FDC9F4 /* TestClassWithCustomReferenceCounting.m in Sources */, - 031E50591BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */, + 031E50591BB4A56300E257C3 /* OCMFunctionsTests.m in Sources */, 03C9CA1E18F05A84006DF94D /* OCMArgTests.m in Sources */, 036865651D3571A8005E6BEE /* OCMQuantifierTests.m in Sources */, 0322DA66191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m in Sources */, diff --git a/Source/OCMock/NSInvocation+OCMAdditions.h b/Source/OCMock/NSInvocation+OCMAdditions.h index bfcd6ef2..9ee387e3 100644 --- a/Source/OCMock/NSInvocation+OCMAdditions.h +++ b/Source/OCMock/NSInvocation+OCMAdditions.h @@ -47,10 +47,8 @@ - (NSString *)selectorDescriptionAtIndex:(NSInteger)anInt; - (BOOL)methodIsInInitFamily; -- (BOOL)methodIsInAllocFamily; -- (BOOL)methodIsInCopyFamily; -- (BOOL)methodIsInMutableCopyFamily; -- (BOOL)methodIsInNewFamily; +// Create Family is defined as the superset of Alloc/New/Copy/MutableCopy Families +- (BOOL)methodIsInCreateFamily; @end diff --git a/Source/OCMock/NSInvocation+OCMAdditions.m b/Source/OCMock/NSInvocation+OCMAdditions.m index 8f44a9f2..f84099ed 100644 --- a/Source/OCMock/NSInvocation+OCMAdditions.m +++ b/Source/OCMock/NSInvocation+OCMAdditions.m @@ -612,5 +612,9 @@ - (BOOL)methodIsInNewFamily return [self isMethodFamily:@"new"]; } +- (BOOL)methodIsInCreateFamily +{ + return [self methodIsInAllocFamily] || [self methodIsInCopyFamily] || [self methodIsInMutableCopyFamily] || [self methodIsInNewFamily]; +} @end diff --git a/Source/OCMock/NSMethodSignature+OCMAdditions.h b/Source/OCMock/NSMethodSignature+OCMAdditions.h index 6d86e8cb..f842d394 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 25986034..4e57069d 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 44b582f7..e2a5ecf9 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 f8e4def0..a3aa1427 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,10 +39,80 @@ -(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, NULL, &argSize); + 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) + { + // 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)) { - block(anInvocation); + [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]; + } } @end diff --git a/Source/OCMock/OCMBoxedReturnValueProvider.m b/Source/OCMock/OCMBoxedReturnValueProvider.m index f28bbc55..3947f891 100644 --- a/Source/OCMock/OCMBoxedReturnValueProvider.m +++ b/Source/OCMock/OCMBoxedReturnValueProvider.m @@ -17,20 +17,22 @@ #import "OCMBoxedReturnValueProvider.h" #import "OCMFunctionsPrivate.h" #import "NSValue+OCMAdditions.h" - +#import "NSMethodSignature+OCMAdditions.h" @implementation OCMBoxedReturnValueProvider - (void)handleInvocation:(NSInvocation *)anInvocation { - const char *returnType = [[anInvocation methodSignature] methodReturnType]; - NSUInteger returnTypeSize = [[anInvocation methodSignature] methodReturnLength]; - char valueBuffer[returnTypeSize]; + NSUInteger valueSize = 0; NSValue *returnValueAsNSValue = (NSValue *)returnValue; + 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]; - if([self isMethodReturnType:returnType compatibleWithValueType:[returnValueAsNSValue objCType] - value:valueBuffer valueSize:returnTypeSize]) + if([signature isMethodReturnTypeCompatibleWithValueType:returnValueType value:valueBuffer valueSize:valueSize]) { [anInvocation setReturnValue:valueBuffer]; } @@ -41,22 +43,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 51a553a7..4c9974ad 100644 --- a/Source/OCMock/OCMFunctions.m +++ b/Source/OCMock/OCMFunctions.m @@ -19,7 +19,8 @@ #import "OCClassMockObject.h" #import "OCPartialMockObject.h" #import "OCMLocation.h" - +#import "OCProtocolMockObject.h" +#import "NSInvocation+OCMAdditions.h" #pragma mark Known private API @@ -328,11 +329,26 @@ BOOL OCMIsApplePrivateMethod(Class cls, SEL sel) ([selName hasPrefix:@"_"] || [selName hasSuffix:@"_"]); } +BOOL OCMIsBlock(id potentialBlock) +{ + static Class blockClass; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + blockClass = [^{} class]; + Class nsObjectClass = [NSObject class]; + while([blockClass superclass] != nsObjectClass) + { + blockClass = [blockClass superclass]; + } + }); + return [potentialBlock isKindOfClass:blockClass]; +} BOOL OCMIsNonEscapingBlock(id block) { struct OCMBlockDef *blockRef = (__bridge struct OCMBlockDef *)block; - return (blockRef->flags & OCMBlockIsNoEscape) != 0; + return OCMIsBlock(block) && (blockRef->flags & OCMBlockIsNoEscape) != 0; } @@ -466,3 +482,328 @@ void OCMReportFailure(OCMLocation *loc, NSString *description) } } + +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)[NSDictionary class]: @"NSDictionary *", + (id)[NSNumber class]: @"NSNumber *", + (id)[NSOrderedSet class]: @"NSOrderedSet *", + (id)[NSSet class]: @"NSSet *", + (id)[NSString class]: @"NSString *", + (id)[NSURLRequest class]: @"NSURLRequest *", + (id)[NSTimer class]: @"NSTimer *", + }; + 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 fdf8e41b..ecba6ea8 100644 --- a/Source/OCMock/OCMFunctionsPrivate.h +++ b/Source/OCMock/OCMFunctionsPrivate.h @@ -48,9 +48,11 @@ OCPartialMockObject *OCMGetAssociatedMockForObject(id anObject); 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 { @@ -76,3 +78,42 @@ enum 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 fd8ecafc..148f2a57 100644 --- a/Source/OCMock/OCMInvocationStub.m +++ b/Source/OCMock/OCMInvocationStub.m @@ -19,8 +19,6 @@ #import "OCMArgAction.h" #import "NSInvocation+OCMAdditions.h" -#define UNSET_RETURN_VALUE_MARKER ((id)0x01234567) - @implementation OCMInvocationStub @@ -55,13 +53,18 @@ - (void)handleInvocation:(NSInvocation *)anInvocation if([anInvocation methodIsInInitFamily]) { - 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 an init method. If you intended to return nil, make this explicit with .andReturn(nil)", NSStringFromSelector([anInvocation selector])]; } diff --git a/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.m b/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.m index b3f18288..70f3f30b 100644 --- a/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.m +++ b/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.m @@ -35,8 +35,7 @@ - (void)handleInvocation:(NSInvocation *)anInvocation @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Expected invocation with object return type. Did you mean to use andReturnValue: instead?" userInfo:nil]; } - if([anInvocation methodIsInAllocFamily] || [anInvocation methodIsInNewFamily] || - [anInvocation methodIsInCopyFamily] || [anInvocation methodIsInMutableCopyFamily]) + if([anInvocation methodIsInCreateFamily]) { // methods that "create" an object return it with an extra retain count [returnValue retain]; diff --git a/Source/OCMock/OCMStubRecorder.h b/Source/OCMock/OCMStubRecorder.h index d735fb68..5ec15593 100644 --- a/Source/OCMock/OCMStubRecorder.h +++ b/Source/OCMock/OCMStubRecorder.h @@ -25,7 +25,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; @end @@ -52,8 +52,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 c0752839..e67bc648 100644 --- a/Source/OCMock/OCMStubRecorder.m +++ b/Source/OCMock/OCMStubRecorder.m @@ -86,7 +86,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; @@ -172,12 +172,12 @@ @implementation OCMStubRecorder (Properties) @dynamic _andDo; -- (OCMStubRecorder *(^)(void (^)(NSInvocation *)))_andDo +- (OCMStubRecorder *(^)(id))_andDo { - id (^theBlock)(void (^)(NSInvocation *)) = ^ (void (^ blockToCall)(NSInvocation *)) - { - return [self andDo:blockToCall]; - }; + id (^theBlock)(id) = ^ (id blockToCall) + { + return [self andDo:blockToCall]; + }; return [[theBlock copy] autorelease]; } diff --git a/Source/OCMock/OCProtocolMockObject.h b/Source/OCMock/OCProtocolMockObject.h index 59e5394c..478f0dd4 100644 --- a/Source/OCMock/OCProtocolMockObject.h +++ b/Source/OCMock/OCProtocolMockObject.h @@ -23,5 +23,7 @@ - (id)initWithProtocol:(Protocol *)aProtocol; +- (Protocol *)mockedProtocol; + @end diff --git a/Source/OCMock/OCProtocolMockObject.m b/Source/OCMock/OCProtocolMockObject.m index 0b175857..a29c3c58 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/OCMBoxedReturnValueProviderTests.m b/Source/OCMockTests/OCMBoxedReturnValueProviderTests.m deleted file mode 100644 index e647065b..00000000 --- a/Source/OCMockTests/OCMBoxedReturnValueProviderTests.m +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2015-2020 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 "OCMBoxedReturnValueProvider.h" - - -@interface OCMBoxedReturnValueProvider(Private) -- (BOOL)isMethodReturnType:(const char *)returnType compatibleWithValueType:(const char *)valueType value:(const char*)value valueSize:(size_t)valueSize; -@end - - -@interface OCMBoxedReturnValueProviderTests : XCTestCase - -@end - -@implementation OCMBoxedReturnValueProviderTests - -- (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}"; - - OCMBoxedReturnValueProvider *boxed = [OCMBoxedReturnValueProvider new]; - XCTAssertTrue([boxed isMethodReturnType:type1 compatibleWithValueType:type2 value:NULL valueSize:0]); - XCTAssertTrue([boxed isMethodReturnType:type1 compatibleWithValueType:type3 value:NULL valueSize:0]); - XCTAssertTrue([boxed isMethodReturnType:type2 compatibleWithValueType:type1 value:NULL valueSize:0]); - XCTAssertTrue([boxed isMethodReturnType:type2 compatibleWithValueType:type3 value:NULL valueSize:0]); - XCTAssertTrue([boxed isMethodReturnType:type3 compatibleWithValueType:type1 value:NULL valueSize:0]); - XCTAssertTrue([boxed isMethodReturnType:type3 compatibleWithValueType:type2 value:NULL valueSize: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]})}}}}"; - - OCMBoxedReturnValueProvider *boxed = [OCMBoxedReturnValueProvider new]; - XCTAssertTrue([boxed isMethodReturnType:type1 compatibleWithValueType:type2 value:NULL valueSize:0]); -} - - -- (void)testCorrectEqualityForStructureWithUnknownName -{ - // see https://github.com/erikdoe/ocmock/issues/333 - const char *type1 = "{?=dd}"; - const char *type2 = "{CLLocationCoordinate2D=dd}"; - - OCMBoxedReturnValueProvider *boxed = [OCMBoxedReturnValueProvider new]; - XCTAssertTrue([boxed isMethodReturnType:type1 compatibleWithValueType:type2 value:NULL valueSize: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}}}}"; - - OCMBoxedReturnValueProvider *boxed = [OCMBoxedReturnValueProvider new]; - XCTAssertTrue([boxed isMethodReturnType:type1 compatibleWithValueType:type2 value:NULL valueSize:0]); -} - -@end - - - - diff --git a/Source/OCMockTests/OCMFunctionsTests.m b/Source/OCMockTests/OCMFunctionsTests.m new file mode 100644 index 00000000..7d18dff3 --- /dev/null +++ b/Source/OCMockTests/OCMFunctionsTests.m @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2020 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 +#import "OCMFunctions.h" +#import "OCMFunctionsPrivate.h" +#import "OCMockObject.h" +#import "OCMockMacros.h" + +#pragma mark Helper classes + +// 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)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 5f0f3d11..2f6723b3 100644 --- a/Source/OCMockTests/OCMockObjectTests.m +++ b/Source/OCMockTests/OCMockObjectTests.m @@ -691,7 +691,6 @@ - (void)testThrowsWhenTryingToUseForwardToRealObjectOnNonPartialMock XCTAssertThrows([[[mock expect] andForwardToRealObject] name], @"Should have raised and exception."); } - #pragma mark returning values in pass-by-reference arguments - (void)testReturnsValuesInPassByReferenceArguments @@ -1100,6 +1099,122 @@ - (void)testUncalledRejectStubDoesNotCountAsExpectation } +#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