FBXCTestDaemonsProxy.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  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 "FBScreenRecordingPromise.h"
  17. #import "FBScreenRecordingRequest.h"
  18. #import "XCTestDriver.h"
  19. #import "XCTRunnerDaemonSession.h"
  20. #import "XCUIApplication.h"
  21. #import "XCUIDevice.h"
  22. #define LAUNCH_APP_TIMEOUT_SEC 300
  23. static void (*originalLaunchAppMethod)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *));
  24. static void swizzledLaunchApp(id self, SEL _cmd, NSString *path, NSString *bundleID,
  25. NSArray *arguments, NSDictionary *environment,
  26. void (^reply)(_Bool, NSError *))
  27. {
  28. __block BOOL isSuccessful;
  29. __block NSError *error;
  30. dispatch_semaphore_t sem = dispatch_semaphore_create(0);
  31. originalLaunchAppMethod(self, _cmd, path, bundleID, arguments, environment, ^(BOOL passed, NSError *innerError) {
  32. isSuccessful = passed;
  33. error = innerError;
  34. dispatch_semaphore_signal(sem);
  35. });
  36. int64_t timeoutNs = (int64_t)(LAUNCH_APP_TIMEOUT_SEC * NSEC_PER_SEC);
  37. if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) {
  38. NSString *message = [NSString stringWithFormat:@"The application '%@' cannot be launched within %d seconds timeout",
  39. bundleID ?: path, LAUNCH_APP_TIMEOUT_SEC];
  40. @throw [NSException exceptionWithName:FBTimeoutException reason:message userInfo:nil];
  41. }
  42. if (!isSuccessful || nil != error) {
  43. [FBLogger logFmt:@"%@", error.description];
  44. NSString *message = error.description ?: [NSString stringWithFormat:@"The application '%@' is not installed on the device under test",
  45. bundleID ?: path];
  46. @throw [NSException exceptionWithName:FBApplicationMissingException reason:message userInfo:nil];
  47. }
  48. reply(isSuccessful, error);
  49. }
  50. @implementation FBXCTestDaemonsProxy
  51. #pragma clang diagnostic push
  52. #pragma clang diagnostic ignored "-Wobjc-load-method"
  53. + (void)load
  54. {
  55. [self.class swizzleLaunchApp];
  56. }
  57. #pragma clang diagnostic pop
  58. + (void)swizzleLaunchApp {
  59. Method original = class_getInstanceMethod([XCTRunnerDaemonSession class],
  60. @selector(launchApplicationWithPath:bundleID:arguments:environment:completion:));
  61. if (original == nil) {
  62. [FBLogger log:@"Could not find method -[XCTRunnerDaemonSession launchApplicationWithPath:]"];
  63. return;
  64. }
  65. // Workaround for https://github.com/appium/WebDriverAgent/issues/702
  66. originalLaunchAppMethod = (void(*)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *))) method_getImplementation(original);
  67. method_setImplementation(original, (IMP)swizzledLaunchApp);
  68. }
  69. + (id<XCTestManager_ManagerInterface>)testRunnerProxy
  70. {
  71. static id<XCTestManager_ManagerInterface> proxy = nil;
  72. if ([FBConfiguration shouldUseSingletonTestManager]) {
  73. static dispatch_once_t onceToken;
  74. dispatch_once(&onceToken, ^{
  75. [FBLogger logFmt:@"Using singleton test manager"];
  76. proxy = [self.class retrieveTestRunnerProxy];
  77. });
  78. } else {
  79. [FBLogger logFmt:@"Using general test manager"];
  80. proxy = [self.class retrieveTestRunnerProxy];
  81. }
  82. NSAssert(proxy != NULL, @"Could not determine testRunnerProxy", proxy);
  83. return proxy;
  84. }
  85. + (id<XCTestManager_ManagerInterface>)retrieveTestRunnerProxy
  86. {
  87. return ((XCTRunnerDaemonSession *)[XCTRunnerDaemonSession sharedSession]).daemonProxy;
  88. }
  89. + (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error
  90. {
  91. __block NSError *innerError = nil;
  92. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  93. void (^errorHandler)(NSError *) = ^(NSError *invokeError) {
  94. if (nil != invokeError) {
  95. innerError = invokeError;
  96. }
  97. completion();
  98. };
  99. XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) {
  100. errorHandler(invokeError);
  101. };
  102. [[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) {
  103. handlerBlock(record, invokeError);
  104. }];
  105. }];
  106. if (nil != innerError) {
  107. if (error) {
  108. *error = innerError;
  109. }
  110. return NO;
  111. }
  112. return YES;
  113. }
  114. + (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError *__autoreleasing*)error
  115. {
  116. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  117. if (![session respondsToSelector:@selector(openURL:usingApplication:completion:)]) {
  118. return [[[FBErrorBuilder builder]
  119. withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs with given application"]
  120. buildError:error];
  121. }
  122. __block NSError *innerError = nil;
  123. __block BOOL didSucceed = NO;
  124. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  125. [session openURL:url usingApplication:bundleId completion:^(bool result, NSError *invokeError) {
  126. if (nil != invokeError) {
  127. innerError = invokeError;
  128. } else {
  129. didSucceed = result;
  130. }
  131. completion();
  132. }];
  133. }];
  134. if (nil != innerError && error) {
  135. *error = innerError;
  136. }
  137. return didSucceed;
  138. }
  139. + (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasing*)error
  140. {
  141. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  142. if (![session respondsToSelector:@selector(openDefaultApplicationForURL:completion:)]) {
  143. return [[[FBErrorBuilder builder]
  144. withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
  145. buildError:error];
  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. return [[[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. }
  173. if (![session supportsLocationSimulation]) {
  174. return [[[FBErrorBuilder builder]
  175. withDescriptionFormat:@"Your device does not support location simulation"]
  176. buildError:error];
  177. }
  178. __block NSError *innerError = nil;
  179. __block BOOL didSucceed = NO;
  180. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  181. [session setSimulatedLocation:location completion:^(bool result, NSError *invokeError) {
  182. if (nil != invokeError) {
  183. innerError = invokeError;
  184. } else {
  185. didSucceed = result;
  186. }
  187. completion();
  188. }];
  189. }];
  190. if (nil != innerError && error) {
  191. *error = innerError;
  192. }
  193. return didSucceed;
  194. }
  195. + (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error;
  196. {
  197. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  198. if (![session respondsToSelector:@selector(getSimulatedLocationWithReply:)]) {
  199. [[[FBErrorBuilder builder]
  200. withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
  201. buildError:error];
  202. return nil;
  203. }
  204. if (![session supportsLocationSimulation]) {
  205. [[[FBErrorBuilder builder]
  206. withDescriptionFormat:@"Your device does not support location simulation"]
  207. buildError:error];
  208. return nil;
  209. }
  210. __block NSError *innerError = nil;
  211. __block CLLocation *location = nil;
  212. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  213. [session getSimulatedLocationWithReply:^(CLLocation *reply, NSError *invokeError) {
  214. if (nil != invokeError) {
  215. innerError = invokeError;
  216. } else {
  217. location = reply;
  218. }
  219. completion();
  220. }];
  221. }];
  222. if (nil != innerError && error) {
  223. *error = innerError;
  224. }
  225. return location;
  226. }
  227. + (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error
  228. {
  229. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  230. if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) {
  231. return [[[FBErrorBuilder builder]
  232. withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
  233. buildError:error];
  234. }
  235. if (![session supportsLocationSimulation]) {
  236. return [[[FBErrorBuilder builder]
  237. withDescriptionFormat:@"Your device does not support location simulation"]
  238. buildError:error];
  239. }
  240. __block NSError *innerError = nil;
  241. __block BOOL didSucceed = NO;
  242. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  243. [session clearSimulatedLocationWithReply:^(bool result, NSError *invokeError) {
  244. if (nil != invokeError) {
  245. innerError = invokeError;
  246. } else {
  247. didSucceed = result;
  248. }
  249. completion();
  250. }];
  251. }];
  252. if (nil != innerError && error) {
  253. *error = innerError;
  254. }
  255. return didSucceed;
  256. }
  257. #endif
  258. + (FBScreenRecordingPromise *)startScreenRecordingWithRequest:(FBScreenRecordingRequest *)request
  259. error:(NSError *__autoreleasing*)error
  260. {
  261. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  262. if (![session respondsToSelector:@selector(startScreenRecordingWithRequest:withReply:)]) {
  263. [[[FBErrorBuilder builder]
  264. withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"]
  265. buildError:error];
  266. return nil;
  267. }
  268. if (![session supportsScreenRecording]) {
  269. [[[FBErrorBuilder builder]
  270. withDescriptionFormat:@"Your device does not support screen recording"]
  271. buildError:error];
  272. return nil;
  273. }
  274. id nativeRequest = [request toNativeRequestWithError:error];
  275. if (nil == nativeRequest) {
  276. return nil;
  277. }
  278. __block id futureMetadata = nil;
  279. __block NSError *innerError = nil;
  280. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  281. [session startScreenRecordingWithRequest:nativeRequest withReply:^(id reply, NSError *invokeError) {
  282. if (nil == invokeError) {
  283. futureMetadata = reply;
  284. } else {
  285. innerError = invokeError;
  286. }
  287. completion();
  288. }];
  289. }];
  290. if (nil != innerError) {
  291. if (error) {
  292. *error = innerError;
  293. }
  294. return nil;
  295. }
  296. return [[FBScreenRecordingPromise alloc] initWithNativePromise:futureMetadata];
  297. }
  298. + (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid error:(NSError *__autoreleasing*)error
  299. {
  300. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  301. if (![session respondsToSelector:@selector(stopScreenRecordingWithUUID:withReply:)]) {
  302. return [[[FBErrorBuilder builder]
  303. withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"]
  304. buildError:error];
  305. }
  306. if (![session supportsScreenRecording]) {
  307. return [[[FBErrorBuilder builder]
  308. withDescriptionFormat:@"Your device does not support screen recording"]
  309. buildError:error];
  310. }
  311. __block NSError *innerError = nil;
  312. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  313. [session stopScreenRecordingWithUUID:uuid withReply:^(NSError *invokeError) {
  314. if (nil != invokeError) {
  315. innerError = invokeError;
  316. }
  317. completion();
  318. }];
  319. }];
  320. if (nil != innerError && error) {
  321. *error = innerError;
  322. }
  323. return nil == innerError;
  324. }
  325. @end