From 60fbf491d221b4c47dacda84c740dbc7746600e7 Mon Sep 17 00:00:00 2001 From: Dave MacLachlan Date: Wed, 20 May 2020 17:22:20 -0700 Subject: [PATCH] Add support for unretained and unsafeunretained arguments for stubs. Allows marking an argument in a stub as being either 'unsafe' or 'unsafeunretained'. An unsafe object argument is retained by the stub, but not by invocations on the mock. An unsafe unretained object argument is not retained by the stub or by invocations on the mock. This allows for mocking of methods that do not retain their arguments. This should simplify testing of methods that are called in dealloc, and give people a way out of other retain-loop problems. --- Source/OCMock.xcodeproj/project.pbxproj | 24 +++++++++ Source/OCMock/NSInvocation+OCMAdditions.h | 2 +- Source/OCMock/NSInvocation+OCMAdditions.m | 31 ++++++------ Source/OCMock/OCMArg.h | 21 ++++++++ Source/OCMock/OCMArg.m | 11 +++++ Source/OCMock/OCMInvocationMatcher.h | 3 ++ Source/OCMock/OCMInvocationMatcher.m | 41 +++++++++++++++- Source/OCMock/OCMUnretainedArgument.h | 30 ++++++++++++ Source/OCMock/OCMUnretainedArgument.m | 59 +++++++++++++++++++++++ Source/OCMock/OCMockObject.m | 24 ++++----- Source/OCMockTests/OCMArgTests.m | 6 +++ Source/OCMockTests/OCMStubRecorderTests.m | 44 +++++++++++++++++ Source/OCMockTests/OCMockObjectTests.m | 55 +++++++++++++++++++++ 13 files changed, 320 insertions(+), 31 deletions(-) create mode 100644 Source/OCMock/OCMUnretainedArgument.h create mode 100644 Source/OCMock/OCMUnretainedArgument.m 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