diff --git a/Source/OCMock/NSInvocation+OCMAdditions.h b/Source/OCMock/NSInvocation+OCMAdditions.h index 1ecd95cb..9d1dfcff 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 b2a65070..6f382c56 100644 --- a/Source/OCMock/NSInvocation+OCMAdditions.m +++ b/Source/OCMock/NSInvocation+OCMAdditions.m @@ -54,9 +54,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) { @@ -112,7 +123,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 14cc8cbd..c2ae89a9 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 *)setTo:(id)value @@ -142,4 +182,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 57116c5c..4f5052d7 100644 --- a/Source/OCMock/OCMConstraint.h +++ b/Source/OCMock/OCMConstraint.h @@ -16,7 +16,22 @@ #import -@interface OCMConstraint : NSObject + +// 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 + +@property (readonly) OCMConstraintOptions constraintOptions; + +- (instancetype)initWithOptions:(OCMConstraintOptions)options NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; - (BOOL)evaluate:(id)value; @@ -27,6 +42,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 @@ -44,8 +61,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 @@ -60,8 +77,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 @@ -70,8 +87,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 028fca81..4e1de140 100644 --- a/Source/OCMock/OCMConstraint.m +++ b/Source/OCMock/OCMConstraint.m @@ -20,6 +20,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; @@ -30,6 +45,16 @@ - (id)copyWithZone:(struct _NSZone *)zone __unused 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]; @@ -41,19 +66,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]; } @@ -65,22 +90,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 @@ -89,33 +110,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]; } @@ -153,8 +171,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) @@ -168,6 +187,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]; } @@ -182,7 +206,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]; @@ -195,10 +219,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 fb621e6e..68e7f93a 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 d492671f..5c6a8103 100644 --- a/Source/OCMock/OCMockObject.m +++ b/Source/OCMock/OCMockObject.m @@ -169,13 +169,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]; } } @@ -404,9 +397,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 6e14bffb..eccc87be 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 380ac407..18d11f53 100644 --- a/Source/OCMockTests/OCMockObjectTests.m +++ b/Source/OCMockTests/OCMockObjectTests.m @@ -87,6 +87,17 @@ @implementation TestClassWithProperty @end +@interface TestClassWithCopyProperty : NSObject + +@property (nonatomic, copy) NSString *title; + +@end + +@implementation TestClassWithCopyProperty + +@synthesize title; + +@end @interface TestClassWithBlockArgMethod : NSObject @@ -213,6 +224,42 @@ + (BOOL)supportsMocking:(NSString **)reasonPtr @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"; @@ -499,6 +546,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