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 "OCMFunctionsPrivate.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 |