diff --git a/Source/OCMock.xcodeproj/project.pbxproj b/Source/OCMock.xcodeproj/project.pbxproj index 2a9d519f..1b26df67 100644 --- a/Source/OCMock.xcodeproj/project.pbxproj +++ b/Source/OCMock.xcodeproj/project.pbxproj @@ -279,6 +279,16 @@ 817EB15C1BD765130047E85A /* OCMBlockArgCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */; }; 817EB15D1BD765130047E85A /* OCMArgAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2833B48908EAD36444671 /* OCMArgAction.h */; }; 817EB1661BD7674D0047E85A /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; }; + 8BF740142476E4B400B9A52C /* OCMUnretainedArgument.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF740122476E4B300B9A52C /* OCMUnretainedArgument.h */; }; + 8BF740152476E4B400B9A52C /* OCMUnretainedArgument.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF740132476E4B400B9A52C /* OCMUnretainedArgument.m */; }; + 8BF740162476E59A00B9A52C /* OCMUnretainedArgument.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF740132476E4B400B9A52C /* OCMUnretainedArgument.m */; }; + 8BF740172476E59A00B9A52C /* OCMUnretainedArgument.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF740132476E4B400B9A52C /* OCMUnretainedArgument.m */; }; + 8BF740182476E59B00B9A52C /* OCMUnretainedArgument.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF740132476E4B400B9A52C /* OCMUnretainedArgument.m */; }; + 8BF740192476E59C00B9A52C /* OCMUnretainedArgument.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF740132476E4B400B9A52C /* OCMUnretainedArgument.m */; }; + 8BF7401A24771FD600B9A52C /* OCMUnretainedArgument.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF740122476E4B300B9A52C /* OCMUnretainedArgument.h */; }; + 8BF7401B24771FD700B9A52C /* OCMUnretainedArgument.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF740122476E4B300B9A52C /* OCMUnretainedArgument.h */; }; + 8BF7401C24771FD700B9A52C /* OCMUnretainedArgument.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF740122476E4B300B9A52C /* OCMUnretainedArgument.h */; }; + 8BF7401D24771FD800B9A52C /* OCMUnretainedArgument.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF740122476E4B300B9A52C /* OCMUnretainedArgument.h */; }; 8DE97C5522B43EE60098C63F /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; }; 8DE97C5622B43EE60098C63F /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; }; 8DE97C5722B43EE60098C63F /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; }; @@ -569,6 +579,8 @@ 3CFBDD751BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestClassWithCustomReferenceCounting.h; sourceTree = ""; }; 3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestClassWithCustomReferenceCounting.m; sourceTree = ""; }; 817EB1621BD765130047E85A /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BF740122476E4B300B9A52C /* OCMUnretainedArgument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMUnretainedArgument.h; sourceTree = ""; }; + 8BF740132476E4B400B9A52C /* OCMUnretainedArgument.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMUnretainedArgument.m; sourceTree = ""; }; 8DE97CA022B43EE60098C63F /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A02926811CA0725A00594AAF /* TestObjects.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TestObjects.xcdatamodel; sourceTree = ""; }; D31108AD1828DB8700737925 /* OCMockLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCMockLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -867,6 +879,8 @@ 03B315A2146333BF0052CD09 /* OCMPassByRefSetter.m */, 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */, 2FA283D58AA7569D8A5B0C57 /* OCMBlockArgCaller.m */, + 8BF740122476E4B300B9A52C /* OCMUnretainedArgument.h */, + 8BF740132476E4B400B9A52C /* OCMUnretainedArgument.m */, ); name = "Argument Constraints and Actions"; sourceTree = ""; @@ -957,6 +971,7 @@ 03B315F5146333C00052CD09 /* OCMPassByRefSetter.h in Headers */, 03B315FA146333C00052CD09 /* OCMRealObjectForwarder.h in Headers */, 03B315FF146333C00052CD09 /* OCMObjectReturnValueProvider.h in Headers */, + 8BF740142476E4B400B9A52C /* OCMUnretainedArgument.h in Headers */, 03B31604146333C00052CD09 /* OCObserverMockObject.h in Headers */, 03B31609146333C00052CD09 /* OCPartialMockObject.h in Headers */, 0368656D1D357317005E6BEE /* OCMQuantifier.h in Headers */, @@ -1006,6 +1021,7 @@ 817EB1661BD7674D0047E85A /* OCMFunctionsPrivate.h in Headers */, 03B31605146333C00052CD09 /* OCObserverMockObject.h in Headers */, 03B3160A146333C00052CD09 /* OCPartialMockObject.h in Headers */, + 8BF7401A24771FD600B9A52C /* OCMUnretainedArgument.h in Headers */, 03B31614146333C00052CD09 /* OCProtocolMockObject.h in Headers */, 2FA28E1EB6B8536785258DF5 /* OCMInvocationMatcher.h in Headers */, 0322DA6A19118B4600CACAF1 /* OCMVerifier.h in Headers */, @@ -1057,6 +1073,7 @@ 817EB1591BD765130047E85A /* NSObject+OCMAdditions.h in Headers */, 817EB15A1BD765130047E85A /* NSValue+OCMAdditions.h in Headers */, 817EB15B1BD765130047E85A /* OCMFunctions.h in Headers */, + 8BF7401C24771FD700B9A52C /* OCMUnretainedArgument.h in Headers */, 817EB15C1BD765130047E85A /* OCMBlockArgCaller.h in Headers */, 817EB15D1BD765130047E85A /* OCMArgAction.h in Headers */, 2FA28806443827E286F12F6F /* OCMNonRetainingObjectReturnValueProvider.h in Headers */, @@ -1089,6 +1106,7 @@ 8DE97C8C22B43EE60098C63F /* OCMBoxedReturnValueProvider.h in Headers */, 8DE97C8D22B43EE60098C63F /* OCMExceptionReturnValueProvider.h in Headers */, 8DE97C8E22B43EE60098C63F /* OCMIndirectReturnValueProvider.h in Headers */, + 8BF7401D24771FD800B9A52C /* OCMUnretainedArgument.h in Headers */, 8DE97C8F22B43EE60098C63F /* OCMNotificationPoster.h in Headers */, 8DE97C9022B43EE60098C63F /* OCMObjectReturnValueProvider.h in Headers */, 8DE97C9122B43EE60098C63F /* OCMFunctionsPrivate.h in Headers */, @@ -1146,6 +1164,7 @@ F0B951481B00810C00942C38 /* NSObject+OCMAdditions.h in Headers */, F0B951491B00810C00942C38 /* NSValue+OCMAdditions.h in Headers */, F0B9514A1B00810C00942C38 /* OCMFunctions.h in Headers */, + 8BF7401B24771FD700B9A52C /* OCMUnretainedArgument.h in Headers */, 2FA28B7BDB3319A499E90525 /* OCMBlockArgCaller.h in Headers */, 2FA280E60213BA09F007C173 /* OCMArgAction.h in Headers */, 2FA28AFBD67EAB9DD1F23BF5 /* OCMNonRetainingObjectReturnValueProvider.h in Headers */, @@ -1409,6 +1428,7 @@ 03B315CA146333BF0052CD09 /* OCMBlockCaller.m in Sources */, 036865681D3572ED005E6BEE /* OCMQuantifier.m in Sources */, 03B315CF146333BF0052CD09 /* OCMBoxedReturnValueProvider.m in Sources */, + 8BF740152476E4B400B9A52C /* OCMUnretainedArgument.m in Sources */, 03B315D4146333BF0052CD09 /* OCMConstraint.m in Sources */, 03B315D9146333BF0052CD09 /* OCMExceptionReturnValueProvider.m in Sources */, 03B315DE146333BF0052CD09 /* OCMIndirectReturnValueProvider.m in Sources */, @@ -1451,6 +1471,7 @@ 03B315CC146333BF0052CD09 /* OCMBlockCaller.m in Sources */, 036865691D3572ED005E6BEE /* OCMQuantifier.m in Sources */, 03B315D1146333BF0052CD09 /* OCMBoxedReturnValueProvider.m in Sources */, + 8BF740162476E59A00B9A52C /* OCMUnretainedArgument.m in Sources */, 03DCED6D183406BC0059089E /* NSObject+OCMAdditions.m in Sources */, 03B315D6146333BF0052CD09 /* OCMConstraint.m in Sources */, 03B315DB146333BF0052CD09 /* OCMExceptionReturnValueProvider.m in Sources */, @@ -1521,6 +1542,7 @@ 817EB11F1BD765130047E85A /* OCMInvocationMatcher.m in Sources */, 0368656B1D3572ED005E6BEE /* OCMQuantifier.m in Sources */, 817EB1201BD765130047E85A /* OCMInvocationStub.m in Sources */, + 8BF740182476E59B00B9A52C /* OCMUnretainedArgument.m in Sources */, 817EB1211BD765130047E85A /* OCMInvocationExpectation.m in Sources */, 817EB1221BD765130047E85A /* OCMRealObjectForwarder.m in Sources */, 817EB1231BD765130047E85A /* OCMBlockCaller.m in Sources */, @@ -1563,6 +1585,7 @@ 8DE97C5C22B43EE60098C63F /* OCMVerifier.m in Sources */, 8DE97C5D22B43EE60098C63F /* OCMInvocationMatcher.m in Sources */, 8DE97C5E22B43EE60098C63F /* OCMInvocationStub.m in Sources */, + 8BF740192476E59C00B9A52C /* OCMUnretainedArgument.m in Sources */, 8DE97C5F22B43EE60098C63F /* OCMInvocationExpectation.m in Sources */, 8DE97C6022B43EE60098C63F /* OCMRealObjectForwarder.m in Sources */, 8DE97C6122B43EE60098C63F /* OCMBlockCaller.m in Sources */, @@ -1633,6 +1656,7 @@ F0B951141B0080EC00942C38 /* OCMInvocationMatcher.m in Sources */, 0368656A1D3572ED005E6BEE /* OCMQuantifier.m in Sources */, F0B951151B0080EC00942C38 /* OCMInvocationStub.m in Sources */, + 8BF740172476E59A00B9A52C /* OCMUnretainedArgument.m in Sources */, F0B951161B0080EC00942C38 /* OCMInvocationExpectation.m in Sources */, F0B951171B0080EC00942C38 /* OCMRealObjectForwarder.m in Sources */, F0B951181B0080EC00942C38 /* OCMBlockCaller.m in Sources */, diff --git a/Source/OCMock/NSInvocation+OCMAdditions.h b/Source/OCMock/NSInvocation+OCMAdditions.h index bfcd6ef2..74af90ba 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)retainObjectArgumentsExcludingObject:(id)objectToExclude excludingObjectsAtIndexes:(NSIndexSet *)indexes; - (id)getArgumentAtIndexAsObject:(NSInteger)argIndex; diff --git a/Source/OCMock/NSInvocation+OCMAdditions.m b/Source/OCMock/NSInvocation+OCMAdditions.m index 1c447bac..ac2ada82 100644 --- a/Source/OCMock/NSInvocation+OCMAdditions.m +++ b/Source/OCMock/NSInvocation+OCMAdditions.m @@ -55,12 +55,11 @@ + (NSInvocation *)invocationForBlock:(id)block withArguments:(NSArray *)argument static NSString *const OCMRetainedObjectArgumentsKey = @"OCMRetainedObjectArgumentsKey"; -- (void)retainObjectArgumentsExcludingObject:(id)objectToExclude +- (void)retainObjectArgumentsExcludingObject:(id)objectToExclude excludingObjectsAtIndexes:(NSIndexSet *)indexes; { - if(objc_getAssociatedObject(self, OCMRetainedObjectArgumentsKey) != nil) + if (objc_getAssociatedObject(self, OCMRetainedObjectArgumentsKey) != nil) { - // looks like we've retained the arguments already; do nothing else - return; + [NSException raise:NSInternalInconsistencyException format:@"Arguments have already been retained"]; } NSMutableArray *retainedArguments = [[NSMutableArray alloc] init]; @@ -80,7 +79,16 @@ - (void)retainObjectArgumentsExcludingObject:(id)objectToExclude for(NSUInteger index = 2; index < numberOfArguments; index++) { const char *argumentType = [[self methodSignature] getArgumentTypeAtIndex:index]; - if(OCMIsObjectType(argumentType)) + BOOL isObjectType = OCMIsObjectType(argumentType); + if ([indexes containsIndex:index]) + { + if (!isObjectType) + { + [NSException raise:NSInternalInconsistencyException format:@"Argument at %d is not an object", (int)index]; + } + continue; + } + if (isObjectType) { id argument; [self getArgument:&argument atIndex:index]; @@ -106,18 +114,9 @@ - (void)retainObjectArgumentsExcludingObject:(id)objectToExclude { id returnValue; [self getReturnValue:&returnValue]; - if((returnValue != nil) && (returnValue != objectToExclude)) + if (returnValue != nil) { - if(OCMIsBlockType(returnType)) - { - id blockReturnValue = [returnValue copy]; - [retainedArguments addObject:blockReturnValue]; - [blockReturnValue release]; - } - else - { - [retainedArguments addObject:returnValue]; - } + [NSException raise:NSInternalInconsistencyException format:@"Return value should never be set"]; } } diff --git a/Source/OCMock/OCMArg.h b/Source/OCMock/OCMArg.h index 56280486..cdf9be0d 100644 --- a/Source/OCMock/OCMArg.h +++ b/Source/OCMock/OCMArg.h @@ -32,6 +32,22 @@ + (id)checkWithSelector:(SEL)selector onObject:(id)anObject; + (id)checkWithBlock:(BOOL (^)(id obj))block; +// Unretained object arguments are not retained by invocations on the mock, but are retained by the +// stub itself. A use case for this is when you are stubbing an argument to a method that does not +// retain its argument using an `OCMArg` variant that you do not want to keep a reference to. +// See `OCMOCK_ANY_UNRETAINED`. ++ (id)unretainedObject:(id)anObject; + +// Unsafe unretained object arguments are not retained by invocations on the mock or by the stub. +// A potential use case for this is when you are stubbing methods that do not retain their +// arguments and you want to verify dealloc conditions. An example of this would be verifying +// KVO registration/deregistration that occurs in the init/dealloc of an object. If the object were +// retained by the mocking system in any way you would never see the deregistration. +// Note that you *must* keep a reference to anObject outside this call or you will crash. +// Something like `[OCMArg unsafeUnretainedObject:[[Foo alloc] init]]` under ARC is a guaranteed +// dangling pointer problem. ++ (id)unsafeUnretainedObject:(id)anObject; + // manipulating arguments + (id *)setTo:(id)value; @@ -49,6 +65,11 @@ #define OCMOCK_ANY [OCMArg any] +// See comments on [OCMArg unretainedObject] and [OCMArg unsafeUnretainedObject]. +#define OCMOCK_UNSAFE_UNRETAINED(x) [OCMArg unsafeUnretainedObject:(x)] +#define OCMOCK_UNRETAINED(x) [OCMArg unretainedObject:(x)] +#define OCMOCK_ANY_UNRETAINED OCMOCK_UNRETAINED(OCMOCK_ANY) + #if defined(__GNUC__) && !defined(__STRICT_ANSI__) #define OCMOCK_VALUE(variable) \ ({ __typeof__(variable) __v = (variable); [NSValue value:&__v withObjCType:@encode(__typeof__(__v))]; }) diff --git a/Source/OCMock/OCMArg.m b/Source/OCMock/OCMArg.m index dbe3be07..0f584636 100644 --- a/Source/OCMock/OCMArg.m +++ b/Source/OCMock/OCMArg.m @@ -19,6 +19,7 @@ #import #import "OCMPassByRefSetter.h" #import "OCMBlockArgCaller.h" +#import "OCMUnretainedArgument.h" @implementation OCMArg @@ -81,6 +82,16 @@ + (id)checkWithBlock:(BOOL (^)(id))block return [[[OCMBlockConstraint alloc] initWithConstraintBlock:block] autorelease]; } ++ (id)unretainedObject:(id)anObject +{ + return [[[OCMUnretainedArgument alloc] initWithObject:anObject safe:YES] autorelease]; +} + ++ (id)unsafeUnretainedObject:(id)anObject +{ + return [[[OCMUnretainedArgument alloc] initWithObject:anObject safe:NO] autorelease]; +} + + (id *)setTo:(id)value { return (id *)[[[OCMPassByRefSetter alloc] initWithValue:value] autorelease]; diff --git a/Source/OCMock/OCMInvocationMatcher.h b/Source/OCMock/OCMInvocationMatcher.h index c98dcd89..39d587a6 100644 --- a/Source/OCMock/OCMInvocationMatcher.h +++ b/Source/OCMock/OCMInvocationMatcher.h @@ -21,6 +21,7 @@ NSInvocation *recordedInvocation; BOOL recordedAsClassMethod; BOOL ignoreNonObjectArgs; + NSIndexSet *unretainedArgumentIndexes; } - (void)setInvocation:(NSInvocation *)anInvocation; @@ -34,4 +35,6 @@ - (BOOL)matchesSelector:(SEL)aSelector; - (BOOL)matchesInvocation:(NSInvocation *)anInvocation; +- (NSIndexSet *)unretainedArgumentIndexes; + @end diff --git a/Source/OCMock/OCMInvocationMatcher.m b/Source/OCMock/OCMInvocationMatcher.m index 247f51d8..ffc409f3 100644 --- a/Source/OCMock/OCMInvocationMatcher.m +++ b/Source/OCMock/OCMInvocationMatcher.m @@ -21,6 +21,7 @@ #import "NSInvocation+OCMAdditions.h" #import "OCMInvocationMatcher.h" #import "OCMFunctionsPrivate.h" +#import "OCMUnretainedArgument.h" @interface NSObject(HCMatcherDummy) @@ -33,17 +34,53 @@ @implementation OCMInvocationMatcher - (void)dealloc { [recordedInvocation release]; + [unretainedArgumentIndexes release]; [super dealloc]; } +- (NSIndexSet *)unretainedArgumentIndexes +{ + return unretainedArgumentIndexes; +} + - (void)setInvocation:(NSInvocation *)anInvocation { - [recordedInvocation release]; + if (recordedInvocation != nil) + { + [NSException raise:NSInternalInconsistencyException format:@"Invocation should only be set once on a matcher"]; + } + + // Strip any "unretained arguments" from the invocation and record them. + NSMutableIndexSet *unretainedIndexes = [NSMutableIndexSet indexSet]; + NSMutableIndexSet *unsafeUnretainedIndexes = [NSMutableIndexSet indexSet]; + NSMethodSignature *signature = [anInvocation methodSignature]; + NSUInteger n = [signature numberOfArguments]; + for(NSUInteger i = 2; i < n; i++) + { + const char *argType = [signature getArgumentTypeAtIndex:i]; + if (OCMIsObjectType(argType)) + { + OCMUnretainedArgument *unretainedArg; + [anInvocation getArgument:&unretainedArg atIndex:i]; + if ([unretainedArg isKindOfClass:[OCMUnretainedArgument class]]) + { + [unretainedIndexes addIndex:i]; + if (![unretainedArg isSafe]) + { + [unsafeUnretainedIndexes addIndex:i]; + } + id realArg = [unretainedArg object]; + [anInvocation setArgument:&realArg atIndex:i]; + } + } + } + unretainedArgumentIndexes = [unretainedIndexes copy]; + // Don't do a regular -retainArguments on the invocation that we use for matching. NSInvocation // effectively does an strcpy on char* arguments which messes up matching them literally and blows // up with anyPointer (in strlen since it's not actually a C string). Also on the off-chance that // anInvocation contains self as an argument, -retainArguments would create a retain cycle. - [anInvocation retainObjectArgumentsExcludingObject:self]; + [anInvocation retainObjectArgumentsExcludingObject:self excludingObjectsAtIndexes:unsafeUnretainedIndexes]; recordedInvocation = [anInvocation retain]; } diff --git a/Source/OCMock/OCMUnretainedArgument.h b/Source/OCMock/OCMUnretainedArgument.h new file mode 100644 index 00000000..42f29950 --- /dev/null +++ b/Source/OCMock/OCMUnretainedArgument.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2015-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +// Do not use directly. See methods and comments in OCMArg.h for usage. +@interface OCMUnretainedArgument : NSObject +{ + id object; + BOOL isSafe; +} + +- (instancetype)initWithObject:(id)anObject safe:(BOOL)safe; +- (id)object; +- (BOOL)isSafe; + +@end diff --git a/Source/OCMock/OCMUnretainedArgument.m b/Source/OCMock/OCMUnretainedArgument.m new file mode 100644 index 00000000..cafe58db --- /dev/null +++ b/Source/OCMock/OCMUnretainedArgument.m @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMUnretainedArgument.h" + + +@implementation OCMUnretainedArgument + +- (instancetype)initWithObject:(id)anObject safe:(BOOL)safe +{ + if (anObject == nil) + { + [NSException raise:NSInvalidArgumentException format:@"Object must be non-nil for OCMUnretainedArgument"]; + } + if ((self = [super init])) + { + object = anObject; + isSafe = safe; + if (isSafe) + { + object = [anObject retain]; + } + } + return self; +} + +- (void)dealloc +{ + if (isSafe) + { + [object release]; + } + [super dealloc]; +} + +- (id)object +{ + return object; +} + +- (BOOL)isSafe +{ + return isSafe; +} + +@end diff --git a/Source/OCMock/OCMockObject.m b/Source/OCMock/OCMockObject.m index 2c99e283..5565f371 100644 --- a/Source/OCMock/OCMockObject.m +++ b/Source/OCMock/OCMockObject.m @@ -367,18 +367,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) { @@ -395,6 +383,18 @@ - (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. + // However we need to retain everything on anInvocation that isn't self because we expect them to + // stick around after this method returns. Use our special method to retain just what's needed. + // This still doesn't completely prevent retain cycles since any of the arguments could have a + // strong reference to self. Those will have to be broken with manual calls to -stopMocking. + [anInvocation retainObjectArgumentsExcludingObject:self excludingObjectsAtIndexes:[stub unretainedArgumentIndexes]]; + [invocations addObject:anInvocation]; + } + if(stub == nil) return NO; diff --git a/Source/OCMockTests/OCMArgTests.m b/Source/OCMockTests/OCMArgTests.m index 3f07eee2..2f86d062 100644 --- a/Source/OCMockTests/OCMArgTests.m +++ b/Source/OCMockTests/OCMArgTests.m @@ -101,4 +101,10 @@ - (void)testHandlesNonObjectPointersGracefully XCTAssertEqual([OCMArg resolveSpecialValues:nonObjectPointerValue], nonObjectPointerValue, @"Should have returned value as is."); } +- (void)testThrowsForNilArgumentToUnretainedObjects +{ + XCTAssertThrows([OCMArg unretainedObject:nil]); + XCTAssertThrows([OCMArg unsafeUnretainedObject:nil]); +} + @end diff --git a/Source/OCMockTests/OCMStubRecorderTests.m b/Source/OCMockTests/OCMStubRecorderTests.m index 2334dffb..94dc759a 100644 --- a/Source/OCMockTests/OCMStubRecorderTests.m +++ b/Source/OCMockTests/OCMStubRecorderTests.m @@ -21,6 +21,7 @@ #import "OCMExceptionReturnValueProvider.h" #import "OCMInvocationMatcher.h" #import "OCMInvocationStub.h" +#import "OCMArg.h" @interface OCMStubRecorderTests : XCTestCase @@ -67,4 +68,47 @@ - (void)testAddsExceptionReturnValueProvider } +- (void)testRecordsUnretainedObjectArgument +{ + NSString *arg = @"I love mocks."; + id mock = [OCMockObject mockForClass:[NSString class]]; + OCMStubRecorder *recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; + [(id)recorder stringByAppendingString:[OCMArg unretainedObject:arg]]; + XCTAssertEqualObjects([(OCMInvocationStub *)[recorder invocationMatcher] unretainedArgumentIndexes], [NSIndexSet indexSetWithIndex:2]); +} + +- (void)testRecordsUnsafeUnretainedObjectArgument +{ + NSString *arg = @"I love mocks."; + id mock = [OCMockObject mockForClass:[NSString class]]; + OCMStubRecorder *recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; + [(id)recorder stringByAppendingString:[OCMArg unsafeUnretainedObject:arg]]; + XCTAssertEqualObjects([(OCMInvocationStub *)[recorder invocationMatcher] unretainedArgumentIndexes], [NSIndexSet indexSetWithIndex:2]); +} + +- (void)testRecorderRetainsUnretainedObjectArgument +{ + __weak NSString *weakArg = nil; + id recorder = nil; + NSString *arg = [[NSString alloc] initWithUTF8String:"I love mocks"]; + weakArg = arg; + id mock = [OCMockObject mockForClass:[NSString class]]; + recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; + [recorder stringByAppendingString:[OCMArg unretainedObject:arg]]; + arg = nil; + XCTAssertNotNil(weakArg); +} + +- (void)testRecorderDoesNotRetainUnsafeUnretainedObjectArgument +{ + __weak NSString *weakArg = nil; + id recorder = nil; + NSString *arg = [[NSString alloc] initWithUTF8String:"I love mocks"]; + weakArg = arg; + id mock = [OCMockObject mockForClass:[NSString class]]; + recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; + [recorder stringByAppendingString:[OCMArg unsafeUnretainedObject:arg]]; + arg = nil; + XCTAssertNil(weakArg); +} @end diff --git a/Source/OCMockTests/OCMockObjectTests.m b/Source/OCMockTests/OCMockObjectTests.m index 8cfd0ae4..eab2f12e 100644 --- a/Source/OCMockTests/OCMockObjectTests.m +++ b/Source/OCMockTests/OCMockObjectTests.m @@ -192,6 +192,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"; @@ -473,6 +508,26 @@ - (void)testBlocksAreNotConsideredNonObjectArguments XCTAssertTrue(blockWasInvoked, @"Should not have ignored the block argument."); } +- (void)testAnyUnretainedObjectArgumentsAreNotRetained +{ + mock = OCMClassMock([TestClassListenerManager class]); + [[mock expect] addListener:OCMOCK_ANY_UNRETAINED]; + [[mock expect] removeListener:OCMOCK_ANY_UNRETAINED]; + TestClassListener *listener = [[TestClassListener alloc] initWithListenerManager:mock]; + listener = nil; + [mock verify]; +} + +- (void)testUnsafeUnretainedObjectArgumentsAreNotRetained +{ + mock = OCMClassMock([TestClassListenerManager class]); + TestClassListener *listener = [TestClassListener alloc]; + [[mock expect] addListener:OCMOCK_UNSAFE_UNRETAINED(listener)]; + [[mock expect] removeListener:OCMOCK_UNSAFE_UNRETAINED(listener)]; + listener = [listener initWithListenerManager:mock]; + listener = nil; + [mock verify]; +} #pragma mark returning values from stubbed methods