diff --git a/Source/OCMock/NSInvocation+OCMAdditions.h b/Source/OCMock/NSInvocation+OCMAdditions.h index 5f338741..9e4da4d1 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 6a717ca8..3219e567 100644 --- a/Source/OCMock/NSInvocation+OCMAdditions.m +++ b/Source/OCMock/NSInvocation+OCMAdditions.m @@ -18,6 +18,7 @@ #import "NSInvocation+OCMAdditions.h" #import "NSMethodSignature+OCMAdditions.h" #import "OCMArg.h" +#import "OCMConstraint.h" #import "OCMFunctionsPrivate.h" #if(TARGET_OS_OSX && (!defined(__MAC_10_10) || __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_10)) || \ @@ -53,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) { @@ -111,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 27167078..f221b32c 100644 --- a/Source/OCMock/OCMArg.h +++ b/Source/OCMock/OCMArg.h @@ -16,10 +16,36 @@ #import -@interface OCMArg : NSObject +// 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 +58,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 063181ac..a320a521 100644 --- a/Source/OCMock/OCMArg.m +++ b/Source/OCMock/OCMArg.m @@ -25,7 +25,7 @@ @implementation OCMArg + (id)any { - return [OCMAnyConstraint constraint]; + return [self anyWithOptions:OCMArgDefaultOptions]; } + (void *)anyPointer @@ -45,41 +45,80 @@ + (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 value; + return [self isEqual:value options:OCMArgDefaultOptions]; } + (id)isNotEqual:(id)value { - OCMIsNotEqualConstraint *constraint = [OCMIsNotEqualConstraint constraint]; - constraint->testValue = value; - return constraint; + 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 +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 39714db0..1796159e 100644 --- a/Source/OCMock/OCMConstraint.h +++ b/Source/OCMock/OCMConstraint.h @@ -16,9 +16,22 @@ #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 -+ (instancetype)constraint; +@property (readonly) OCMConstraintOptions constraintOptions; + +- (instancetype)initWithOptions:(OCMConstraintOptions)options NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + - (BOOL)evaluate:(id)value; // if you are looking for any, isNil, etc, they have moved to OCMArg @@ -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 @@ -40,20 +55,30 @@ @interface OCMIsNotNilConstraint : OCMConstraint @end -@interface OCMIsNotEqualConstraint : OCMConstraint +@interface OCMEqualityConstraint : OCMConstraint { -@public id testValue; } +- (instancetype)initWithTestValue:(id)testValue options:(OCMConstraintOptions)options NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithOptions:(OCMConstraintOptions)options NS_UNAVAILABLE; + +@end + +@interface OCMIsEqualConstraint : OCMEqualityConstraint +@end + +@interface OCMIsNotEqualConstraint : OCMEqualityConstraint @end @interface OCMInvocationConstraint : OCMConstraint { -@public NSInvocation *invocation; } +- (instancetype)initWithInvocation:(NSInvocation *)invocation options:(OCMConstraintOptions)options NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithOptions:(OCMConstraintOptions)options NS_UNAVAILABLE; + @end @interface OCMBlockConstraint : OCMConstraint @@ -61,7 +86,8 @@ BOOL (^block)(id); } -- (instancetype)initWithConstraintBlock:(BOOL (^)(id))block; +- (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 5232bf2d..7ec790b0 100644 --- a/Source/OCMock/OCMConstraint.m +++ b/Source/OCMock/OCMConstraint.m @@ -16,13 +16,23 @@ #import #import "OCMConstraint.h" - +#import "OCMFunctions.h" @implementation OCMConstraint -+ (instancetype)constraint +- (instancetype)initWithOptions:(OCMConstraintOptions)options { - return [[[self alloc] init] autorelease]; + 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 @@ -37,25 +47,38 @@ - (id)copyWithZone:(struct _NSZone *)zone __unused + (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject { - OCMInvocationConstraint *constraint = [OCMInvocationConstraint constraint]; - NSMethodSignature *signature = [anObject methodSignatureForSelector:aSelector]; - if(signature == nil) - [NSException raise:NSInvalidArgumentException - format:@"Unknown selector %@ used in constraint.", NSStringFromSelector(aSelector)]; - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; - [invocation setTarget:anObject]; - [invocation setSelector:aSelector]; - constraint->invocation = invocation; - return constraint; + return [self constraintWithSelector:aSelector onObject:anObject options:OCMConstraintDefaultOptions]; } + (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue { - OCMInvocationConstraint *constraint = (OCMInvocationConstraint *)[self constraintWithSelector:aSelector onObject:anObject]; - if([[constraint->invocation methodSignature] numberOfArguments] < 4) + return [self constraintWithSelector:aSelector onObject:anObject withValue:aValue options:OCMConstraintDefaultOptions]; +} + ++ (NSInvocation *)invocationWithSelector:(SEL)aSelector onObject:(id)anObject +{ + NSMethodSignature *signature = [anObject methodSignatureForSelector:aSelector]; + if(signature == nil) + [NSException raise:NSInvalidArgumentException format:@"Unknown selector %@ used in constraint.", NSStringFromSelector(aSelector)]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setTarget:anObject]; + [invocation setSelector:aSelector]; + return invocation; +} + ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject options:(OCMConstraintOptions)options +{ + NSInvocation *invocation = [self invocationWithSelector:aSelector onObject:anObject]; + return [[[OCMInvocationConstraint alloc] initWithInvocation:invocation options:options] autorelease]; +} + ++ (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."]; - [constraint->invocation setArgument:&aValue atIndex:3]; - return constraint; + [invocation setArgument:&aValue atIndex:3]; + return [[[OCMInvocationConstraint alloc] initWithInvocation:invocation options:options] autorelease]; } @@ -66,6 +89,14 @@ + (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; @@ -76,35 +107,57 @@ - (BOOL)evaluate:(id)value #pragma mark - -@implementation OCMIsNilConstraint +@implementation OCMEqualityConstraint -- (BOOL)evaluate:(id)value +- (instancetype)initWithTestValue:(id)aTestValue options:(OCMConstraintOptions)options { - return value == nil; + if((self = [super initWithOptions:options])) + { + if(self.constraintOptions & OCMConstraintDoNotRetainStubArg) + { + testValue = aTestValue; + } + else + { + testValue = [aTestValue retain]; + } + } + return self; } -@end +- (void)dealloc +{ + if(!(self.constraintOptions & OCMConstraintDoNotRetainStubArg)) + { + [testValue release]; + } + [super dealloc]; +} +@end -#pragma mark - +#pragma mark - -@implementation OCMIsNotNilConstraint +@implementation OCMIsEqualConstraint - (BOOL)evaluate:(id)value { - return value != nil; + // Note that ordering of `[testValue isEqual:value]` is intentional as we want `testValue` + // to control what equality means in this case. `value` may not even support equality. + return value == testValue || [testValue isEqual:value]; } @end - -#pragma mark - +#pragma mark - @implementation OCMIsNotEqualConstraint - (BOOL)evaluate:(id)value { - return ![value isEqual:testValue]; + // Note that ordering of `[testValue isEqual:value]` is intentional as we want `testValue` + // to control what inequality means in this case. `value` may not even support equality. + return value != testValue && ![testValue isEqual: value]; } @end @@ -114,9 +167,41 @@ - (BOOL)evaluate:(id)value @implementation OCMInvocationConstraint +- (instancetype)initWithInvocation:(NSInvocation *)anInvocation options:(OCMConstraintOptions)options +{ + if((self = [super initWithOptions:options])) + { + NSMethodSignature *signature = [anInvocation methodSignature]; + if([signature numberOfArguments] < 3) + { + [NSException raise:NSInvalidArgumentException format:@"invocation must take at least one argument (other than _cmd and self)"]; + } + if(!(OCMIsObjectType([signature getArgumentTypeAtIndex:2]))) + { + [NSException raise:NSInvalidArgumentException format:@"invocation's second argument must be an object type"]; + } + 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]; + } + return self; +} + +- (void)dealloc +{ + [invocation release]; + [super 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]; @@ -129,10 +214,15 @@ - (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 55162843..2a807e37 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 ec652b5d..593f87ae 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 b21a0aa5..4d2aa63d 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 c8c70a03..800a2113 100644 --- a/Source/OCMockTests/OCMConstraintTests.m +++ b/Source/OCMockTests/OCMConstraintTests.m @@ -17,6 +17,18 @@ #import #import "OCMConstraint.h" +@interface TestEqualityFake : NSObject +@property BOOL isValueEqual; +@end + +@implementation TestEqualityFake + +- (BOOL)isEqual:(id)object +{ + return self.isValueEqual; +} + +@end @interface OCMConstraintTests : XCTestCase { @@ -35,39 +47,63 @@ - (void)setUp - (void)testAnyAcceptsAnything { - OCMConstraint *constraint = [OCMAnyConstraint constraint]; + 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)testNotEqualAcceptsAnythingButValue +{ + 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 options:OCMConstraintDefaultOptions]; + + XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted value."); + XCTAssertFalse([constraint evaluate:nil], @"Should not have accepted nil."); } -- (void)testIsNilAcceptsOnlyNil +- (void)testEqualUsesTestValuesDefinitionOfEquality { - OCMConstraint *constraint = [OCMIsNilConstraint constraint]; + TestEqualityFake *testValue = [[TestEqualityFake alloc] init]; + testValue.isValueEqual = YES; - XCTAssertFalse([constraint evaluate:@"foo"], @"Should not have accepted a value."); - XCTAssertTrue([constraint evaluate:nil], @"Should have accepted nil."); + TestEqualityFake *value = [[TestEqualityFake alloc] init]; + value.isValueEqual = NO; + + OCMIsEqualConstraint *constraint = [[OCMIsEqualConstraint alloc] initWithTestValue:testValue options:OCMConstraintDefaultOptions]; + XCTAssertTrue([constraint evaluate:value]); } -- (void)testIsNotNilAcceptsAnythingButNil +- (void)testNotEqualUsesTestValuesDefinitionOfEquality { - OCMConstraint *constraint = [OCMIsNotNilConstraint constraint]; + TestEqualityFake *testValue = [[TestEqualityFake alloc] init]; + testValue.isValueEqual = NO; - XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted a value."); - XCTAssertFalse([constraint evaluate:nil], @"Should not have accepted nil."); + TestEqualityFake *value = [[TestEqualityFake alloc] init]; + value.isValueEqual = YES; + + OCMIsNotEqualConstraint *constraint = [[OCMIsNotEqualConstraint alloc] initWithTestValue:testValue options:OCMConstraintDefaultOptions]; + XCTAssertTrue([constraint evaluate:value]); } -- (void)testNotEqualAcceptsAnythingButValue +- (void)testEqualAcceptsNothingButValue { - OCMIsNotEqualConstraint *constraint = [OCMIsNotEqualConstraint constraint]; - constraint->testValue = @"foo"; + OCMIsEqualConstraint *constraint = [[OCMIsEqualConstraint 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:@"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 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; @@ -117,9 +153,9 @@ - (void)testUsesBlock { BOOL (^checkForFooBlock)(id) = ^(id value) { 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."); @@ -129,11 +165,11 @@ - (void)testBlockConstraintCanCaptureArgument { __block NSString *captured; BOOL (^captureArgBlock)(id) = ^(id value) { - captured = value; - return YES; - }; - - OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithConstraintBlock:captureArgBlock]; + captured = value; + return YES; + }; + + OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithOptions:OCMConstraintDefaultOptions block:captureArgBlock]; [constraint evaluate:@"foo"]; XCTAssertEqualObjects(@"foo", captured, @"Should have captured value from last invocation."); @@ -143,9 +179,67 @@ - (void)testBlockConstraintCanCaptureArgument - (void)testEvaluateNilBlockReturnsNo { - OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithConstraintBlock:nil]; - + OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithOptions:OCMConstraintDefaultOptions block:nil]; XCTAssertFalse([constraint evaluate:@"foo"]); } +- (void)testEvaluateInvocationRetainsInvocation +{ + OCMInvocationConstraint *constraint; + @autoreleasepool { + SEL selector = @selector(checkArg:); + NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; + [anInvocation setTarget:self]; + [anInvocation setSelector:selector]; + constraint = [[OCMInvocationConstraint alloc] initWithInvocation:anInvocation options:OCMConstraintDefaultOptions]; + } + XCTAssertTrue([constraint evaluate:@"foo"]); +} + +- (BOOL)methodWithNoArgs +{ + return YES; +} + +- (void)testEvaluateInvocationThrowsForInvocationForMethodWithoutArgument +{ + SEL selector = @selector(methodWithNoArgs); + NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; + [anInvocation setTarget:self]; + [anInvocation setSelector:selector]; + XCTAssertThrowsSpecificNamed([[OCMInvocationConstraint alloc] initWithInvocation:anInvocation options:OCMConstraintDefaultOptions], NSException, NSInvalidArgumentException); +} + +- (BOOL)aMethodWithInt:(int)anInt +{ + return YES; +} + +- (void)testEvaluateInvocationThrowsForInvocationForMethodWithoutObjectArgument +{ + SEL selector = @selector(aMethodWithInt:); + NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; + [anInvocation setTarget:self]; + [anInvocation setSelector:selector]; + XCTAssertThrowsSpecificNamed([[OCMInvocationConstraint alloc] initWithInvocation:anInvocation options:OCMConstraintDefaultOptions], NSException, NSInvalidArgumentException); +} + +- (void)aMethodThatDoesNotReturnBool:(id)anArg +{ +} + +- (void)testEvaluateInvocationThrowsForInvocationThatDoesNotReturnBool +{ + SEL selector = @selector(aMethodThatDoesNotReturnBool:); + NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; + [anInvocation setTarget:self]; + [anInvocation setSelector:selector]; + 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 27543673..d060698f 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 @@ -214,6 +225,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"; @@ -503,6 +550,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