From a8b48786b738a4a2c846edc06775f540d9e78fc9 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 | 2 +- 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 | 88 ++++++++++++++++++----- Source/OCMock/OCMInvocationMatcher.m | 3 +- Source/OCMock/OCMockObject.m | 20 +++--- Source/OCMockTests/OCMArgTests.m | 46 ++++++++++++ Source/OCMockTests/OCMConstraintTests.m | 31 ++++---- Source/OCMockTests/OCMockObjectTests.m | 87 ++++++++++++++++++++++ 11 files changed, 377 insertions(+), 63 deletions(-) diff --git a/Source/OCMock/NSInvocation+OCMAdditions.h b/Source/OCMock/NSInvocation+OCMAdditions.h index bfcd6ef2..c64cfa84 100644 --- a/Source/OCMock/NSInvocation+OCMAdditions.h +++ b/Source/OCMock/NSInvocation+OCMAdditions.h @@ -20,7 +20,7 @@ + (NSInvocation *)invocationForBlock:(id)block withArguments:(NSArray *)arguments; -- (void)retainObjectArgumentsExcludingObject:(id)objectToExclude; +- (void)applyConstraintOptionsFromStubInvocation:(NSInvocation *)stubInvocation excludingObject:(id)objectToExclude; - (id)getArgumentAtIndexAsObject:(NSInteger)argIndex; diff --git a/Source/OCMock/NSInvocation+OCMAdditions.m b/Source/OCMock/NSInvocation+OCMAdditions.m index 80bde798..33f4cf45 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 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 74f2e48d..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 [[[OCMIsEqualConstraint alloc] initWithTestValue:nil] autorelease]; + return [self isNilWithOptions:OCMArgDefaultOptions]; } + (id)isNotNil { - return [[[OCMIsNotEqualConstraint alloc] initWithTestValue:nil] autorelease]; + 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 da693b62..e3d56fc8 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 @@ -39,8 +54,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 @@ -55,8 +70,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 @@ -65,8 +80,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 095c31c8..505b6335 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,6 +89,15 @@ + (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject with @implementation OCMAnyConstraint +- (instancetype)initWithOptions:(OCMConstraintOptions)options +{ + self = [super initWithOptions:options]; + if (self.constraintOptions & OCMConstraintDoNotRetainStubArg) + { + [NSException raise:NSInvalidArgumentException format:@"`OCMConstraintDoNotRetainStubArg` does not make sense for `OCMAnyConstraint`."]; + } + return self; +} - (BOOL)evaluate:(id)value { return YES; @@ -73,22 +107,32 @@ - (BOOL)evaluate:(id)value -#pragma mark -#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]; } @@ -124,8 +168,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) @@ -139,6 +184,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]; } @@ -153,7 +203,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]; @@ -166,10 +216,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 26090ee2..943e2528 100644 --- a/Source/OCMock/OCMockObject.m +++ b/Source/OCMock/OCMockObject.m @@ -374,18 +374,6 @@ - (void)forwardInvocation:(NSInvocation *)anInvocation - (BOOL)handleInvocation:(NSInvocation *)anInvocation { [self assertInvocationsArrayIsPresent]; - @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]; - } - OCMInvocationStub *stub = nil; @synchronized(stubs) { @@ -402,6 +390,14 @@ - (BOOL)handleInvocation:(NSInvocation *)anInvocation // have to call handleInvocation on the stub at the end [stub retain]; } + @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. + // 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 ad053613..bb0b7303 100644 --- a/Source/OCMockTests/OCMArgTests.m +++ b/Source/OCMockTests/OCMArgTests.m @@ -117,4 +117,50 @@ - (void)testIsNotNil XCTAssertFalse([constraint evaluate:nil], @"Should not have accepted nil."); } +- (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 c3ca6e9f..73e61714 100644 --- a/Source/OCMockTests/OCMConstraintTests.m +++ b/Source/OCMockTests/OCMConstraintTests.m @@ -35,7 +35,7 @@ - (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."); @@ -43,7 +43,7 @@ - (void)testAnyAcceptsAnything - (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."); @@ -52,11 +52,11 @@ - (void)testNotEqualAcceptsAnythingButValue - (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."); + 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."); } @@ -112,7 +112,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."); @@ -127,7 +127,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."); @@ -137,7 +137,7 @@ -(void)testBlockConstraintCanCaptureArgument - (void)testEvaluateNilBlockReturnsNo { - OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithConstraintBlock:nil]; + OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithOptions:OCMConstraintDefaultOptions block:nil]; XCTAssertFalse([constraint evaluate:@"foo"]); } @@ -150,7 +150,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"]); } @@ -166,7 +166,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 @@ -180,7 +180,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 @@ -193,7 +193,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 33037b43..11484fe5 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