FPRSelectorInstrumentorTest.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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. #pragma mark - Unswizzle based tests
  45. #ifndef SWIFT_PACKAGE
  46. - (void)testInstanceMethodSwizzle {
  47. NSString *expectedDescription = @"Not the description you expected!";
  48. FPRSelectorInstrumentor *instrumentor =
  49. [[FPRSelectorInstrumentor alloc] initWithSelector:@selector(description)
  50. class:[NSObject class]
  51. isClassSelector:NO];
  52. [instrumentor setReplacingBlock:^NSString *(id _self) {
  53. return expectedDescription;
  54. }];
  55. [instrumentor swizzle];
  56. NSString *returnedDescription = [[[NSObject alloc] init] description];
  57. XCTAssertEqualObjects(returnedDescription, expectedDescription);
  58. [instrumentor unswizzle];
  59. }
  60. - (void)testClassMethodSwizzle {
  61. NSString *expectedDescription = @"Not the description you expected!";
  62. FPRSelectorInstrumentor *instrumentor =
  63. [[FPRSelectorInstrumentor alloc] initWithSelector:@selector(description)
  64. class:[NSObject class]
  65. isClassSelector:YES];
  66. [instrumentor setReplacingBlock:^NSString *(id _self) {
  67. return expectedDescription;
  68. }];
  69. [instrumentor swizzle];
  70. NSString *returnedDescription = [NSObject description];
  71. XCTAssertEqualObjects(returnedDescription, expectedDescription);
  72. [instrumentor unswizzle];
  73. }
  74. - (void)testInstanceMethodSwizzleWithOriginalImpInvocation {
  75. __block BOOL wasInvoked = NO;
  76. NSObject *object = [[NSObject alloc] init];
  77. NSString *originalDescription = [object description];
  78. SEL selector = @selector(description);
  79. Class instrumentedClass = [NSObject class];
  80. FPRSelectorInstrumentor *instrumentor =
  81. [[FPRSelectorInstrumentor alloc] initWithSelector:selector
  82. class:instrumentedClass
  83. isClassSelector:NO];
  84. IMP originalIMP = instrumentor.currentIMP;
  85. [instrumentor setReplacingBlock:^NSString *(id _self) {
  86. wasInvoked = YES;
  87. typedef NSString *(*OriginalImp)(id, SEL);
  88. return ((OriginalImp)originalIMP)(_self, selector);
  89. }];
  90. [instrumentor swizzle];
  91. NSString *newDescription = [object description];
  92. XCTAssertTrue(wasInvoked);
  93. XCTAssertEqualObjects(newDescription, originalDescription);
  94. [instrumentor unswizzle];
  95. }
  96. - (void)testClassMethodSwizzleWithOriginalImpInvocation {
  97. __block BOOL wasInvoked = NO;
  98. NSString *originalDescription = [NSObject description];
  99. SEL selector = @selector(description);
  100. Class instrumentedClass = [NSObject class];
  101. FPRSelectorInstrumentor *instrumentor =
  102. [[FPRSelectorInstrumentor alloc] initWithSelector:selector
  103. class:instrumentedClass
  104. isClassSelector:YES];
  105. IMP originalIMP = instrumentor.currentIMP;
  106. [instrumentor setReplacingBlock:^NSString *(id _self) {
  107. wasInvoked = YES;
  108. typedef NSString *(*OriginalImp)(id, SEL);
  109. return ((OriginalImp)originalIMP)(_self, selector);
  110. }];
  111. [instrumentor swizzle];
  112. NSString *newDescription = [NSObject description];
  113. XCTAssertTrue(wasInvoked);
  114. XCTAssertEqualObjects(newDescription, originalDescription);
  115. [instrumentor unswizzle];
  116. }
  117. - (void)testSwizzlingFunneledInstanceMethodsWithOriginalImpInvocation {
  118. __block BOOL initWasInvoked = NO;
  119. __block BOOL initWithObjectWasInvoked = NO;
  120. FPRSelectorInstrumentor *initInstrumentor =
  121. [[FPRSelectorInstrumentor alloc] initWithSelector:@selector(init)
  122. class:[FPRFunneledInitTestClass class]
  123. isClassSelector:NO];
  124. FPRSelectorInstrumentor *initWithObjectInstrumentor =
  125. [[FPRSelectorInstrumentor alloc] initWithSelector:@selector(initWithObject:)
  126. class:[FPRFunneledInitTestClass class]
  127. isClassSelector:NO];
  128. IMP originalIMPInit = initInstrumentor.currentIMP;
  129. IMP originalIMPInitWithObject = initWithObjectInstrumentor.currentIMP;
  130. [initInstrumentor setReplacingBlock:^id(id FPRFunneledInitTestClassInstance) {
  131. initWasInvoked = YES;
  132. typedef FPRFunneledInitTestClass *(*OriginalImp)(id, SEL);
  133. return ((OriginalImp)originalIMPInit)(FPRFunneledInitTestClassInstance, @selector(init));
  134. }];
  135. [initWithObjectInstrumentor
  136. setReplacingBlock:^id(id FPRFunneledInitTestClassInstance, id object) {
  137. initWithObjectWasInvoked = YES;
  138. typedef FPRFunneledInitTestClass *(*OriginalImp)(id, SEL, id);
  139. return ((OriginalImp)originalIMPInitWithObject)(FPRFunneledInitTestClassInstance,
  140. @selector(initWithObject:), @(1));
  141. }];
  142. [initInstrumentor swizzle];
  143. [initWithObjectInstrumentor swizzle];
  144. FPRFunneledInitTestClass *object1 = [[FPRFunneledInitTestClass alloc] init];
  145. XCTAssertTrue(initWasInvoked);
  146. XCTAssertTrue(initWithObjectWasInvoked);
  147. XCTAssertNotNil(object1);
  148. FPRFunneledInitTestClass *object2 = [[FPRFunneledInitTestClass alloc] initWithObject:@(3)];
  149. XCTAssertNotNil(object2);
  150. [initInstrumentor unswizzle];
  151. [initWithObjectInstrumentor unswizzle];
  152. }
  153. /** Tests that the init method of an object is swizzleable. For ARC-related reasons, init and new
  154. * cannot be swizzled without invoking the original selector.
  155. */
  156. - (void)testThatInitIsSwizzleable {
  157. SEL selector = @selector(init);
  158. Class instrumentedClass = [FPRFunneledInitTestClass class];
  159. FPRSelectorInstrumentor *instrumentor =
  160. [[FPRSelectorInstrumentor alloc] initWithSelector:selector
  161. class:instrumentedClass
  162. isClassSelector:NO];
  163. IMP originalIMP = instrumentor.currentIMP;
  164. __block BOOL wasInvoked = NO;
  165. [instrumentor setReplacingBlock:^id(id _objectInstance) {
  166. wasInvoked = YES;
  167. typedef NSObject *(*OriginalImp)(id, SEL);
  168. return ((OriginalImp)originalIMP)(_objectInstance, selector);
  169. }];
  170. [instrumentor swizzle];
  171. NSObject *object = [[FPRFunneledInitTestClass alloc] init];
  172. XCTAssertNotNil(object);
  173. XCTAssertTrue(wasInvoked);
  174. [instrumentor unswizzle];
  175. }
  176. /** Tests that swizzling a subclass of a class cluster works properly.
  177. *
  178. * If the subclass of a class cluster's superclass is swizzled after the superclass and the
  179. * subclass doesn't provide a concrete implementation of method, the method obtained by the runtime
  180. * will find the already-swizzled superclass method. In this case, the method that existed *before*
  181. * swizzling the superclass method should be returned. The reason is that the
  182. * FPRSelectorInstrumentor may die while the swizzled IMP lives on. If a subclass captures the
  183. * now-dead FPRSelectorInstrumentor's IMP as the "original" IMP, then this will cause a
  184. * null-pointer dereference when a call-through is attempted. To resolve this, a mapping of
  185. * new->original IMPs must be maintained, and if the "original" IMP ends up actually being one of
  186. * our swizzled IMPs, we should instead return the IMP that existed before.
  187. */
  188. - (void)testSwizzlingSubclassOfClassClusterAfterSuperclassCallsNonSwizzledImp {
  189. // A typedef that wraps the completion handler type.
  190. typedef void (^DataTaskCompletionHandler)(NSData *_Nullable, NSURLResponse *_Nullable,
  191. NSError *_Nullable);
  192. NSMutableArray<FPRSelectorInstrumentor *> *superclassInstrumentors =
  193. [[NSMutableArray alloc] init];
  194. NSMutableArray<FPRSelectorInstrumentor *> *subclassInstrumentors = [[NSMutableArray alloc] init];
  195. Class superclass = [NSURLSession class];
  196. Class subclass = [[NSURLSession sharedSession] class];
  197. XCTAssertNotEqual(superclass, subclass);
  198. SEL currentSelector = nil;
  199. FPRSelectorInstrumentor *currentInstrumentor = nil;
  200. // Swizzle the superclass selector.
  201. currentSelector = @selector(dataTaskWithRequest:);
  202. currentInstrumentor = [[FPRSelectorInstrumentor alloc] initWithSelector:currentSelector
  203. class:superclass
  204. isClassSelector:NO];
  205. IMP originalIMP = currentInstrumentor.currentIMP;
  206. [currentInstrumentor setReplacingBlock:^(id session, NSURLRequest *request) {
  207. typedef NSURLSessionDataTask *(*OriginalImp)(id, SEL, NSURLRequest *);
  208. return ((OriginalImp)originalIMP)(session, currentSelector, request);
  209. }];
  210. [superclassInstrumentors addObject:currentInstrumentor];
  211. currentSelector = @selector(dataTaskWithRequest:completionHandler:);
  212. currentInstrumentor = [[FPRSelectorInstrumentor alloc] initWithSelector:currentSelector
  213. class:superclass
  214. isClassSelector:NO];
  215. originalIMP = currentInstrumentor.currentIMP;
  216. [currentInstrumentor setReplacingBlock:^(id session, NSURLRequest *request,
  217. DataTaskCompletionHandler completionHandler) {
  218. DataTaskCompletionHandler wrappedCompletionHandler = nil;
  219. if (completionHandler) {
  220. wrappedCompletionHandler = ^(NSData *data, NSURLResponse *response, NSError *error) {
  221. completionHandler(data, response, error);
  222. };
  223. }
  224. typedef NSURLSessionDataTask *(*OriginalImp)(id, SEL, NSURLRequest *,
  225. DataTaskCompletionHandler);
  226. return ((OriginalImp)originalIMP)(session, currentSelector, request, wrappedCompletionHandler);
  227. }];
  228. [superclassInstrumentors addObject:currentInstrumentor];
  229. // Swizzle the subclass selectors.
  230. currentSelector = @selector(dataTaskWithRequest:);
  231. currentInstrumentor = [[FPRSelectorInstrumentor alloc] initWithSelector:currentSelector
  232. class:subclass
  233. isClassSelector:NO];
  234. originalIMP = currentInstrumentor.currentIMP;
  235. [currentInstrumentor setReplacingBlock:^(id session, NSURLRequest *request) {
  236. typedef NSURLSessionDataTask *(*OriginalImp)(id, SEL, NSURLRequest *);
  237. return ((OriginalImp)originalIMP)(session, currentSelector, request);
  238. }];
  239. [subclassInstrumentors addObject:currentInstrumentor];
  240. currentSelector = @selector(dataTaskWithRequest:completionHandler:);
  241. currentInstrumentor = [[FPRSelectorInstrumentor alloc] initWithSelector:currentSelector
  242. class:subclass
  243. isClassSelector:NO];
  244. originalIMP = currentInstrumentor.currentIMP;
  245. [currentInstrumentor setReplacingBlock:^(id session, NSURLRequest *request,
  246. DataTaskCompletionHandler completionHandler) {
  247. DataTaskCompletionHandler wrappedCompletionHandler = nil;
  248. if (completionHandler) {
  249. wrappedCompletionHandler = ^(NSData *data, NSURLResponse *response, NSError *error) {
  250. completionHandler(data, response, error);
  251. };
  252. }
  253. typedef NSURLSessionDataTask *(*OriginalImp)(id, SEL, NSURLRequest *,
  254. DataTaskCompletionHandler);
  255. return ((OriginalImp)originalIMP)(session, currentSelector, request, wrappedCompletionHandler);
  256. }];
  257. [subclassInstrumentors addObject:currentInstrumentor];
  258. for (FPRSelectorInstrumentor *superclassInstrumentor in superclassInstrumentors) {
  259. [superclassInstrumentor swizzle];
  260. }
  261. for (FPRSelectorInstrumentor *subclassInstrumentor in subclassInstrumentors) {
  262. [subclassInstrumentor swizzle];
  263. }
  264. for (FPRSelectorInstrumentor *superclassInstrumentor in superclassInstrumentors) {
  265. [superclassInstrumentor unswizzle];
  266. }
  267. [superclassInstrumentors removeAllObjects];
  268. superclassInstrumentors = nil;
  269. NSURL *url = [NSURL URLWithString:@"https://abc.xyz"];
  270. __block BOOL completionHandlerCalled = NO;
  271. void (^completionHandler)(NSData *_Nullable, NSURLResponse *_Nullable, NSError *_Nullable) =
  272. ^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) {
  273. completionHandlerCalled = YES;
  274. };
  275. NSURLSessionDataTask *session = [[NSURLSession sharedSession] dataTaskWithURL:url
  276. completionHandler:completionHandler];
  277. XCTAssertNotNil(session);
  278. for (FPRSelectorInstrumentor *subclassInstrumentor in subclassInstrumentors) {
  279. [subclassInstrumentor unswizzle];
  280. }
  281. }
  282. #endif // SWIFT_PACKAGE
  283. /** Tests attempting to swizzle non-existent/unimplemented method (like @dynamic) returns nil. */
  284. - (void)testNonexistentMethodReturnsNil {
  285. FPRSelectorInstrumentor *instrumentor =
  286. [[FPRSelectorInstrumentor alloc] initWithSelector:NSSelectorFromString(@"randomMethod")
  287. class:[NSURLSession class]
  288. isClassSelector:NO];
  289. XCTAssertNil(instrumentor);
  290. [instrumentor unswizzle];
  291. }
  292. @end