| OLD | NEW |
| 1 //------------------------------------------------------------------------------
--------- | 1 /* |
| 2 // $Id$ | 2 * Copyright (c) 2009-2015 Erik Doernenburg and contributors |
| 3 // Copyright (c) 2009 by Mulle Kybernetik. See License file for details. | 3 * |
| 4 //------------------------------------------------------------------------------
--------- | 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 5 * not use these files except in compliance with the License. You may obtain |
| 6 * a copy of the License at |
| 7 * |
| 8 * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 * |
| 10 * Unless required by applicable law or agreed to in writing, software |
| 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 13 * License for the specific language governing permissions and limitations |
| 14 * under the License. |
| 15 */ |
| 5 | 16 |
| 6 #import <objc/runtime.h> | 17 #import <objc/runtime.h> |
| 7 #import "OCPartialMockRecorder.h" | 18 #import "OCMockObject.h" |
| 8 #import "OCPartialMockObject.h" | 19 #import "OCPartialMockObject.h" |
| 20 #import "NSMethodSignature+OCMAdditions.h" |
| 21 #import "NSObject+OCMAdditions.h" |
| 22 #import "OCMFunctions.h" |
| 23 #import "OCMInvocationStub.h" |
| 9 | 24 |
| 10 | 25 |
| 11 @interface OCPartialMockObject (Private) | |
| 12 - (void)forwardInvocationForRealObject:(NSInvocation *)anInvocation; | |
| 13 @end | |
| 14 | |
| 15 | |
| 16 NSString *OCMRealMethodAliasPrefix = @"ocmock_replaced_"; | |
| 17 | |
| 18 @implementation OCPartialMockObject | 26 @implementation OCPartialMockObject |
| 19 | 27 |
| 20 | |
| 21 #pragma mark Mock table | |
| 22 | |
| 23 static NSMutableDictionary *mockTable; | |
| 24 | |
| 25 + (void)initialize | |
| 26 { | |
| 27 if(self == [OCPartialMockObject class]) | |
| 28 mockTable = [[NSMutableDictionary alloc] init]; | |
| 29 } | |
| 30 | |
| 31 + (void)rememberPartialMock:(OCPartialMockObject *)mock forObject:(id)anObject | |
| 32 { | |
| 33 [mockTable setObject:[NSValue valueWithNonretainedObject:mock] forKey:[N
SValue valueWithNonretainedObject:anObject]]; | |
| 34 } | |
| 35 | |
| 36 + (void)forgetPartialMockForObject:(id)anObject | |
| 37 { | |
| 38 [mockTable removeObjectForKey:[NSValue valueWithNonretainedObject:anObje
ct]]; | |
| 39 } | |
| 40 | |
| 41 + (OCPartialMockObject *)existingPartialMockForObject:(id)anObject | |
| 42 { | |
| 43 OCPartialMockObject *mock = [[mockTable objectForKey:[NSValue valueWithN
onretainedObject:anObject]] nonretainedObjectValue]; | |
| 44 if(mock == nil) | |
| 45 [NSException raise:NSInternalInconsistencyException format:@"No
partial mock for object %p", anObject]; | |
| 46 return mock; | |
| 47 } | |
| 48 | |
| 49 | |
| 50 | |
| 51 #pragma mark Initialisers, description, accessors, etc. | 28 #pragma mark Initialisers, description, accessors, etc. |
| 52 | 29 |
| 53 - (id)initWithObject:(NSObject *)anObject | 30 - (id)initWithObject:(NSObject *)anObject |
| 54 { | 31 { |
| 32 NSParameterAssert(anObject != nil); |
| 33 [self assertClassIsSupported:[anObject class]]; |
| 55 [super initWithClass:[anObject class]]; | 34 [super initWithClass:[anObject class]]; |
| 56 realObject = [anObject retain]; | 35 realObject = [anObject retain]; |
| 57 » [[self class] rememberPartialMock:self forObject:anObject]; | 36 [self prepareObjectForInstanceMethodMocking]; |
| 58 » [self setupSubclassForObject:realObject]; | |
| 59 return self; | 37 return self; |
| 60 } | 38 } |
| 61 | 39 |
| 62 - (void)dealloc | 40 - (void)dealloc |
| 63 { | 41 { |
| 64 » if(realObject != nil) | 42 » [self stopMocking]; |
| 65 » » [self stop]; | 43 » [realObject release]; |
| 66 [super dealloc]; | 44 [super dealloc]; |
| 67 } | 45 } |
| 68 | 46 |
| 69 - (NSString *)description | 47 - (NSString *)description |
| 70 { | 48 { |
| 71 » return [NSString stringWithFormat:@"OCPartialMockObject[%@]", NSStringFr
omClass(mockedClass)]; | 49 » return [NSString stringWithFormat:@"OCPartialMockObject(%@)", NSStringFr
omClass(mockedClass)]; |
| 72 } | 50 } |
| 73 | 51 |
| 74 - (NSObject *)realObject | 52 - (NSObject *)realObject |
| 75 { | 53 { |
| 76 return realObject; | 54 return realObject; |
| 77 } | 55 } |
| 78 | 56 |
| 79 - (void)stop | 57 #pragma mark Helper methods |
| 58 |
| 59 - (void)assertClassIsSupported:(Class)class |
| 80 { | 60 { |
| 81 » object_setClass(realObject, [self mockedClass]); | 61 NSString *classname = NSStringFromClass(class); |
| 82 » [realObject release]; | 62 NSString *reason = nil; |
| 83 » [[self class] forgetPartialMockForObject:realObject]; | 63 if([classname hasPrefix:@"__NSTagged"] || [classname hasPrefix:@"NSTagged"]) |
| 84 » realObject = nil; | 64 reason = [NSString stringWithFormat:@"OCMock does not support partially
mocking tagged classes; got %@", classname]; |
| 65 else if([classname hasPrefix:@"__NSCF"]) |
| 66 reason = [NSString stringWithFormat:@"OCMock does not support partially
mocking toll-free bridged classes; got %@", classname]; |
| 67 |
| 68 if(reason != nil) |
| 69 [[NSException exceptionWithName:NSInvalidArgumentException reason:reason
userInfo:nil] raise]; |
| 85 } | 70 } |
| 86 | 71 |
| 87 | 72 |
| 88 #pragma mark Subclass management | 73 #pragma mark Extending/overriding superclass behaviour |
| 89 | 74 |
| 90 - (void)setupSubclassForObject:(id)anObject | 75 - (void)stopMocking |
| 91 { | 76 { |
| 92 » Class realClass = [anObject class]; | 77 if(realObject != nil) |
| 93 » double timestamp = [NSDate timeIntervalSinceReferenceDate]; | 78 { |
| 94 » const char *className = [[NSString stringWithFormat:@"%@-%p-%f", realCla
ss, anObject, timestamp] UTF8String]; | 79 OCMSetAssociatedMockForObject(nil, realObject); |
| 95 » Class subclass = objc_allocateClassPair(realClass, className, 0); | 80 object_setClass(realObject, [self mockedClass]); |
| 96 » objc_registerClassPair(subclass); | 81 [realObject release]; |
| 97 » object_setClass(anObject, subclass); | 82 realObject = nil; |
| 98 » | 83 } |
| 99 » Method forwardInvocationMethod = class_getInstanceMethod([self class], @
selector(forwardInvocationForRealObject:)); | 84 [super stopMocking]; |
| 100 » IMP forwardInvocationImp = method_getImplementation(forwardInvocationMet
hod); | |
| 101 » const char *forwardInvocationTypes = method_getTypeEncoding(forwardInvoc
ationMethod); | |
| 102 » class_addMethod(subclass, @selector(forwardInvocation:), forwardInvocati
onImp, forwardInvocationTypes); | |
| 103 } | 85 } |
| 104 | 86 |
| 105 - (void)setupForwarderForSelector:(SEL)selector | 87 - (void)addStub:(OCMInvocationStub *)aStub |
| 106 { | 88 { |
| 107 » Class subclass = [[self realObject] class]; | 89 [super addStub:aStub]; |
| 108 » Method originalMethod = class_getInstanceMethod([subclass superclass], s
elector); | 90 if(![aStub recordedAsClassMethod]) |
| 109 » IMP originalImp = method_getImplementation(originalMethod); | 91 [self setupForwarderForSelector:[[aStub recordedInvocation] selector]]; |
| 110 | |
| 111 » IMP forwarderImp = [subclass instanceMethodForSelector:@selector(aMethod
ThatMustNotExist)]; | |
| 112 » class_addMethod(subclass, method_getName(originalMethod), forwarderImp,
method_getTypeEncoding(originalMethod)); | |
| 113 | |
| 114 » SEL aliasSelector = NSSelectorFromString([OCMRealMethodAliasPrefix strin
gByAppendingString:NSStringFromSelector(selector)]); | |
| 115 » class_addMethod(subclass, aliasSelector, originalImp, method_getTypeEnco
ding(originalMethod)); | |
| 116 } | |
| 117 | |
| 118 - (void)forwardInvocationForRealObject:(NSInvocation *)anInvocation | |
| 119 { | |
| 120 » // in here "self" is a reference to the real object, not the mock | |
| 121 » OCPartialMockObject *mock = [OCPartialMockObject existingPartialMockForO
bject:self]; | |
| 122 » if([mock handleInvocation:anInvocation] == NO) | |
| 123 » » [NSException raise:NSInternalInconsistencyException format:@"End
ed up in subclass forwarder for %@ with unstubbed method %@", | |
| 124 » » [self class], NSStringFromSelector([anInvocation selector])]; | |
| 125 } | |
| 126 | |
| 127 | |
| 128 | |
| 129 #pragma mark Overrides | |
| 130 | |
| 131 - (id)getNewRecorder | |
| 132 { | |
| 133 » return [[[OCPartialMockRecorder alloc] initWithSignatureResolver:self] a
utorelease]; | |
| 134 } | 92 } |
| 135 | 93 |
| 136 - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation | 94 - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation |
| 137 { | 95 { |
| 138 [anInvocation invokeWithTarget:realObject]; | 96 [anInvocation invokeWithTarget:realObject]; |
| 139 } | 97 } |
| 140 | 98 |
| 141 | 99 |
| 100 #pragma mark Subclass management |
| 101 |
| 102 - (void)prepareObjectForInstanceMethodMocking |
| 103 { |
| 104 OCMSetAssociatedMockForObject(self, realObject); |
| 105 |
| 106 /* dynamically create a subclass and set it as the class of the object */ |
| 107 Class subclass = OCMCreateSubclass(mockedClass, realObject); |
| 108 object_setClass(realObject, subclass); |
| 109 |
| 110 /* point forwardInvocation: of the object to the implementation in the mock
*/ |
| 111 Method myForwardMethod = class_getInstanceMethod([self mockObjectClass],
@selector(forwardInvocationForRealObject:)); |
| 112 IMP myForwardIMP = method_getImplementation(myForwardMethod); |
| 113 class_addMethod(subclass, @selector(forwardInvocation:), myForwardIMP, metho
d_getTypeEncoding(myForwardMethod)); |
| 114 |
| 115 /* do the same for forwardingTargetForSelector, remember existing imp with a
lias selector */ |
| 116 Method myForwardingTargetMethod = class_getInstanceMethod([self mockObjectCl
ass], @selector(forwardingTargetForSelectorForRealObject:)); |
| 117 IMP myForwardingTargetIMP = method_getImplementation(myForwardingTargetMetho
d); |
| 118 IMP originalForwardingTargetIMP = [mockedClass instanceMethodForSelector:@se
lector(forwardingTargetForSelector:)]; |
| 119 class_addMethod(subclass, @selector(forwardingTargetForSelector:), myForward
ingTargetIMP, method_getTypeEncoding(myForwardingTargetMethod)); |
| 120 class_addMethod(subclass, @selector(ocmock_replaced_forwardingTargetForSelec
tor:), originalForwardingTargetIMP, method_getTypeEncoding(myForwardingTargetMet
hod)); |
| 121 |
| 122 /* We also override the -class method to return the original class */ |
| 123 Method myObjectClassMethod = class_getInstanceMethod([self mockObjectClass],
@selector(classForRealObject)); |
| 124 const char *objectClassTypes = method_getTypeEncoding(myObjectClassMethod); |
| 125 IMP myObjectClassImp = method_getImplementation(myObjectClassMethod); |
| 126 class_addMethod(subclass, @selector(class), myObjectClassImp, objectClassTyp
es); |
| 127 |
| 128 /* Adding forwarder for most instance methods to allow for verify after run
*/ |
| 129 NSArray *methodBlackList = @[@"class", @"forwardingTargetForSelector:", @"me
thodSignatureForSelector:", @"forwardInvocation:", |
| 130 @"allowsWeakReference", @"retainWeakReference", @"isBlock"]; |
| 131 [NSObject enumerateMethodsInClass:mockedClass usingBlock:^(Class cls, SEL se
l) { |
| 132 if((cls == [NSObject class]) || (cls == [NSProxy class])) |
| 133 return; |
| 134 NSString *className = NSStringFromClass(cls); |
| 135 NSString *selName = NSStringFromSelector(sel); |
| 136 if(([className hasPrefix:@"NS"] || [className hasPrefix:@"UI"]) && |
| 137 ([selName hasPrefix:@"_"] || [selName hasSuffix:@"_"])) |
| 138 return; |
| 139 if([methodBlackList containsObject:selName]) |
| 140 return; |
| 141 @try |
| 142 { |
| 143 [self setupForwarderForSelector:sel]; |
| 144 } |
| 145 @catch(NSException *e) |
| 146 { |
| 147 // ignore for now |
| 148 } |
| 149 }]; |
| 150 } |
| 151 |
| 152 - (void)setupForwarderForSelector:(SEL)sel |
| 153 { |
| 154 SEL aliasSelector = OCMAliasForOriginalSelector(sel); |
| 155 if(class_getInstanceMethod(object_getClass(realObject), aliasSelector) != NU
LL) |
| 156 return; |
| 157 |
| 158 Method originalMethod = class_getInstanceMethod(mockedClass, sel); |
| 159 IMP originalIMP = method_getImplementation(originalMethod); |
| 160 const char *types = method_getTypeEncoding(originalMethod); |
| 161 /* Might be NULL if the selector is forwarded to another class */ |
| 162 // TODO: check the fallback implementation is actually sufficient |
| 163 if(types == NULL) |
| 164 types = ([[mockedClass instanceMethodSignatureForSelector:sel] fullObjCT
ypes]); |
| 165 |
| 166 Class subclass = object_getClass([self realObject]); |
| 167 IMP forwarderIMP = [mockedClass instanceMethodForwarderForSelector:sel]; |
| 168 class_replaceMethod(subclass, sel, forwarderIMP, types); |
| 169 class_addMethod(subclass, aliasSelector, originalIMP, types); |
| 170 } |
| 171 |
| 172 |
| 173 // Implementation of the -class method; return the Class that was reported with
[realObject class] prior to mocking |
| 174 - (Class)classForRealObject |
| 175 { |
| 176 // in here "self" is a reference to the real object, not the mock |
| 177 OCPartialMockObject *mock = OCMGetAssociatedMockForObject(self); |
| 178 if(mock == nil) |
| 179 [NSException raise:NSInternalInconsistencyException format:@"No partial
mock for object %p", self]; |
| 180 return [mock mockedClass]; |
| 181 } |
| 182 |
| 183 |
| 184 - (id)forwardingTargetForSelectorForRealObject:(SEL)sel |
| 185 { |
| 186 // in here "self" is a reference to the real object, not the mock |
| 187 OCPartialMockObject *mock = OCMGetAssociatedMockForObject(self); |
| 188 if(mock == nil) |
| 189 [NSException raise:NSInternalInconsistencyException format:@"No partial
mock for object %p", self]; |
| 190 if([mock handleSelector:sel]) |
| 191 return self; |
| 192 |
| 193 return [self ocmock_replaced_forwardingTargetForSelector:sel]; |
| 194 } |
| 195 |
| 196 // Make the compiler happy in -forwardingTargetForSelectorForRealObject: becaus
e it can't find the message⦠|
| 197 - (id)ocmock_replaced_forwardingTargetForSelector:(SEL)sel |
| 198 { |
| 199 return nil; |
| 200 } |
| 201 |
| 202 |
| 203 - (void)forwardInvocationForRealObject:(NSInvocation *)anInvocation |
| 204 { |
| 205 // in here "self" is a reference to the real object, not the mock |
| 206 OCPartialMockObject *mock = OCMGetAssociatedMockForObject(self); |
| 207 if(mock == nil) |
| 208 [NSException raise:NSInternalInconsistencyException format:@"No partial
mock for object %p", self]; |
| 209 |
| 210 if([mock handleInvocation:anInvocation] == NO) |
| 211 { |
| 212 [anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation sele
ctor])]; |
| 213 [anInvocation invoke]; |
| 214 } |
| 215 } |
| 216 |
| 217 |
| 142 @end | 218 @end |
| OLD | NEW |