FPRSelectorInstrumentorTest.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. // Copyright 2020 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import <XCTest/XCTest.h>
  15. #import "FirebasePerformance/Sources/Instrumentation/FPRSelectorInstrumentor.h"
  16. /** A class used to test that swizzling init methods that funnel to the designated
  17. * initializer works as intended. */
  18. @interface FPRFunneledInitTestClass : NSObject
  19. @property(nonatomic) id object;
  20. @end
  21. @implementation FPRFunneledInitTestClass
  22. - (instancetype)init {
  23. self = [self initWithObject:nil];
  24. return self;
  25. }
  26. - (instancetype)initWithObject:(id)object {
  27. self = [super init];
  28. if (self) {
  29. _object = object;
  30. }
  31. return self;
  32. }
  33. @end
  34. @interface FPRSelectorInstrumentorTest : XCTestCase
  35. @end
  36. @implementation FPRSelectorInstrumentorTest
  37. - (void)testInitWithSelector {
  38. FPRSelectorInstrumentor *instrumentor =
  39. [[FPRSelectorInstrumentor alloc] initWithSelector:@selector(description)
  40. class:[NSObject class]
  41. isClassSelector:NO];
  42. XCTAssertNotNil(instrumentor);
  43. }
  44. - (void)testInstanceMethodSwizzle {
  45. NSString *expectedDescription = @"Not the description you expected!";
  46. FPRSelectorInstrumentor *instrumentor =
  47. [[FPRSelectorInstrumentor alloc] initWithSelector:@selector(description)
  48. class:[NSObject class]
  49. isClassSelector:NO];
  50. [instrumentor setReplacingBlock:^NSString *(id _self) {
  51. return expectedDescription;
  52. }];
  53. [instrumentor swizzle];
  54. NSString *returnedDescription = [[[NSObject alloc] init] description];
  55. XCTAssertEqualObjects(returnedDescription, expectedDescription);
  56. [instrumentor unswizzle];
  57. }
  58. - (void)testClassMethodSwizzle {
  59. NSString *expectedDescription = @"Not the description you expected!";
  60. FPRSelectorInstrumentor *instrumentor =
  61. [[FPRSelectorInstrumentor alloc] initWithSelector:@selector(description)
  62. class:[NSObject class]
  63. isClassSelector:YES];
  64. [instrumentor setReplacingBlock:^NSString *(id _self) {
  65. return expectedDescription;
  66. }];
  67. [instrumentor swizzle];
  68. NSString *returnedDescription = [NSObject description];
  69. XCTAssertEqualObjects(returnedDescription, expectedDescription);
  70. [instrumentor unswizzle];
  71. }
  72. - (void)testInstanceMethodSwizzleWithOriginalImpInvocation {
  73. __block BOOL wasInvoked = NO;
  74. NSObject *object = [[NSObject alloc] init];
  75. NSString *originalDescription = [object description];
  76. SEL selector = @selector(description);
  77. Class instrumentedClass = [NSObject class];
  78. FPRSelectorInstrumentor *instrumentor =
  79. [[FPRSelectorInstrumentor alloc] initWithSelector:selector
  80. class:instrumentedClass
  81. isClassSelector:NO];
  82. IMP originalIMP = instrumentor.currentIMP;
  83. [instrumentor setReplacingBlock:^NSString *(id _self) {
  84. wasInvoked = YES;
  85. typedef NSString *(*OriginalImp)(id, SEL);
  86. return ((OriginalImp)originalIMP)(_self, selector);
  87. }];
  88. [instrumentor swizzle];
  89. NSString *newDescription = [object description];
  90. XCTAssertTrue(wasInvoked);
  91. XCTAssertEqualObjects(newDescription, originalDescription);
  92. [instrumentor unswizzle];
  93. }
  94. - (void)testClassMethodSwizzleWithOriginalImpInvocation {
  95. __block BOOL wasInvoked = NO;
  96. NSString *originalDescription = [NSObject description];
  97. SEL selector = @selector(description);
  98. Class instrumentedClass = [NSObject class];
  99. FPRSelectorInstrumentor *instrumentor =
  100. [[FPRSelectorInstrumentor alloc] initWithSelector:selector
  101. class:instrumentedClass
  102. isClassSelector:YES];
  103. IMP originalIMP = instrumentor.currentIMP;
  104. [instrumentor setReplacingBlock:^NSString *(id _self) {
  105. wasInvoked = YES;
  106. typedef NSString *(*OriginalImp)(id, SEL);
  107. return ((OriginalImp)originalIMP)(_self, selector);
  108. }];
  109. [instrumentor swizzle];
  110. NSString *newDescription = [NSObject description];
  111. XCTAssertTrue(wasInvoked);
  112. XCTAssertEqualObjects(newDescription, originalDescription);
  113. [instrumentor unswizzle];
  114. }
  115. - (void)testSwizzlingFunneledInstanceMethodsWithOriginalImpInvocation {
  116. __block BOOL initWasInvoked = NO;
  117. __block BOOL initWithObjectWasInvoked = NO;
  118. FPRSelectorInstrumentor *initInstrumentor =
  119. [[FPRSelectorInstrumentor alloc] initWithSelector:@selector(init)
  120. class:[FPRFunneledInitTestClass class]
  121. isClassSelector:NO];
  122. FPRSelectorInstrumentor *initWithObjectInstrumentor =
  123. [[FPRSelectorInstrumentor alloc] initWithSelector:@selector(initWithObject:)
  124. class:[FPRFunneledInitTestClass class]
  125. isClassSelector:NO];
  126. IMP originalIMPInit = initInstrumentor.currentIMP;
  127. IMP originalIMPInitWithObject = initWithObjectInstrumentor.currentIMP;
  128. [initInstrumentor setReplacingBlock:^id(id FPRFunneledInitTestClassInstance) {
  129. initWasInvoked = YES;
  130. typedef FPRFunneledInitTestClass *(*OriginalImp)(id, SEL);
  131. return ((OriginalImp)originalIMPInit)(FPRFunneledInitTestClassInstance, @selector(init));
  132. }];
  133. [initWithObjectInstrumentor
  134. setReplacingBlock:^id(id FPRFunneledInitTestClassInstance, id object) {
  135. initWithObjectWasInvoked = YES;
  136. typedef FPRFunneledInitTestClass *(*OriginalImp)(id, SEL, id);
  137. return ((OriginalImp)originalIMPInitWithObject)(FPRFunneledInitTestClassInstance,
  138. @selector(initWithObject:), @(1));
  139. }];
  140. [initInstrumentor swizzle];
  141. [initWithObjectInstrumentor swizzle];
  142. FPRFunneledInitTestClass *object1 = [[FPRFunneledInitTestClass alloc] init];
  143. XCTAssertTrue(initWasInvoked);
  144. XCTAssertTrue(initWithObjectWasInvoked);
  145. XCTAssertNotNil(object1);
  146. FPRFunneledInitTestClass *object2 = [[FPRFunneledInitTestClass alloc] initWithObject:@(3)];
  147. XCTAssertNotNil(object2);
  148. [initInstrumentor unswizzle];
  149. [initWithObjectInstrumentor unswizzle];
  150. }
  151. /** Tests that the init method of an object is swizzleable. For ARC-related reasons, init and new
  152. * cannot be swizzled without invoking the original selector.
  153. */
  154. - (void)testThatInitIsSwizzleable {
  155. SEL selector = @selector(init);
  156. Class instrumentedClass = [FPRFunneledInitTestClass class];
  157. FPRSelectorInstrumentor *instrumentor =
  158. [[FPRSelectorInstrumentor alloc] initWithSelector:selector
  159. class:instrumentedClass
  160. isClassSelector:NO];
  161. IMP originalIMP = instrumentor.currentIMP;
  162. __block BOOL wasInvoked = NO;
  163. [instrumentor setReplacingBlock:^id(id _objectInstance) {
  164. wasInvoked = YES;
  165. typedef NSObject *(*OriginalImp)(id, SEL);
  166. return ((OriginalImp)originalIMP)(_objectInstance, selector);
  167. }];
  168. [instrumentor swizzle];
  169. NSObject *object = [[FPRFunneledInitTestClass alloc] init];
  170. XCTAssertNotNil(object);
  171. XCTAssertTrue(wasInvoked);
  172. [instrumentor unswizzle];
  173. }
  174. /** Tests that swizzling a subclass of a class cluster works properly.
  175. *
  176. * If the subclass of a class cluster's superclass is swizzled after the superclass and the
  177. * subclass doesn't provide a concrete implementation of method, the method obtained by the runtime
  178. * will find the already-swizzled superclass method. In this case, the method that existed *before*
  179. * swizzling the superclass method should be returned. The reason is that the
  180. * FPRSelectorInstrumentor may die while the swizzled IMP lives on. If a subclass captures the
  181. * now-dead FPRSelectorInstrumentor's IMP as the "original" IMP, then this will cause a
  182. * null-pointer dereference when a call-through is attempted. To resolve this, a mapping of
  183. * new->original IMPs must be maintained, and if the "original" IMP ends up actually being one of
  184. * our swizzled IMPs, we should instead return the IMP that existed before.
  185. */
  186. - (void)testSwizzlingSubclassOfClassClusterAfterSuperclassCallsNonSwizzledImp {
  187. // A typedef that wraps the completion handler type.
  188. typedef void (^DataTaskCompletionHandler)(NSData *_Nullable, NSURLResponse *_Nullable,
  189. NSError *_Nullable);
  190. NSMutableArray<FPRSelectorInstrumentor *> *superclassInstrumentors =
  191. [[NSMutableArray alloc] init];
  192. NSMutableArray<FPRSelectorInstrumentor *> *subclassInstrumentors = [[NSMutableArray alloc] init];
  193. Class superclass = [NSURLSession class];
  194. Class subclass = [[NSURLSession sharedSession] class];
  195. XCTAssertNotEqual(superclass, subclass);
  196. SEL currentSelector = nil;
  197. FPRSelectorInstrumentor *currentInstrumentor = nil;
  198. // Swizzle the superclass selector.
  199. currentSelector = @selector(dataTaskWithRequest:);
  200. currentInstrumentor = [[FPRSelectorInstrumentor alloc] initWithSelector:currentSelector
  201. class:superclass
  202. isClassSelector:NO];
  203. IMP originalIMP = currentInstrumentor.currentIMP;
  204. [currentInstrumentor setReplacingBlock:^(id session, NSURLRequest *request) {
  205. typedef NSURLSessionDataTask *(*OriginalImp)(id, SEL, NSURLRequest *);
  206. return ((OriginalImp)originalIMP)(session, currentSelector, request);
  207. }];
  208. [superclassInstrumentors addObject:currentInstrumentor];
  209. currentSelector = @selector(dataTaskWithRequest:completionHandler:);
  210. currentInstrumentor = [[FPRSelectorInstrumentor alloc] initWithSelector:currentSelector
  211. class:superclass
  212. isClassSelector:NO];
  213. originalIMP = currentInstrumentor.currentIMP;
  214. [currentInstrumentor setReplacingBlock:^(id session, NSURLRequest *request,
  215. DataTaskCompletionHandler completionHandler) {
  216. DataTaskCompletionHandler wrappedCompletionHandler = nil;
  217. if (completionHandler) {
  218. wrappedCompletionHandler = ^(NSData *data, NSURLResponse *response, NSError *error) {
  219. completionHandler(data, response, error);
  220. };
  221. }
  222. typedef NSURLSessionDataTask *(*OriginalImp)(id, SEL, NSURLRequest *,
  223. DataTaskCompletionHandler);
  224. return ((OriginalImp)originalIMP)(session, currentSelector, request, wrappedCompletionHandler);
  225. }];
  226. [superclassInstrumentors addObject:currentInstrumentor];
  227. // Swizzle the subclass selectors.
  228. currentSelector = @selector(dataTaskWithRequest:);
  229. currentInstrumentor = [[FPRSelectorInstrumentor alloc] initWithSelector:currentSelector
  230. class:subclass
  231. isClassSelector:NO];
  232. originalIMP = currentInstrumentor.currentIMP;
  233. [currentInstrumentor setReplacingBlock:^(id session, NSURLRequest *request) {
  234. typedef NSURLSessionDataTask *(*OriginalImp)(id, SEL, NSURLRequest *);
  235. return ((OriginalImp)originalIMP)(session, currentSelector, request);
  236. }];
  237. [subclassInstrumentors addObject:currentInstrumentor];
  238. currentSelector = @selector(dataTaskWithRequest:completionHandler:);
  239. currentInstrumentor = [[FPRSelectorInstrumentor alloc] initWithSelector:currentSelector
  240. class:subclass
  241. isClassSelector:NO];
  242. originalIMP = currentInstrumentor.currentIMP;
  243. [currentInstrumentor setReplacingBlock:^(id session, NSURLRequest *request,
  244. DataTaskCompletionHandler completionHandler) {
  245. DataTaskCompletionHandler wrappedCompletionHandler = nil;
  246. if (completionHandler) {
  247. wrappedCompletionHandler = ^(NSData *data, NSURLResponse *response, NSError *error) {
  248. completionHandler(data, response, error);
  249. };
  250. }
  251. typedef NSURLSessionDataTask *(*OriginalImp)(id, SEL, NSURLRequest *,
  252. DataTaskCompletionHandler);
  253. return ((OriginalImp)originalIMP)(session, currentSelector, request, wrappedCompletionHandler);
  254. }];
  255. [subclassInstrumentors addObject:currentInstrumentor];
  256. for (FPRSelectorInstrumentor *superclassInstrumentor in superclassInstrumentors) {
  257. [superclassInstrumentor swizzle];
  258. }
  259. for (FPRSelectorInstrumentor *subclassInstrumentor in subclassInstrumentors) {
  260. [subclassInstrumentor swizzle];
  261. }
  262. for (FPRSelectorInstrumentor *superclassInstrumentor in superclassInstrumentors) {
  263. [superclassInstrumentor unswizzle];
  264. }
  265. [superclassInstrumentors removeAllObjects];
  266. superclassInstrumentors = nil;
  267. NSURL *url = [NSURL URLWithString:@"https://abc.xyz"];
  268. __block BOOL completionHandlerCalled = NO;
  269. void (^completionHandler)(NSData *_Nullable, NSURLResponse *_Nullable, NSError *_Nullable) =
  270. ^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) {
  271. completionHandlerCalled = YES;
  272. };
  273. NSURLSessionDataTask *session = [[NSURLSession sharedSession] dataTaskWithURL:url
  274. completionHandler:completionHandler];
  275. XCTAssertNotNil(session);
  276. for (FPRSelectorInstrumentor *subclassInstrumentor in subclassInstrumentors) {
  277. [subclassInstrumentor unswizzle];
  278. }
  279. }
  280. /** Tests attempting to swizzle non-existent/unimplemented method (like @dynamic) returns nil. */
  281. - (void)testNonexistentMethodReturnsNil {
  282. FPRSelectorInstrumentor *instrumentor =
  283. [[FPRSelectorInstrumentor alloc] initWithSelector:NSSelectorFromString(@"randomMethod")
  284. class:[NSURLSession class]
  285. isClassSelector:NO];
  286. XCTAssertNil(instrumentor);
  287. [instrumentor unswizzle];
  288. }
  289. @end