From 0e5d34454d597c85bedccdc2b71e46dff732f982 Mon Sep 17 00:00:00 2001 From: Dave MacLachlan Date: Fri, 5 Jun 2020 13:58:59 -0700 Subject: [PATCH] Add support for controlling retain/copy semantics for arguments to stubs. Allows marking an argument in a stub as having various semantics: - is not retained by invocations Object arguments are retained by default in OCMock. In some cases to avoid retain loops you need to mark an argument as unretained. - is not retained by stub Stub arguments are retained by default in OCMock. In some specialized cases you do not want the stub arguments retained - is copied by invocation Some arguments have copy semantics and we need the invocation to copy the argument instead of retain it. --- Source/OCMock/NSInvocation+OCMAdditions.h | 3 +- Source/OCMock/NSInvocation+OCMAdditions.m | 40 +++++--- Source/OCMock/OCMArg.h | 53 +++++++---- Source/OCMock/OCMArg.m | 68 ++++++++++--- Source/OCMock/OCMConstraint.h | 29 ++++-- Source/OCMock/OCMConstraint.m | 110 ++++++++++++++-------- Source/OCMock/OCMInvocationMatcher.m | 30 +----- Source/OCMock/OCMockObject.m | 8 +- Source/OCMockTests/OCMArgTests.m | 46 ++++++++- Source/OCMockTests/OCMConstraintTests.m | 47 ++++----- Source/OCMockTests/OCMStubRecorderTests.m | 43 --------- Source/OCMockTests/OCMockObjectTests.m | 44 +++++++-- 12 files changed, 313 insertions(+), 208 deletions(-) diff --git a/Source/OCMock/NSInvocation+OCMAdditions.h b/Source/OCMock/NSInvocation+OCMAdditions.h index 74af90ba..99d91f9b 100644 --- a/Source/OCMock/NSInvocation+OCMAdditions.h +++ b/Source/OCMock/NSInvocation+OCMAdditions.h @@ -20,8 +20,7 @@ + (NSInvocation *)invocationForBlock:(id)block withArguments:(NSArray *)arguments; -- (void)retainObjectArgumentsExcludingObject:(id)objectToExclude excludingObjectsAtIndexes:(NSIndexSet *)indexes; - +- (void)applyConstraintOptionsFromStubInvocation:(NSInvocation *)stubInvocation excludingObject:(id)objectToExclude; - (id)getArgumentAtIndexAsObject:(NSInteger)argIndex; - (NSString *)invocationDescription; diff --git a/Source/OCMock/NSInvocation+OCMAdditions.m b/Source/OCMock/NSInvocation+OCMAdditions.m index bc1510c7..e8394532 100644 --- a/Source/OCMock/NSInvocation+OCMAdditions.m +++ b/Source/OCMock/NSInvocation+OCMAdditions.m @@ -53,9 +53,20 @@ + (NSInvocation *)invocationForBlock:(id)block withArguments:(NSArray *)argument } +- (OCMConstraintOptions)getArgumentContraintOptionsForArgumentAtIndex:(NSUInteger)index +{ + id argument; + [self getArgument:&argument atIndex:index]; + if(![argument isProxy] && [argument isKindOfClass:[OCMConstraint class]]) + { + return [(OCMConstraint *)argument constraintOptions]; + } + return OCMConstraintDefaultOptions; +} + static NSString *const OCMRetainedObjectArgumentsKey = @"OCMRetainedObjectArgumentsKey"; -- (void)retainObjectArgumentsExcludingObject:(id)objectToExclude excludingObjectsAtIndexes:(NSIndexSet *)indexes; +- (void)applyConstraintOptionsFromStubInvocation:(NSInvocation *)stubInvocation excludingObject:(id)objectToExclude { if(objc_getAssociatedObject(self, OCMRetainedObjectArgumentsKey) != nil) { @@ -80,16 +91,7 @@ - (void)retainObjectArgumentsExcludingObject:(id)objectToExclude excludingObject for(NSUInteger index = 2; index < numberOfArguments; index++) { const char *argumentType = [[self methodSignature] getArgumentTypeAtIndex:index]; - BOOL isObjectType = OCMIsObjectType(argumentType); - if ([indexes containsIndex:index]) - { - if (!isObjectType) - { - [NSException raise:NSInternalInconsistencyException format:@"Argument at %d is not an object", (int)index]; - } - continue; - } - if (isObjectType) + if (OCMIsObjectType(argumentType)) { id argument; [self getArgument:&argument atIndex:index]; @@ -118,7 +120,21 @@ - (void)retainObjectArgumentsExcludingObject:(id)objectToExclude excludingObject } else { - [retainedArguments addObject:argument]; + // Conform to the constraintOptions in the stub (if any). + OCMConstraintOptions constraintOptions = [stubInvocation getArgumentContraintOptionsForArgumentAtIndex:index]; + if((constraintOptions & OCMConstraintCopyInvocationArg)) + { + // Copy not only retains the copy in our array + // but updates the arg in the invocation that we store. + id argCopy = [argument copy]; + [retainedArguments addObject:argCopy]; + [self setArgument:&argCopy atIndex:index]; + [argCopy release]; + } + else if(!(constraintOptions & OCMConstraintDoNotRetainInvocationArg)) + { + [retainedArguments addObject:argument]; + } } } } diff --git a/Source/OCMock/OCMArg.h b/Source/OCMock/OCMArg.h index 140259c8..f3fe2a74 100644 --- a/Source/OCMock/OCMArg.h +++ b/Source/OCMock/OCMArg.h @@ -16,10 +16,37 @@ #import +// Options for controlling how OCMArgs function. +typedef NS_OPTIONS(NSUInteger, OCMArgOptions) { + // The OCMArg will retain/release the value passed to it, and invocations on a stub that has + // arguments that the OCMArg is constraining will retain the values passed to them for the + // arguments being constrained by the OCMArg. + OCMArgDefaultOptions = 0UL, + + // The OCMArg will not retain/release the value passed to it. Is only applicable for + // `isEqual:options:` and `isNotEqual:options`. The caller is responsible for making sure that the + // arg is valid for the required lifetime. Note that unless `OCMArgDoNotRetainInvocationArg` is + // also specified, invocations of the stub that the OCMArg arg is constraining will retain values + // passed to them for the arguments being constrained by the OCMArg. `OCMArgNeverRetainArg` is + // usually what you want to use. + OCMArgDoNotRetainStubArg = (1UL << 0), + + // Invocations on a stub that has arguments that the OCMArg is constraining will retain/release + // the values passed to them for the arguments being constrained by the OCMArg. + OCMArgDoNotRetainInvocationArg = (1UL << 1), + + // Invocations on a stub that has arguments that the OCMArg is constraining will copy/release + // the values passed to them for the arguments being constrained by the OCMArg. + OCMArgCopyInvocationArg = (1UL << 2), + + OCMArgNeverRetainArg = OCMArgDoNotRetainStubArg | OCMArgDoNotRetainInvocationArg, +}; + @interface OCMArg : NSObject // constraining arguments +// constrain using OCMArgDefaultOptions + (id)any; + (SEL)anySelector; + (void *)anyPointer; @@ -32,21 +59,14 @@ + (id)checkWithSelector:(SEL)selector onObject:(id)anObject; + (id)checkWithBlock:(BOOL (^)(id obj))block; -// Unretained object arguments are not retained by invocations on the mock, but are retained by the -// stub itself. A use case for this is when you are stubbing an argument to a method that does not -// retain its argument using an `OCMArg` variant that you do not want to keep a reference to. -// See `OCMOCK_ANY_UNRETAINED`. -+ (id)unretainedObject:(id)anObject; - -// Unsafe unretained object arguments are not retained by invocations on the mock or by the stub. -// A potential use case for this is when you are stubbing methods that do not retain their -// arguments and you want to verify dealloc conditions. An example of this would be verifying -// KVO registration/deregistration that occurs in the init/dealloc of an object. If the object were -// retained by the mocking system in any way you would never see the deregistration. -// Note that you *must* keep a reference to anObject outside this call or you will crash. -// Something like `[OCMArg unsafeUnretainedObject:[[Foo alloc] init]]` under ARC is a guaranteed -// dangling pointer problem. -+ (id)unsafeUnretainedObject:(id)anObject; ++ (id)anyWithOptions:(OCMArgOptions)options; ++ (id)isNilWithOptions:(OCMArgOptions)options; ++ (id)isNotNilWithOptions:(OCMArgOptions)options; ++ (id)isEqual:(id)value options:(OCMArgOptions)options; ++ (id)isNotEqual:(id)value options:(OCMArgOptions)options; ++ (id)isKindOfClass:(Class)cls options:(OCMArgOptions)options; ++ (id)checkWithSelector:(SEL)selector onObject:(id)anObject options:(OCMArgOptions)options; ++ (id)checkWithOptions:(OCMArgOptions)options withBlock:(BOOL (^)(id obj))block; // manipulating arguments @@ -61,9 +81,6 @@ + (id)resolveSpecialValues:(NSValue *)value; -// Return YES if `object` is either an unretained or an unsafe unretained object. -+ (BOOL)isUnretained:(id)object; - @end #define OCMOCK_ANY [OCMArg any] diff --git a/Source/OCMock/OCMArg.m b/Source/OCMock/OCMArg.m index 9dbc6e44..b253376e 100644 --- a/Source/OCMock/OCMArg.m +++ b/Source/OCMock/OCMArg.m @@ -25,7 +25,7 @@ @implementation OCMArg + (id)any { - return [[[OCMAnyConstraint alloc] init] autorelease]; + return [self anyWithOptions:OCMArgDefaultOptions]; } + (void *)anyPointer @@ -45,39 +45,79 @@ + (SEL)anySelector + (id)isNil { - return [OCMIsNilConstraint constraint]; + return [self isNilWithOptions:OCMArgDefaultOptions]; } + (id)isNotNil { - return [OCMIsNotNilConstraint constraint]; + return [self isNotNilWithOptions:OCMArgDefaultOptions]; } + (id)isEqual:(id)value { - return [[[OCMIsEqualConstraint alloc] initWithTestValue:value] autorelease]; + return [self isEqual:value options:OCMArgDefaultOptions]; } + (id)isNotEqual:(id)value { - return [[[OCMIsNotEqualConstraint alloc] initWithTestValue:value] autorelease]; + return [self isNotEqual:value options:OCMArgDefaultOptions]; } + (id)isKindOfClass:(Class)cls { - return [[[OCMBlockConstraint alloc] initWithConstraintBlock:^BOOL(id obj) { - return [obj isKindOfClass:cls]; - }] autorelease]; + return [self isKindOfClass:cls options:OCMArgDefaultOptions]; } + (id)checkWithSelector:(SEL)selector onObject:(id)anObject { - return [OCMConstraint constraintWithSelector:selector onObject:anObject]; + return [self checkWithSelector:selector onObject:anObject options:OCMArgDefaultOptions]; } + (id)checkWithBlock:(BOOL (^)(id))block { - return [[[OCMBlockConstraint alloc] initWithConstraintBlock:block] autorelease]; + return [self checkWithOptions:OCMArgDefaultOptions withBlock:block]; +} + ++ (id)anyWithOptions:(OCMArgOptions)options +{ + return [[[OCMAnyConstraint alloc] initWithOptions:[self constraintOptionsFromArgOptions:options]] autorelease]; +} + ++ (id)isNilWithOptions:(OCMArgOptions)options +{ + return [[[OCMIsEqualConstraint alloc] initWithTestValue:nil options:[self constraintOptionsFromArgOptions:options]] autorelease]; +} + ++ (id)isNotNilWithOptions:(OCMArgOptions)options +{ + return [[[OCMIsNotEqualConstraint alloc] initWithTestValue:nil options:[self constraintOptionsFromArgOptions:options]] autorelease]; +} + ++ (id)isEqual:(id)value options:(OCMArgOptions)options +{ + return [[[OCMIsEqualConstraint alloc] initWithTestValue:value options:[self constraintOptionsFromArgOptions:options]] autorelease]; +} + ++ (id)isNotEqual:(id)value options:(OCMArgOptions)options +{ + return [[[OCMIsNotEqualConstraint alloc] initWithTestValue:value options:[self constraintOptionsFromArgOptions:options]] autorelease]; +} + ++ (id)isKindOfClass:(Class)cls options:(OCMArgOptions)options +{ + return [[[OCMBlockConstraint alloc] initWithOptions:[self constraintOptionsFromArgOptions:options] block:^BOOL(id obj) { + return [obj isKindOfClass:cls]; + }] autorelease]; +} + ++ (id)checkWithSelector:(SEL)selector onObject:(id)anObject options:(OCMArgOptions)options +{ + return [OCMConstraint constraintWithSelector:selector onObject:anObject options:[self constraintOptionsFromArgOptions:options]]; +} + ++ (id)checkWithOptions:(OCMArgOptions)options withBlock:(BOOL (^)(id obj))block +{ + return [[[OCMBlockConstraint alloc] initWithOptions:[self constraintOptionsFromArgOptions:options] block:block] autorelease]; } + (id)unretainedObject:(id)anObject @@ -152,9 +192,13 @@ + (id)resolveSpecialValues:(NSValue *)value return value; } -+ (BOOL)isUnretained:(id)object ++ (OCMConstraintOptions)constraintOptionsFromArgOptions:(OCMArgOptions)argOptions { - return object_getClass(object) == [OCMUnretainedArgument class]; + OCMConstraintOptions constraintOptions = 0; + if(argOptions & OCMArgDoNotRetainStubArg) constraintOptions |= OCMConstraintDoNotRetainStubArg; + if(argOptions & OCMArgDoNotRetainInvocationArg) constraintOptions |= OCMConstraintDoNotRetainInvocationArg; + if(argOptions & OCMArgCopyInvocationArg) constraintOptions |= OCMConstraintCopyInvocationArg; + return constraintOptions; } @end diff --git a/Source/OCMock/OCMConstraint.h b/Source/OCMock/OCMConstraint.h index e0b3eb2c..4cd294e3 100644 --- a/Source/OCMock/OCMConstraint.h +++ b/Source/OCMock/OCMConstraint.h @@ -16,8 +16,21 @@ #import +// See OCMArgOptions for documentation on options. +typedef NS_OPTIONS(NSUInteger, OCMConstraintOptions) { + OCMConstraintDefaultOptions = 0UL, + OCMConstraintDoNotRetainStubArg = (1UL << 0), + OCMConstraintDoNotRetainInvocationArg = (1UL << 1), + OCMConstraintCopyInvocationArg = (1UL << 2), + OCMConstraintNeverRetainArg = OCMConstraintDoNotRetainStubArg | OCMConstraintDoNotRetainInvocationArg, +}; -@interface OCMConstraint : NSObject +@interface OCMConstraint : NSObject + +@property (readonly) OCMConstraintOptions constraintOptions; + +- (instancetype)initWithOptions:(OCMConstraintOptions)options NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; - (BOOL)evaluate:(id)value; @@ -28,6 +41,8 @@ + (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject; + (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue; ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject options:(OCMConstraintOptions)options; ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue options:(OCMConstraintOptions)options; @end @@ -45,8 +60,8 @@ id testValue; } -- (instancetype)initWithTestValue:(id)testValue NS_DESIGNATED_INITIALIZER; -- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithTestValue:(id)testValue options:(OCMConstraintOptions)options NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithOptions:(OCMConstraintOptions)options NS_UNAVAILABLE; @end @@ -61,8 +76,8 @@ NSInvocation *invocation; } -- (instancetype)initWithInvocation:(NSInvocation *)invocation NS_DESIGNATED_INITIALIZER; -- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithInvocation:(NSInvocation *)invocation options:(OCMConstraintOptions)options NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithOptions:(OCMConstraintOptions)options NS_UNAVAILABLE; @end @@ -71,8 +86,8 @@ BOOL (^block)(id); } -- (instancetype)initWithConstraintBlock:(BOOL (^)(id))block NS_DESIGNATED_INITIALIZER; -- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithOptions:(OCMConstraintOptions)options block:(BOOL (^)(id))block NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithOptions:(OCMConstraintOptions)options NS_UNAVAILABLE; @end diff --git a/Source/OCMock/OCMConstraint.m b/Source/OCMock/OCMConstraint.m index 49b139b5..4ffd681d 100644 --- a/Source/OCMock/OCMConstraint.m +++ b/Source/OCMock/OCMConstraint.m @@ -19,6 +19,21 @@ @implementation OCMConstraint +- (instancetype)initWithOptions:(OCMConstraintOptions)options +{ + self = [super init]; + if(self) + { + OCMConstraintOptions badOptions = (OCMConstraintDoNotRetainInvocationArg | OCMConstraintCopyInvocationArg); + if((options & badOptions) == badOptions) + { + [NSException raise:NSInvalidArgumentException format:@"`OCMConstraintDoNotRetainInvocationArg` and `OCMConstraintCopyInvocationArg` are mutually exclusive."]; + } + _constraintOptions = options; + } + return self; +} + - (BOOL)evaluate:(id)value { return NO; @@ -29,6 +44,16 @@ - (id)copyWithZone:(struct _NSZone *)zone return [self retain]; } ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject +{ + return [self constraintWithSelector:aSelector onObject:anObject options:OCMConstraintDefaultOptions]; +} + ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue +{ + return [self constraintWithSelector:aSelector onObject:anObject withValue:aValue options:OCMConstraintDefaultOptions]; +} + + (NSInvocation *)invocationWithSelector:(SEL)aSelector onObject:(id)anObject { NSMethodSignature *signature = [anObject methodSignatureForSelector:aSelector]; @@ -40,19 +65,19 @@ + (NSInvocation *)invocationWithSelector:(SEL)aSelector onObject:(id)anObject return invocation; } -+ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject options:(OCMConstraintOptions)options { NSInvocation *invocation = [self invocationWithSelector:aSelector onObject:anObject]; - return [[[OCMInvocationConstraint alloc] initWithInvocation:invocation] autorelease]; + return [[[OCMInvocationConstraint alloc] initWithInvocation:invocation options:options] autorelease]; } -+ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue options:(OCMConstraintOptions)options { NSInvocation *invocation = [self invocationWithSelector:aSelector onObject:anObject]; - if([[invocation methodSignature] numberOfArguments] < 4) - [NSException raise:NSInvalidArgumentException format:@"Constraint with value requires selector with two arguments."]; - [invocation setArgument:&aValue atIndex:3]; - return [[[OCMInvocationConstraint alloc] initWithInvocation:invocation] autorelease]; + if([[invocation methodSignature] numberOfArguments] < 4) + [NSException raise:NSInvalidArgumentException format:@"Constraint with value requires selector with two arguments."]; + [invocation setArgument:&aValue atIndex:3]; + return [[[OCMInvocationConstraint alloc] initWithInvocation:invocation options:options] autorelease]; } @@ -64,22 +89,18 @@ + (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject with @implementation OCMAnyConstraint -- (BOOL)evaluate:(id)value +- (instancetype)initWithOptions:(OCMConstraintOptions)options { - return YES; + self = [super initWithOptions:options]; + if (self.constraintOptions & OCMConstraintDoNotRetainStubArg) + { + [NSException raise:NSInvalidArgumentException format:@"`OCMConstraintDoNotRetainStubArg` does not make sense for `OCMAnyConstraint`."]; + } + return self; } - -@end - - - -#pragma mark - - -@implementation OCMIsNilConstraint - - (BOOL)evaluate:(id)value { - return value == nil; + return YES; } @end @@ -88,33 +109,30 @@ - (BOOL)evaluate:(id)value #pragma mark - -@implementation OCMIsNotNilConstraint - -- (BOOL)evaluate:(id)value -{ - return value != nil; -} - -@end - - - -#pragma mark -#pragma mark - - @implementation OCMEqualityConstraint -- (instancetype)initWithTestValue:(id)aTestValue +- (instancetype)initWithTestValue:(id)aTestValue options:(OCMConstraintOptions)options { - if((self = [super init])) + if((self = [super initWithOptions:options])) { - testValue = [aTestValue retain]; + if(self.constraintOptions & OCMConstraintDoNotRetainStubArg) + { + testValue = aTestValue; + } + else + { + testValue = [aTestValue retain]; + } } return self; } - (void)dealloc { - [testValue release]; + if(!(self.constraintOptions & OCMConstraintDoNotRetainStubArg)) + { + [testValue release]; + } [super dealloc]; } @@ -152,8 +170,9 @@ - (BOOL)evaluate:(id)value @implementation OCMInvocationConstraint -- (instancetype)initWithInvocation:(NSInvocation *)anInvocation { - if((self = [super init])) +- (instancetype)initWithInvocation:(NSInvocation *)anInvocation options:(OCMConstraintOptions)options +{ + self = [super initWithOptions:options]; { NSMethodSignature *signature = [anInvocation methodSignature]; if([signature numberOfArguments] < 3) @@ -167,6 +186,11 @@ - (instancetype)initWithInvocation:(NSInvocation *)anInvocation { if(strcmp([signature methodReturnType], @encode(BOOL))) { [NSException raise:NSInvalidArgumentException format:@"invocation must return BOOL"]; + + } + if (self.constraintOptions & OCMConstraintDoNotRetainStubArg) + { + [NSException raise:NSInvalidArgumentException format:@"`OCMConstraintDoNotRetainStubArg` does not make sense for `OCMInvocationConstraint`."]; } invocation = [anInvocation retain]; } @@ -181,7 +205,7 @@ - (void)dealloc - (BOOL)evaluate:(id)value { - [invocation setArgument:&value atIndex:2]; // should test if constraint takes arg + [invocation setArgument:&value atIndex:2]; [invocation invoke]; BOOL returnValue; [invocation getReturnValue:&returnValue]; @@ -194,10 +218,14 @@ - (BOOL)evaluate:(id)value @implementation OCMBlockConstraint -- (instancetype)initWithConstraintBlock:(BOOL (^)(id))aBlock +- (instancetype)initWithOptions:(OCMConstraintOptions)options block:(BOOL (^)(id))aBlock; { - if ((self = [super init])) + if((self = [super initWithOptions:options])) { + if(self.constraintOptions & OCMConstraintDoNotRetainStubArg) + { + [NSException raise:NSInvalidArgumentException format:@"`OCMConstraintDoNotRetainStubArg` does not make sense for `OCMBlockConstraint`."]; + } block = [aBlock copy]; } diff --git a/Source/OCMock/OCMInvocationMatcher.m b/Source/OCMock/OCMInvocationMatcher.m index baf934e4..3b4772dc 100644 --- a/Source/OCMock/OCMInvocationMatcher.m +++ b/Source/OCMock/OCMInvocationMatcher.m @@ -45,39 +45,13 @@ - (NSIndexSet *)unretainedArgumentIndexes - (void)setInvocation:(NSInvocation *)anInvocation { - // Strip any "unretained arguments" from the invocation and record them. - NSMutableIndexSet *unretainedIndexes = [NSMutableIndexSet indexSet]; - NSMutableIndexSet *unsafeUnretainedIndexes = [NSMutableIndexSet indexSet]; - NSMethodSignature *signature = [anInvocation methodSignature]; - NSUInteger n = [signature numberOfArguments]; - for(NSUInteger i = 2; i < n; i++) - { - const char *argType = [signature getArgumentTypeAtIndex:i]; - if (OCMIsObjectType(argType)) - { - OCMUnretainedArgument *unretainedArg; - [anInvocation getArgument:&unretainedArg atIndex:i]; - if ([OCMArg isUnretained:unretainedArg]) - { - [unretainedIndexes addIndex:i]; - if (![unretainedArg isSafe]) - { - [unsafeUnretainedIndexes addIndex:i]; - } - id realArg = [unretainedArg object]; - [anInvocation setArgument:&realArg atIndex:i]; - } - } - } - [unretainedArgumentIndexes release]; - unretainedArgumentIndexes = [unretainedIndexes copy]; - [recordedInvocation release]; // Don't do a regular -retainArguments on the invocation that we use for matching. NSInvocation // effectively does an strcpy on char* arguments which messes up matching them literally and blows // up with anyPointer (in strlen since it's not actually a C string). Also on the off-chance that // anInvocation contains self as an argument, -retainArguments would create a retain cycle. - [anInvocation retainObjectArgumentsExcludingObject:self excludingObjectsAtIndexes:unsafeUnretainedIndexes]; + // All of our stub specific constraint options are handled in the constraints themselves. + [anInvocation applyConstraintOptionsFromStubInvocation:nil excludingObject:self]; recordedInvocation = [anInvocation retain]; } diff --git a/Source/OCMock/OCMockObject.m b/Source/OCMock/OCMockObject.m index 9757c1ee..943e2528 100644 --- a/Source/OCMock/OCMockObject.m +++ b/Source/OCMock/OCMockObject.m @@ -394,14 +394,10 @@ - (BOOL)handleInvocation:(NSInvocation *)anInvocation { // We can't do a normal retain arguments on anInvocation because its target/arguments/return // value could be self. That would produce a retain cycle self->invocations->anInvocation->self. - // However we need to retain everything on anInvocation that isn't self because we expect them to - // stick around after this method returns. Use our special method to retain just what's needed. - // This still doesn't completely prevent retain cycles since any of the arguments could have a - // strong reference to self. Those will have to be broken with manual calls to -stopMocking. - [anInvocation retainObjectArgumentsExcludingObject:self excludingObjectsAtIndexes:[stub unretainedArgumentIndexes]]; + // We also need to handle the OCMConstraintOptions that have been specified or implied for our arguments. + [anInvocation applyConstraintOptionsFromStubInvocation:[stub recordedInvocation] excludingObject:self]; [invocations addObject:anInvocation]; } - if(stub == nil) return NO; diff --git a/Source/OCMockTests/OCMArgTests.m b/Source/OCMockTests/OCMArgTests.m index 2f86d062..eeb21f38 100644 --- a/Source/OCMockTests/OCMArgTests.m +++ b/Source/OCMockTests/OCMArgTests.m @@ -101,10 +101,50 @@ - (void)testHandlesNonObjectPointersGracefully XCTAssertEqual([OCMArg resolveSpecialValues:nonObjectPointerValue], nonObjectPointerValue, @"Should have returned value as is."); } -- (void)testThrowsForNilArgumentToUnretainedObjects +- (void)testIsEqualDoesNotRetainArgumentWithOCMArgDoNotRetainStubArg { - XCTAssertThrows([OCMArg unretainedObject:nil]); - XCTAssertThrows([OCMArg unsafeUnretainedObject:nil]); + __weak id value; + OCMConstraint *constraint; + @autoreleasepool { + value = [NSArray arrayWithObject:self]; + constraint = [OCMArg isEqual:value options:OCMArgDoNotRetainStubArg]; + } + XCTAssertNil(value); +} + +- (void)testIsEqualDoesRetainArgumentWithOCMArgDefaultOptions +{ + __weak id value; + OCMConstraint *constraint; + @autoreleasepool { + value = [NSArray arrayWithObject:self]; + constraint = [OCMArg isEqual:value]; + + } + XCTAssertNotNil(value); +} + +- (void)testIsNotEqualDoesNotRetainArgumentWithOCMArgDoNotRetainStubArg +{ + __weak id value; + OCMConstraint *constraint; + @autoreleasepool { + value = [NSArray arrayWithObject:self]; + constraint = [OCMArg isNotEqual:value options:OCMArgDoNotRetainStubArg]; + } + XCTAssertNil(value); +} + +- (void)testIsNotEqualDoesRetainArgumentWithOCMArgDefaultOptions +{ + __weak id value; + OCMConstraint *constraint; + @autoreleasepool { + value = [NSArray arrayWithObject:self]; + constraint = [OCMArg isNotEqual:value]; + + } + XCTAssertNotNil(value); } @end diff --git a/Source/OCMockTests/OCMConstraintTests.m b/Source/OCMockTests/OCMConstraintTests.m index aed88aa6..c7078735 100644 --- a/Source/OCMockTests/OCMConstraintTests.m +++ b/Source/OCMockTests/OCMConstraintTests.m @@ -47,37 +47,21 @@ - (void)setUp - (void)testAnyAcceptsAnything { - OCMConstraint *constraint = [[OCMAnyConstraint alloc] init]; + OCMConstraint *constraint = [[OCMAnyConstraint alloc] initWithOptions:OCMConstraintDefaultOptions]; XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted a value."); XCTAssertTrue([constraint evaluate:@"bar"], @"Should have accepted another value."); XCTAssertTrue([constraint evaluate:nil], @"Should have accepted nil."); } -- (void)testIsNilAcceptsOnlyNil -{ - OCMConstraint *constraint = [OCMIsNilConstraint constraint]; - - XCTAssertFalse([constraint evaluate:@"foo"], @"Should not have accepted a value."); - XCTAssertTrue([constraint evaluate:nil], @"Should have accepted nil."); -} - -- (void)testIsNotNilAcceptsAnythingButNil -{ - OCMConstraint *constraint = [OCMIsNotNilConstraint constraint]; - - XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted a value."); - XCTAssertFalse([constraint evaluate:nil], @"Should not have accepted nil."); -} - - (void)testNotEqualAcceptsAnythingButValue { - OCMIsNotEqualConstraint *constraint = [[OCMIsNotEqualConstraint alloc] initWithTestValue:@"foo"]; + OCMIsNotEqualConstraint *constraint = [[OCMIsNotEqualConstraint alloc] initWithTestValue:@"foo" options:OCMConstraintDefaultOptions]; XCTAssertFalse([constraint evaluate:@"foo"], @"Should not have accepted value."); XCTAssertTrue([constraint evaluate:@"bar"], @"Should have accepted other value."); XCTAssertTrue([constraint evaluate:nil], @"Should have accepted nil."); - constraint = [[OCMIsNotEqualConstraint alloc] initWithTestValue:nil]; + constraint = [[OCMIsNotEqualConstraint alloc] initWithTestValue:nil options:OCMConstraintDefaultOptions]; XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted value."); XCTAssertFalse([constraint evaluate:nil], @"Should not have accepted nil."); @@ -109,20 +93,18 @@ - (void)testNotEqualUsesTestValuesDefinitionOfEquality - (void)testEqualAcceptsNothingButValue { - OCMIsEqualConstraint *constraint = [[OCMIsEqualConstraint alloc] initWithTestValue:@"foo"]; + OCMIsEqualConstraint *constraint = [[OCMIsEqualConstraint alloc] initWithTestValue:@"foo" options:OCMConstraintDefaultOptions]; XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted value."); XCTAssertFalse([constraint evaluate:@"bar"], @"Should not have accepted other value."); XCTAssertFalse([constraint evaluate:nil], @"Should not have accepted nil."); - constraint = [[OCMIsEqualConstraint alloc] initWithTestValue:nil]; + constraint = [[OCMIsEqualConstraint alloc] initWithTestValue:nil options:OCMConstraintDefaultOptions]; XCTAssertFalse([constraint evaluate:@"foo"], @"Should not have accepted other value."); XCTAssertTrue([constraint evaluate:nil], @"Should have accepted nil."); - } - - (BOOL)checkArg:(id)theArg { didCallCustomConstraint = YES; @@ -175,7 +157,7 @@ -(void)testUsesBlock return [value isEqualToString:@"foo"]; }; - OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithConstraintBlock:checkForFooBlock]; + OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithOptions:OCMConstraintDefaultOptions block:checkForFooBlock]; XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted foo."); XCTAssertFalse([constraint evaluate:@"bar"], @"Should not have accepted bar."); @@ -190,7 +172,7 @@ -(void)testBlockConstraintCanCaptureArgument return YES; }; - OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithConstraintBlock:captureArgBlock]; + OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithOptions:OCMConstraintDefaultOptions block:captureArgBlock]; [constraint evaluate:@"foo"]; XCTAssertEqualObjects(@"foo", captured, @"Should have captured value from last invocation."); @@ -200,7 +182,7 @@ -(void)testBlockConstraintCanCaptureArgument - (void)testEvaluateNilBlockReturnsNo { - OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithConstraintBlock:nil]; + OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithOptions:OCMConstraintDefaultOptions block:nil]; XCTAssertFalse([constraint evaluate:@"foo"]); } @@ -213,7 +195,7 @@ - (void)testEvaluateInvocationRetainsInvocation NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; [anInvocation setTarget:self]; [anInvocation setSelector:selector]; - constraint = [[OCMInvocationConstraint alloc] initWithInvocation:anInvocation]; + constraint = [[OCMInvocationConstraint alloc] initWithInvocation:anInvocation options:OCMConstraintDefaultOptions]; } XCTAssertTrue([constraint evaluate:@"foo"]); } @@ -229,7 +211,7 @@ - (void)testEvaluateInvocationThrowsForInvocationForMethodWithoutArgument NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; [anInvocation setTarget:self]; [anInvocation setSelector:selector]; - XCTAssertThrowsSpecificNamed([[OCMInvocationConstraint alloc] initWithInvocation:anInvocation], NSException, NSInvalidArgumentException); + XCTAssertThrowsSpecificNamed([[OCMInvocationConstraint alloc] initWithInvocation:anInvocation options:OCMConstraintDefaultOptions], NSException, NSInvalidArgumentException); } - (BOOL)aMethodWithInt:(int)anInt @@ -243,7 +225,7 @@ - (void)testEvaluateInvocationThrowsForInvocationForMethodWithoutObjectArgument NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; [anInvocation setTarget:self]; [anInvocation setSelector:selector]; - XCTAssertThrowsSpecificNamed([[OCMInvocationConstraint alloc] initWithInvocation:anInvocation], NSException, NSInvalidArgumentException); + XCTAssertThrowsSpecificNamed([[OCMInvocationConstraint alloc] initWithInvocation:anInvocation options:OCMConstraintDefaultOptions], NSException, NSInvalidArgumentException); } - (void)aMethodThatDoesNotReturnBool:(id)anArg @@ -256,7 +238,12 @@ - (void)testEvaluateInvocationThrowsForInvocationThatDoesNotReturnBool NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; [anInvocation setTarget:self]; [anInvocation setSelector:selector]; - XCTAssertThrowsSpecificNamed([[OCMInvocationConstraint alloc] initWithInvocation:anInvocation], NSException, NSInvalidArgumentException); + XCTAssertThrowsSpecificNamed([[OCMInvocationConstraint alloc] initWithInvocation:anInvocation options:OCMConstraintDefaultOptions], NSException, NSInvalidArgumentException); +} + +- (void)testConstraintThrowsForBadOptions +{ + XCTAssertThrowsSpecificNamed([[OCMIsEqualConstraint alloc] initWithTestValue:nil options:OCMConstraintDoNotRetainInvocationArg | OCMConstraintCopyInvocationArg], NSException, NSInvalidArgumentException); } @end diff --git a/Source/OCMockTests/OCMStubRecorderTests.m b/Source/OCMockTests/OCMStubRecorderTests.m index 94dc759a..f64a62eb 100644 --- a/Source/OCMockTests/OCMStubRecorderTests.m +++ b/Source/OCMockTests/OCMStubRecorderTests.m @@ -68,47 +68,4 @@ - (void)testAddsExceptionReturnValueProvider } -- (void)testRecordsUnretainedObjectArgument -{ - NSString *arg = @"I love mocks."; - id mock = [OCMockObject mockForClass:[NSString class]]; - OCMStubRecorder *recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; - [(id)recorder stringByAppendingString:[OCMArg unretainedObject:arg]]; - XCTAssertEqualObjects([(OCMInvocationStub *)[recorder invocationMatcher] unretainedArgumentIndexes], [NSIndexSet indexSetWithIndex:2]); -} - -- (void)testRecordsUnsafeUnretainedObjectArgument -{ - NSString *arg = @"I love mocks."; - id mock = [OCMockObject mockForClass:[NSString class]]; - OCMStubRecorder *recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; - [(id)recorder stringByAppendingString:[OCMArg unsafeUnretainedObject:arg]]; - XCTAssertEqualObjects([(OCMInvocationStub *)[recorder invocationMatcher] unretainedArgumentIndexes], [NSIndexSet indexSetWithIndex:2]); -} - -- (void)testRecorderRetainsUnretainedObjectArgument -{ - __weak NSString *weakArg = nil; - id recorder = nil; - NSString *arg = [[NSString alloc] initWithUTF8String:"I love mocks"]; - weakArg = arg; - id mock = [OCMockObject mockForClass:[NSString class]]; - recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; - [recorder stringByAppendingString:[OCMArg unretainedObject:arg]]; - arg = nil; - XCTAssertNotNil(weakArg); -} - -- (void)testRecorderDoesNotRetainUnsafeUnretainedObjectArgument -{ - __weak NSString *weakArg = nil; - id recorder = nil; - NSString *arg = [[NSString alloc] initWithUTF8String:"I love mocks"]; - weakArg = arg; - id mock = [OCMockObject mockForClass:[NSString class]]; - recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; - [recorder stringByAppendingString:[OCMArg unsafeUnretainedObject:arg]]; - arg = nil; - XCTAssertNil(weakArg); -} @end diff --git a/Source/OCMockTests/OCMockObjectTests.m b/Source/OCMockTests/OCMockObjectTests.m index 2c660224..caefa679 100644 --- a/Source/OCMockTests/OCMockObjectTests.m +++ b/Source/OCMockTests/OCMockObjectTests.m @@ -88,6 +88,17 @@ @implementation TestClassWithProperty @end +@interface TestClassWithCopyProperty : NSObject + +@property (nonatomic, copy) NSString *title; + +@end + +@implementation TestClassWithCopyProperty + +@synthesize title; + +@end @interface TestClassWithBlockArgMethod : NSObject @@ -514,27 +525,48 @@ - (void)testThrowsWhenAttemptingToStubMethodOnStoppedMock XCTAssertThrowsSpecificNamed([[mock stub] rangeOfString:@"foo" options:0], NSException, NSInternalInconsistencyException); } -- (void)testAnyUnretainedObjectArgumentsAreNotRetained +- (void)testAnyWithOCMArgDoNotRetainInvocationArgIsNotRetainedByInvocation { mock = OCMClassMock([TestClassListenerManager class]); - [[mock expect] addListener:OCMOCK_ANY_UNRETAINED]; - [[mock expect] removeListener:OCMOCK_ANY_UNRETAINED]; + [[mock expect] addListener:[OCMArg anyWithOptions:OCMArgDoNotRetainInvocationArg]]; + [[mock expect] removeListener:[OCMArg anyWithOptions:OCMArgDoNotRetainInvocationArg]]; TestClassListener *listener = [[TestClassListener alloc] initWithListenerManager:mock]; listener = nil; [mock verify]; } -- (void)testUnsafeUnretainedObjectArgumentsAreNotRetained +- (void)testArgumentWithOCMArgNeverRetainArgIsNotRetainedByStubOrInvocation { mock = OCMClassMock([TestClassListenerManager class]); TestClassListener *listener = [TestClassListener alloc]; - [[mock expect] addListener:OCMOCK_UNSAFE_UNRETAINED(listener)]; - [[mock expect] removeListener:OCMOCK_UNSAFE_UNRETAINED(listener)]; + [[mock expect] addListener:[OCMArg isEqual:listener options:OCMArgNeverRetainArg]]; + [[mock expect] removeListener:[OCMArg isEqual:listener options:OCMArgNeverRetainArg]]; listener = [listener initWithListenerManager:mock]; listener = nil; [mock verify]; } +- (void)testArgumentWithDefaultOptionsIsNotCopiedByInvocation +{ + mock = OCMClassMock([TestClassWithCopyProperty class]); + [[mock stub] setTitle:[OCMArg any]]; + NSMutableString *aString = [@"foo" mutableCopy]; + [mock setTitle:aString]; + [aString appendString:@"bar"]; + // If the string *were* being handled properly, this would fail. + OCMVerify([mock setTitle:@"foobar"]); +} + +- (void)testArgumentWithOCMArgCopyInvocationArgIsCopiedByInvocation +{ + mock = OCMClassMock([TestClassWithCopyProperty class]); + [[mock stub] setTitle:[OCMArg anyWithOptions:OCMArgCopyInvocationArg]]; + NSMutableString *aString = [@"foo" mutableCopy]; + [mock setTitle:aString]; + [aString appendString:@"bar"]; + OCMVerify([mock setTitle:@"foo"]); +} + #pragma mark returning values from stubbed methods - (void)testReturnsStubbedReturnValue