From 0ee6a61535effbb37e8ce4f4dfab226c6a3ec87b Mon Sep 17 00:00:00 2001 From: Dave MacLachlan Date: Wed, 25 Nov 2020 15:25:57 -0800 Subject: [PATCH] Improve `andDo:` type safety and simplify usage. The current `andDo` blocks have some major problems: a) They are difficult to use from ARC. When getting values out of NSInvocations, one has to be careful to use `__unsafe_unretained` on arguments, or risk over retaining/releasing and corrupting the heap. When returning values one has to be careful to make sure that they are handled correctly (often using `__autoreleasing`) or once again risk memory issues. b) They don't deal with refactors well. Since extracting argument inside of the blocks depends on extracting by index, it is easy to change a method signature and forget to deal with the index. In many cases it may "just pass" even though the arguments no longer line up correctly. In other cases it leads to crashes that are difficult to debug. c) The signature is often overly complicated for simple blocks. In many cases all we want is a block that just returns a value, or doesn't need to take arguments or return a value. This changes modifies andDo blocks so that the current `andDo:^void(NSInvocation *invocation) { ... }` block is now considered deprecated and gives you options to the type of block you want to pass in. The first is the simple block `^{ ... }` that takes no arguments and has an optional return value. The runtime code verifies that the return value (if supplied) is compatible with what the invocation expects. The second option is the full block which is `^(returnType)(id localSelf, Arg1Type arg1, ...)` where `returnType` and the argument list match the return type and arguments to the invocation. These values are checked at runtime to make sure that they match what the invocation expects. To aid in the transition from the deprecated block type to the new block types, the runtime will `NSLog` a warning about the deprecated block type, and will attempt to suggest a good block declaration to replace it with. They would look something like this: ``` Warning: Replace `^(NSInvocation *invocation) { ... }` with `^id(NSSet *localSelf, NSNumber *count, NSSet *set) { return ... }` ``` This changes code that previously would have looked like this: ``` OCMStub([mockURL setResourceValues:attributes error:[OCMArg anyObjectRef]]) .andDo(^(NSInvocation *invocation) { __unsafe_unretained NSError **errorOut = nil; [invocation getArgument:&errorOut atIndex:3]; *errorOut = error; BOOL returnValue = NO; [invocation setReturnValue:&returnValue]; }); ``` to ``` OCMStub([mockURL setResourceValues:attributes error:[OCMArg anyObjectRef]]) .andDo(^BOOL(NSURL *localSelf, NSDictionary *keyedValues, NSError **error) { *error = [NSError errorWithDomain:@"Error" code:0 userInfo:nil]; return NO; }); ``` and adds a large amount of runtime checking to verify safety. I can break this up into some smaller CLs if we agree that this is a reasonable direction to head in. I have tested the changes here extensively on all of our code at Google. --- Source/OCMock.xcodeproj/project.pbxproj | 6 - .../OCMock/NSMethodSignature+OCMAdditions.h | 4 + .../OCMock/NSMethodSignature+OCMAdditions.m | 5 + Source/OCMock/OCMBlockCaller.h | 18 +- Source/OCMock/OCMBlockCaller.m | 80 ++++- Source/OCMock/OCMBoxedReturnValueProvider.m | 26 +- Source/OCMock/OCMFunctions.m | 330 ++++++++++++++++- Source/OCMock/OCMFunctionsPrivate.h | 42 +++ Source/OCMock/OCMInvocationStub.m | 13 +- Source/OCMock/OCMStubRecorder.h | 5 +- Source/OCMock/OCMStubRecorder.m | 8 +- Source/OCMock/OCProtocolMockObject.h | 2 + Source/OCMock/OCProtocolMockObject.m | 6 + Source/OCMockTests/OCMFunctionsTests.m | 334 +++++++++++++++++- Source/OCMockTests/OCMockObjectTests.m | 119 ++++++- 15 files changed, 949 insertions(+), 49 deletions(-) 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