FBXCTestDaemonsProxy.m 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. /**
  2. * Copyright (c) 2015-present, Facebook, Inc.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the BSD-style license found in the
  6. * LICENSE file in the root directory of this source tree. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. */
  9. #import "FBXCTestDaemonsProxy.h"
  10. #import <objc/runtime.h>
  11. #import "FBConfiguration.h"
  12. #import "FBErrorBuilder.h"
  13. #import "FBExceptions.h"
  14. #import "FBLogger.h"
  15. #import "FBRunLoopSpinner.h"
  16. #import "XCTestDriver.h"
  17. #import "XCTRunnerDaemonSession.h"
  18. #import "XCUIApplication.h"
  19. #import "XCUIDevice.h"
  20. #define LAUNCH_APP_TIMEOUT_SEC 300
  21. static void (*originalLaunchAppMethod)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *));
  22. static void swizzledLaunchApp(id self, SEL _cmd, NSString *path, NSString *bundleID,
  23. NSArray *arguments, NSDictionary *environment,
  24. void (^reply)(_Bool, NSError *))
  25. {
  26. __block BOOL isSuccessful;
  27. __block NSError *error;
  28. dispatch_semaphore_t sem = dispatch_semaphore_create(0);
  29. originalLaunchAppMethod(self, _cmd, path, bundleID, arguments, environment, ^(BOOL passed, NSError *innerError) {
  30. isSuccessful = passed;
  31. error = innerError;
  32. dispatch_semaphore_signal(sem);
  33. });
  34. int64_t timeoutNs = (int64_t)(LAUNCH_APP_TIMEOUT_SEC * NSEC_PER_SEC);
  35. if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) {
  36. NSString *message = [NSString stringWithFormat:@"The application '%@' cannot be launched within %d seconds timeout",
  37. bundleID ?: path, LAUNCH_APP_TIMEOUT_SEC];
  38. @throw [NSException exceptionWithName:FBTimeoutException reason:message userInfo:nil];
  39. }
  40. if (!isSuccessful || nil != error) {
  41. [FBLogger logFmt:@"%@", error.description];
  42. NSString *message = error.description ?: [NSString stringWithFormat:@"The application '%@' is not installed on the device under test",
  43. bundleID ?: path];
  44. @throw [NSException exceptionWithName:FBApplicationMissingException reason:message userInfo:nil];
  45. }
  46. reply(isSuccessful, error);
  47. }
  48. @implementation FBXCTestDaemonsProxy
  49. #pragma clang diagnostic push
  50. #pragma clang diagnostic ignored "-Wobjc-load-method"
  51. + (void)load
  52. {
  53. [self.class swizzleLaunchApp];
  54. }
  55. #pragma clang diagnostic pop
  56. + (void)swizzleLaunchApp {
  57. Method original = class_getInstanceMethod([XCTRunnerDaemonSession class],
  58. @selector(launchApplicationWithPath:bundleID:arguments:environment:completion:));
  59. if (original == nil) {
  60. [FBLogger log:@"Could not find method -[XCTRunnerDaemonSession launchApplicationWithPath:]"];
  61. return;
  62. }
  63. // Workaround for https://github.com/appium/WebDriverAgent/issues/702
  64. originalLaunchAppMethod = (void(*)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *))) method_getImplementation(original);
  65. method_setImplementation(original, (IMP)swizzledLaunchApp);
  66. }
  67. + (id<XCTestManager_ManagerInterface>)testRunnerProxy
  68. {
  69. static id<XCTestManager_ManagerInterface> proxy = nil;
  70. if ([FBConfiguration shouldUseSingletonTestManager]) {
  71. static dispatch_once_t onceToken;
  72. dispatch_once(&onceToken, ^{
  73. [FBLogger logFmt:@"Using singleton test manager"];
  74. proxy = [self.class retrieveTestRunnerProxy];
  75. });
  76. } else {
  77. [FBLogger logFmt:@"Using general test manager"];
  78. proxy = [self.class retrieveTestRunnerProxy];
  79. }
  80. NSAssert(proxy != NULL, @"Could not determine testRunnerProxy", proxy);
  81. return proxy;
  82. }
  83. + (id<XCTestManager_ManagerInterface>)retrieveTestRunnerProxy
  84. {
  85. return ((XCTRunnerDaemonSession *)[XCTRunnerDaemonSession sharedSession]).daemonProxy;
  86. }
  87. + (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error
  88. {
  89. __block NSError *innerError = nil;
  90. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  91. void (^errorHandler)(NSError *) = ^(NSError *invokeError) {
  92. if (nil != invokeError) {
  93. innerError = invokeError;
  94. }
  95. completion();
  96. };
  97. XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) {
  98. errorHandler(invokeError);
  99. };
  100. [[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) {
  101. handlerBlock(record, invokeError);
  102. }];
  103. }];
  104. if (nil != innerError) {
  105. if (error) {
  106. *error = innerError;
  107. }
  108. return NO;
  109. }
  110. return YES;
  111. }
  112. + (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError *__autoreleasing*)error
  113. {
  114. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  115. if (![session respondsToSelector:@selector(openURL:usingApplication:completion:)]) {
  116. [[[FBErrorBuilder builder]
  117. withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs with given application"]
  118. buildError:error];
  119. return NO;
  120. }
  121. __block NSError *innerError = nil;
  122. __block BOOL didSucceed = NO;
  123. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  124. [session openURL:url usingApplication:bundleId completion:^(bool result, NSError *invokeError) {
  125. if (nil != invokeError) {
  126. innerError = invokeError;
  127. } else {
  128. didSucceed = result;
  129. }
  130. completion();
  131. }];
  132. }];
  133. if (nil != innerError && error) {
  134. *error = innerError;
  135. }
  136. return didSucceed;
  137. }
  138. + (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasing*)error
  139. {
  140. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  141. if (![session respondsToSelector:@selector(openDefaultApplicationForURL:completion:)]) {
  142. [[[FBErrorBuilder builder]
  143. withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
  144. buildError:error];
  145. return NO;
  146. }
  147. __block NSError *innerError = nil;
  148. __block BOOL didSucceed = NO;
  149. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  150. [session openDefaultApplicationForURL:url completion:^(bool result, NSError *invokeError) {
  151. if (nil != invokeError) {
  152. innerError = invokeError;
  153. } else {
  154. didSucceed = result;
  155. }
  156. completion();
  157. }];
  158. }];
  159. if (nil != innerError && error) {
  160. *error = innerError;
  161. }
  162. return didSucceed;
  163. }
  164. #if !TARGET_OS_TV
  165. + (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleasing*)error
  166. {
  167. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  168. if (![session respondsToSelector:@selector(setSimulatedLocation:completion:)]) {
  169. [[[FBErrorBuilder builder]
  170. withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
  171. buildError:error];
  172. return NO;
  173. }
  174. if (![session supportsLocationSimulation]) {
  175. [[[FBErrorBuilder builder]
  176. withDescriptionFormat:@"Your device does not support location simulation"]
  177. buildError:error];
  178. return NO;
  179. }
  180. __block NSError *innerError = nil;
  181. __block BOOL didSucceed = NO;
  182. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  183. [session setSimulatedLocation:location completion:^(bool result, NSError *invokeError) {
  184. if (nil != invokeError) {
  185. innerError = invokeError;
  186. } else {
  187. didSucceed = result;
  188. }
  189. completion();
  190. }];
  191. }];
  192. if (nil != innerError && error) {
  193. *error = innerError;
  194. }
  195. return didSucceed;
  196. }
  197. + (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error;
  198. {
  199. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  200. if (![session respondsToSelector:@selector(getSimulatedLocationWithReply:)]) {
  201. [[[FBErrorBuilder builder]
  202. withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
  203. buildError:error];
  204. return nil;
  205. }
  206. if (![session supportsLocationSimulation]) {
  207. [[[FBErrorBuilder builder]
  208. withDescriptionFormat:@"Your device does not support location simulation"]
  209. buildError:error];
  210. return nil;
  211. }
  212. __block NSError *innerError = nil;
  213. __block CLLocation *location = nil;
  214. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  215. [session getSimulatedLocationWithReply:^(CLLocation *reply, NSError *invokeError) {
  216. if (nil != invokeError) {
  217. innerError = invokeError;
  218. } else {
  219. location = reply;
  220. }
  221. completion();
  222. }];
  223. }];
  224. if (nil != innerError && error) {
  225. *error = innerError;
  226. }
  227. return location;
  228. }
  229. + (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error
  230. {
  231. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  232. if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) {
  233. if (error) {
  234. [[[FBErrorBuilder builder]
  235. withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
  236. buildError:error];
  237. }
  238. return NO;
  239. }
  240. if (![session supportsLocationSimulation]) {
  241. if (error) {
  242. [[[FBErrorBuilder builder]
  243. withDescriptionFormat:@"Your device does not support location simulation"]
  244. buildError:error];
  245. }
  246. return NO;
  247. }
  248. __block NSError *innerError = nil;
  249. __block BOOL didSucceed = NO;
  250. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  251. [session clearSimulatedLocationWithReply:^(bool result, NSError *invokeError) {
  252. if (nil != invokeError) {
  253. innerError = invokeError;
  254. } else {
  255. didSucceed = result;
  256. }
  257. completion();
  258. }];
  259. }];
  260. if (nil != innerError && error) {
  261. *error = innerError;
  262. }
  263. return didSucceed;
  264. }
  265. #endif
  266. @end