| 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" |
| 9 | 20 #import "NSMethodSignature+OCMAdditions.h" |
| 10 | 21 #import "NSObject+OCMAdditions.h" |
| 11 @interface OCPartialMockObject (Private) | 22 #import "OCMFunctions.h" |
| 12 - (void)forwardInvocationForRealObject:(NSInvocation *)anInvocation; | 23 #import "OCMInvocationStub.h" |
| 13 @end | |
| 14 | |
| 15 | |
| 16 NSString *OCMRealMethodAliasPrefix = @"ocmock_replaced_"; | |
| 17 | 24 |
| 18 @implementation OCPartialMockObject | 25 @implementation OCPartialMockObject |
| 19 | 26 |
| 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. | 27 #pragma mark Initialisers, description, accessors, etc. |
| 52 | 28 |
| 53 - (id)initWithObject:(NSObject *)anObject | 29 - (id)initWithObject:(NSObject *)anObject |
| 54 { | 30 { |
| 55 » [super initWithClass:[anObject class]]; | 31 NSParameterAssert(anObject != nil); |
| 56 » realObject = [anObject retain]; | 32 [self assertClassIsSupported:[anObject class]]; |
| 57 » [[self class] rememberPartialMock:self forObject:anObject]; | 33 [super initWithClass:[anObject class]]; |
| 58 » [self setupSubclassForObject:realObject]; | 34 realObject = [anObject retain]; |
| 59 » return self; | 35 [self prepareObjectForInstanceMethodMocking]; |
| 36 return self; |
| 60 } | 37 } |
| 61 | 38 |
| 62 - (void)dealloc | 39 - (void)dealloc |
| 63 { | 40 { |
| 64 » if(realObject != nil) | 41 [self stopMocking]; |
| 65 » » [self stop]; | 42 [realObject release]; |
| 66 » [super dealloc]; | 43 [super dealloc]; |
| 67 } | 44 } |
| 68 | 45 |
| 69 - (NSString *)description | 46 - (NSString *)description |
| 70 { | 47 { |
| 71 » return [NSString stringWithFormat:@"OCPartialMockObject[%@]", NSStringFr
omClass(mockedClass)]; | 48 return [NSString stringWithFormat:@"OCPartialMockObject(%@)", |
| 49 NSStringFromClass(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 |
| 80 { | 58 |
| 81 » object_setClass(realObject, [self mockedClass]); | 59 - (void)assertClassIsSupported:(Class) class { |
| 82 » [realObject release]; | 60 NSString* classname = NSStringFromClass(class); |
| 83 » [[self class] forgetPartialMockForObject:realObject]; | 61 NSString* reason = nil; |
| 84 » realObject = nil; | 62 if ([classname hasPrefix:@"__NSTagged"] || [classname hasPrefix:@"NSTagged"]) |
| 63 reason = [NSString |
| 64 stringWithFormat: |
| 65 @"OCMock does not support partially mocking tagged classes; got %@", |
| 66 classname]; |
| 67 else if ([classname hasPrefix:@"__NSCF"]) |
| 68 reason = |
| 69 [NSString stringWithFormat:@"OCMock does not support partially mocking " |
| 70 @"toll-free bridged classes; got %@", |
| 71 classname]; |
| 72 |
| 73 if (reason != nil) |
| 74 [[NSException exceptionWithName:NSInvalidArgumentException |
| 75 reason:reason |
| 76 userInfo:nil] raise]; |
| 77 } |
| 78 #pragma mark Extending/overriding superclass behaviour |
| 79 |
| 80 - (void)stopMocking { |
| 81 if (realObject != nil) { |
| 82 OCMSetAssociatedMockForObject(nil, realObject); |
| 83 object_setClass(realObject, [self mockedClass]); |
| 84 [realObject release]; |
| 85 realObject = nil; |
| 86 } |
| 87 [super stopMocking]; |
| 88 } |
| 89 |
| 90 - (void)addStub:(OCMInvocationStub*)aStub { |
| 91 [super addStub:aStub]; |
| 92 if (![aStub recordedAsClassMethod]) |
| 93 [self setupForwarderForSelector:[[aStub recordedInvocation] selector]]; |
| 94 } |
| 95 |
| 96 - (void)handleUnRecordedInvocation:(NSInvocation*)anInvocation { |
| 97 [anInvocation invokeWithTarget:realObject]; |
| 98 } |
| 99 |
| 100 #pragma mark Subclass management |
| 101 |
| 102 - (void)prepareObjectForInstanceMethodMocking { |
| 103 OCMSetAssociatedMockForObject(self, realObject); |
| 104 |
| 105 /* dynamically create a subclass and set it as the class of the object */ |
| 106 Class subclass = OCMCreateSubclass(mockedClass, realObject); |
| 107 object_setClass(realObject, subclass); |
| 108 |
| 109 /* point forwardInvocation: of the object to the implementation in the mock */ |
| 110 Method myForwardMethod = class_getInstanceMethod( |
| 111 [self mockObjectClass], @selector(forwardInvocationForRealObject:)); |
| 112 IMP myForwardIMP = method_getImplementation(myForwardMethod); |
| 113 class_addMethod(subclass, @selector(forwardInvocation:), myForwardIMP, |
| 114 method_getTypeEncoding(myForwardMethod)); |
| 115 |
| 116 /* do the same for forwardingTargetForSelector, remember existing imp with |
| 117 * alias selector */ |
| 118 Method myForwardingTargetMethod = class_getInstanceMethod( |
| 119 [self mockObjectClass], |
| 120 @selector(forwardingTargetForSelectorForRealObject:)); |
| 121 IMP myForwardingTargetIMP = |
| 122 method_getImplementation(myForwardingTargetMethod); |
| 123 IMP originalForwardingTargetIMP = [mockedClass |
| 124 instanceMethodForSelector:@selector(forwardingTargetForSelector:)]; |
| 125 class_addMethod(subclass, @selector(forwardingTargetForSelector:), |
| 126 myForwardingTargetIMP, |
| 127 method_getTypeEncoding(myForwardingTargetMethod)); |
| 128 class_addMethod(subclass, |
| 129 @selector(ocmock_replaced_forwardingTargetForSelector:), |
| 130 originalForwardingTargetIMP, |
| 131 method_getTypeEncoding(myForwardingTargetMethod)); |
| 132 |
| 133 /* We also override the -class method to return the original class */ |
| 134 Method myObjectClassMethod = class_getInstanceMethod( |
| 135 [self mockObjectClass], @selector(classForRealObject)); |
| 136 const char* objectClassTypes = method_getTypeEncoding(myObjectClassMethod); |
| 137 IMP myObjectClassImp = method_getImplementation(myObjectClassMethod); |
| 138 class_addMethod(subclass, @selector(class), myObjectClassImp, |
| 139 objectClassTypes); |
| 140 |
| 141 /* Adding forwarder for most instance methods to allow for verify after run */ |
| 142 NSArray* methodBlackList = @[ |
| 143 @"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", |
| 144 @"forwardInvocation:", @"allowsWeakReference", @"retainWeakReference", |
| 145 @"isBlock" |
| 146 ]; |
| 147 [NSObject |
| 148 enumerateMethodsInClass:mockedClass |
| 149 usingBlock:^(Class cls, SEL sel) { |
| 150 if ((cls == [NSObject class]) || (cls == [NSProxy class])) |
| 151 return; |
| 152 NSString* className = NSStringFromClass(cls); |
| 153 NSString* selName = NSStringFromSelector(sel); |
| 154 if (([className hasPrefix:@"NS"] || |
| 155 [className hasPrefix:@"UI"]) && |
| 156 ([selName hasPrefix:@"_"] || [selName hasSuffix:@"_"])) |
| 157 return; |
| 158 if ([methodBlackList containsObject:selName]) |
| 159 return; |
| 160 @try { |
| 161 [self setupForwarderForSelector:sel]; |
| 162 } @catch (NSException* e) { |
| 163 // ignore for now |
| 164 } |
| 165 }]; |
| 166 } |
| 167 |
| 168 - (void)setupForwarderForSelector:(SEL)sel { |
| 169 SEL aliasSelector = OCMAliasForOriginalSelector(sel); |
| 170 if (class_getInstanceMethod(object_getClass(realObject), aliasSelector) != |
| 171 NULL) |
| 172 return; |
| 173 |
| 174 Method originalMethod = class_getInstanceMethod(mockedClass, sel); |
| 175 IMP originalIMP = method_getImplementation(originalMethod); |
| 176 const char* types = method_getTypeEncoding(originalMethod); |
| 177 /* Might be NULL if the selector is forwarded to another class */ |
| 178 // TODO: check the fallback implementation is actually sufficient |
| 179 if (types == NULL) |
| 180 types = |
| 181 ([[mockedClass instanceMethodSignatureForSelector:sel] fullObjCTypes]); |
| 182 |
| 183 Class subclass = object_getClass([self realObject]); |
| 184 IMP forwarderIMP = [mockedClass instanceMethodForwarderForSelector:sel]; |
| 185 class_replaceMethod(subclass, sel, forwarderIMP, types); |
| 186 class_addMethod(subclass, aliasSelector, originalIMP, types); |
| 187 } |
| 188 |
| 189 // Implementation of the -class method; return the Class that was reported with |
| 190 // [realObject class] prior to mocking |
| 191 - (Class)classForRealObject { |
| 192 // in here "self" is a reference to the real object, not the mock |
| 193 OCPartialMockObject* mock = OCMGetAssociatedMockForObject(self); |
| 194 if (mock == nil) |
| 195 [NSException raise:NSInternalInconsistencyException |
| 196 format:@"No partial mock for object %p", self]; |
| 197 return [mock mockedClass]; |
| 198 } |
| 199 |
| 200 - (id)forwardingTargetForSelectorForRealObject:(SEL)sel { |
| 201 // in here "self" is a reference to the real object, not the mock |
| 202 OCPartialMockObject* mock = OCMGetAssociatedMockForObject(self); |
| 203 if (mock == nil) |
| 204 [NSException raise:NSInternalInconsistencyException |
| 205 format:@"No partial mock for object %p", self]; |
| 206 if ([mock handleSelector:sel]) |
| 207 return self; |
| 208 |
| 209 return [self ocmock_replaced_forwardingTargetForSelector:sel]; |
| 210 } |
| 211 |
| 212 // Make the compiler happy in -forwardingTargetForSelectorForRealObject: |
| 213 // because it can't find the message⦠|
| 214 - (id)ocmock_replaced_forwardingTargetForSelector:(SEL)sel { |
| 215 return nil; |
| 216 } |
| 217 |
| 218 - (void)forwardInvocationForRealObject:(NSInvocation*)anInvocation { |
| 219 // in here "self" is a reference to the real object, not the mock |
| 220 OCPartialMockObject* mock = OCMGetAssociatedMockForObject(self); |
| 221 if (mock == nil) |
| 222 [NSException raise:NSInternalInconsistencyException |
| 223 format:@"No partial mock for object %p", self]; |
| 224 |
| 225 if ([mock handleInvocation:anInvocation] == NO) { |
| 226 [anInvocation |
| 227 setSelector:OCMAliasForOriginalSelector([anInvocation selector])]; |
| 228 [anInvocation invoke]; |
| 229 } |
| 85 } | 230 } |
| 86 | 231 |
| 87 | 232 |
| 88 #pragma mark Subclass management | |
| 89 | |
| 90 - (void)setupSubclassForObject:(id)anObject | |
| 91 { | |
| 92 Class realClass = [anObject class]; | |
| 93 double timestamp = [NSDate timeIntervalSinceReferenceDate]; | |
| 94 const char *className = [[NSString stringWithFormat:@"%@-%p-%f", realCla
ss, anObject, timestamp] UTF8String]; | |
| 95 Class subclass = objc_allocateClassPair(realClass, className, 0); | |
| 96 objc_registerClassPair(subclass); | |
| 97 object_setClass(anObject, subclass); | |
| 98 | |
| 99 Method forwardInvocationMethod = class_getInstanceMethod([self class], @
selector(forwardInvocationForRealObject:)); | |
| 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 } | |
| 104 | |
| 105 - (void)setupForwarderForSelector:(SEL)selector | |
| 106 { | |
| 107 Class subclass = [[self realObject] class]; | |
| 108 Method originalMethod = class_getInstanceMethod([subclass superclass], s
elector); | |
| 109 IMP originalImp = method_getImplementation(originalMethod); | |
| 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 } | |
| 135 | |
| 136 - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation | |
| 137 { | |
| 138 [anInvocation invokeWithTarget:realObject]; | |
| 139 } | |
| 140 | |
| 141 | |
| 142 @end | 233 @end |
| OLD | NEW |