From cc8f5383ea009c745c7ab214ead60ae489442b82 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 | 29 +++++- Source/OCMock/OCMArg.h | 36 +++++++ Source/OCMock/OCMArg.m | 69 ++++++++++++-- Source/OCMock/OCMConstraint.h | 29 ++++-- Source/OCMock/OCMConstraint.m | 110 ++++++++++++++-------- Source/OCMock/OCMInvocationMatcher.m | 3 +- Source/OCMock/OCMockObject.m | 15 ++- Source/OCMockTests/OCMArgTests.m | 46 +++++++++ Source/OCMockTests/OCMConstraintTests.m | 51 ++++------ Source/OCMockTests/OCMockObjectTests.m | 87 +++++++++++++++++ 11 files changed, 375 insertions(+), 103 deletions(-) diff --git a/Source/OCMock/NSInvocation+OCMAdditions.h b/Source/OCMock/NSInvocation+OCMAdditions.h index bfcd6ef2..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; - +- (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 80bde798..5b15ed67 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 +- (void)applyConstraintOptionsFromStubInvocation:(NSInvocation *)stubInvocation excludingObject:(id)objectToExclude { if(objc_getAssociatedObject(self, OCMRetainedObjectArgumentsKey) != nil) { @@ -109,7 +120,21 @@ - (void)retainObjectArgumentsExcludingObject:(id)objectToExclude } 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 56280486..87ebbe6c 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,6 +59,15 @@ + (id)checkWithSelector:(SEL)selector onObject:(id)anObject; + (id)checkWithBlock:(BOOL (^)(id obj))block; ++ (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 + (id *)setTo:(id)value; diff --git a/Source/OCMock/OCMArg.m b/Source/OCMock/OCMArg.m index 452dfc1b..53bf8e80 100644 --- a/Source/OCMock/OCMArg.m +++ b/Source/OCMock/OCMArg.m @@ -24,7 +24,7 @@ @implementation OCMArg + (id)any { - return [[[OCMAnyConstraint alloc] init] autorelease]; + return [self anyWithOptions:OCMArgDefaultOptions]; } + (void *)anyPointer @@ -44,39 +44,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 *)setTo:(id)value @@ -141,4 +181,13 @@ + (id)resolveSpecialValues:(NSValue *)value return value; } ++ (OCMConstraintOptions)constraintOptionsFromArgOptions:(OCMArgOptions)argOptions +{ + 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 247f51d8..44b97970 100644 --- a/Source/OCMock/OCMInvocationMatcher.m +++ b/Source/OCMock/OCMInvocationMatcher.m @@ -43,7 +43,8 @@ - (void)setInvocation:(NSInvocation *)anInvocation // 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]; + // 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 92de13d5..ba411867 100644 --- a/Source/OCMock/OCMockObject.m +++ b/Source/OCMock/OCMockObject.m @@ -165,13 +165,6 @@ - (void)addInvocation:(NSInvocation *)anInvocation { @synchronized(invocations) { - // We can't do a normal retain arguments on anInvocation because its target/arguments/return - // value could be self. That would produce a retain cycle self->invocations->anInvocation->self. - // However we need to retain everything on anInvocation that isn't self because we expect them to - // stick around after this method returns. Use our special method to retain just what's needed. - // 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]; [invocations addObject:anInvocation]; } } @@ -400,9 +393,15 @@ - (void)forwardInvocation:(NSInvocation *)anInvocation - (BOOL)handleInvocation:(NSInvocation *)anInvocation { [self assertInvocationsArrayIsPresent]; + OCMInvocationStub *stub = [self stubForInvocation: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. + // We also need to handle the OCMConstraintOptions that have been specified or implied for our arguments. + [anInvocation applyConstraintOptionsFromStubInvocation:[stub recordedInvocation] excludingObject:self]; + [self addInvocation:anInvocation]; - OCMInvocationStub *stub = [self stubForInvocation:anInvocation]; if(stub == nil) return NO; diff --git a/Source/OCMockTests/OCMArgTests.m b/Source/OCMockTests/OCMArgTests.m index 3f07eee2..eeb21f38 100644 --- a/Source/OCMockTests/OCMArgTests.m +++ b/Source/OCMockTests/OCMArgTests.m @@ -101,4 +101,50 @@ - (void)testHandlesNonObjectPointersGracefully XCTAssertEqual([OCMArg resolveSpecialValues:nonObjectPointerValue], nonObjectPointerValue, @"Should have returned value as is."); } +- (void)testIsEqualDoesNotRetainArgumentWithOCMArgDoNotRetainStubArg +{ + __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..d4788a9f 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."); @@ -91,7 +75,7 @@ - (void)testEqualUsesTestValuesDefinitionOfEquality TestEqualityFake *value = [[TestEqualityFake alloc] init]; value.isValueEqual = NO; - OCMIsEqualConstraint *constraint = [[OCMIsEqualConstraint alloc] initWithTestValue:testValue]; + OCMIsEqualConstraint *constraint = [[OCMIsEqualConstraint alloc] initWithTestValue:testValue options:OCMConstraintDefaultOptions]; XCTAssertTrue([constraint evaluate:value]); } @@ -103,26 +87,24 @@ - (void)testNotEqualUsesTestValuesDefinitionOfEquality TestEqualityFake *value = [[TestEqualityFake alloc] init]; value.isValueEqual = YES; - OCMIsNotEqualConstraint *constraint = [[OCMIsNotEqualConstraint alloc] initWithTestValue:testValue]; + OCMIsNotEqualConstraint *constraint = [[OCMIsNotEqualConstraint alloc] initWithTestValue:testValue options:OCMConstraintDefaultOptions]; XCTAssertTrue([constraint evaluate:value]); } - (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/OCMockObjectTests.m b/Source/OCMockTests/OCMockObjectTests.m index 3a9b8dcb..6488bf0e 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 @@ -192,6 +203,41 @@ - (NSString *)stringValue; @end +@interface TestClassListenerManager : NSObject +@end + +@implementation TestClassListenerManager + +- (void)addListener:(id)object +{ +} + +- (void)removeListener:(id)object +{ +} +@end + +@interface TestClassListener : NSObject +{ + TestClassListenerManager *manager; +} +@end + +@implementation TestClassListener +- (instancetype)initWithListenerManager:(TestClassListenerManager *)aManager +{ + self = [super init]; + manager = aManager; + [manager addListener:self]; + return self; +} + +- (void)dealloc +{ + [manager removeListener:self]; +} + +@end static NSString *TestNotification = @"TestNotification"; @@ -479,6 +525,47 @@ - (void)testThrowsWhenAttemptingToStubMethodOnStoppedMock XCTAssertThrowsSpecificNamed([[mock stub] rangeOfString:@"foo" options:0], NSException, NSInternalInconsistencyException); } +- (void)testAnyWithOCMArgDoNotRetainInvocationArgIsNotRetainedByInvocation +{ + mock = OCMClassMock([TestClassListenerManager class]); + [[mock expect] addListener:[OCMArg anyWithOptions:OCMArgDoNotRetainInvocationArg]]; + [[mock expect] removeListener:[OCMArg anyWithOptions:OCMArgDoNotRetainInvocationArg]]; + TestClassListener *listener = [[TestClassListener alloc] initWithListenerManager:mock]; + listener = nil; + [mock verify]; +} + +- (void)testArgumentWithOCMArgNeverRetainArgIsNotRetainedByStubOrInvocation +{ + mock = OCMClassMock([TestClassListenerManager class]); + TestClassListener *listener = [TestClassListener alloc]; + [[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