FBXCTestDaemonsProxy.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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. #pragma clang diagnostic push
  66. #pragma clang diagnostic ignored "-Wcast-function-type-strict"
  67. // Workaround for https://github.com/appium/WebDriverAgent/issues/702
  68. originalLaunchAppMethod = (void(*)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *))) method_getImplementation(original);
  69. method_setImplementation(original, (IMP)swizzledLaunchApp);
  70. #pragma clang diagnostic pop
  71. }
  72. + (id<XCTestManager_ManagerInterface>)testRunnerProxy
  73. {
  74. static id<XCTestManager_ManagerInterface> proxy = nil;
  75. if ([FBConfiguration shouldUseSingletonTestManager]) {
  76. static dispatch_once_t onceToken;
  77. dispatch_once(&onceToken, ^{
  78. [FBLogger logFmt:@"Using singleton test manager"];
  79. proxy = [self.class retrieveTestRunnerProxy];
  80. });
  81. } else {
  82. [FBLogger logFmt:@"Using general test manager"];
  83. proxy = [self.class retrieveTestRunnerProxy];
  84. }
  85. NSAssert(proxy != NULL, @"Could not determine testRunnerProxy", proxy);
  86. return proxy;
  87. }
  88. + (id<XCTestManager_ManagerInterface>)retrieveTestRunnerProxy
  89. {
  90. return ((XCTRunnerDaemonSession *)[XCTRunnerDaemonSession sharedSession]).daemonProxy;
  91. }
  92. + (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error
  93. {
  94. __block NSError *innerError = nil;
  95. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  96. void (^errorHandler)(NSError *) = ^(NSError *invokeError) {
  97. if (nil != invokeError) {
  98. innerError = invokeError;
  99. }
  100. completion();
  101. };
  102. XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) {
  103. errorHandler(invokeError);
  104. };
  105. [[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) {
  106. handlerBlock(record, invokeError);
  107. }];
  108. }];
  109. if (nil != innerError) {
  110. if (error) {
  111. *error = innerError;
  112. }
  113. return NO;
  114. }
  115. return YES;
  116. }
  117. + (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError *__autoreleasing*)error
  118. {
  119. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  120. if (![session respondsToSelector:@selector(openURL:usingApplication:completion:)]) {
  121. return [[[FBErrorBuilder builder]
  122. withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs with given application"]
  123. buildError:error];
  124. }
  125. __block NSError *innerError = nil;
  126. __block BOOL didSucceed = NO;
  127. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  128. [session openURL:url usingApplication:bundleId completion:^(bool result, NSError *invokeError) {
  129. if (nil != invokeError) {
  130. innerError = invokeError;
  131. } else {
  132. didSucceed = result;
  133. }
  134. completion();
  135. }];
  136. }];
  137. if (nil != innerError && error) {
  138. *error = innerError;
  139. }
  140. return didSucceed;
  141. }
  142. + (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasing*)error
  143. {
  144. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  145. if (![session respondsToSelector:@selector(openDefaultApplicationForURL:completion:)]) {
  146. return [[[FBErrorBuilder builder]
  147. withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
  148. buildError:error];
  149. }
  150. __block NSError *innerError = nil;
  151. __block BOOL didSucceed = NO;
  152. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  153. [session openDefaultApplicationForURL:url completion:^(bool result, NSError *invokeError) {
  154. if (nil != invokeError) {
  155. innerError = invokeError;
  156. } else {
  157. didSucceed = result;
  158. }
  159. completion();
  160. }];
  161. }];
  162. if (nil != innerError && error) {
  163. *error = innerError;
  164. }
  165. return didSucceed;
  166. }
  167. #if !TARGET_OS_TV
  168. + (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleasing*)error
  169. {
  170. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  171. if (![session respondsToSelector:@selector(setSimulatedLocation:completion:)]) {
  172. return [[[FBErrorBuilder builder]
  173. withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
  174. buildError:error];
  175. }
  176. if (![session supportsLocationSimulation]) {
  177. return [[[FBErrorBuilder builder]
  178. withDescriptionFormat:@"Your device does not support location simulation"]
  179. buildError:error];
  180. }
  181. __block NSError *innerError = nil;
  182. __block BOOL didSucceed = NO;
  183. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  184. [session setSimulatedLocation:location completion:^(bool result, NSError *invokeError) {
  185. if (nil != invokeError) {
  186. innerError = invokeError;
  187. } else {
  188. didSucceed = result;
  189. }
  190. completion();
  191. }];
  192. }];
  193. if (nil != innerError && error) {
  194. *error = innerError;
  195. }
  196. return didSucceed;
  197. }
  198. + (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error;
  199. {
  200. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  201. if (![session respondsToSelector:@selector(getSimulatedLocationWithReply:)]) {
  202. [[[FBErrorBuilder builder]
  203. withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
  204. buildError:error];
  205. return nil;
  206. }
  207. if (![session supportsLocationSimulation]) {
  208. [[[FBErrorBuilder builder]
  209. withDescriptionFormat:@"Your device does not support location simulation"]
  210. buildError:error];
  211. return nil;
  212. }
  213. __block NSError *innerError = nil;
  214. __block CLLocation *location = nil;
  215. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  216. [session getSimulatedLocationWithReply:^(CLLocation *reply, NSError *invokeError) {
  217. if (nil != invokeError) {
  218. innerError = invokeError;
  219. } else {
  220. location = reply;
  221. }
  222. completion();
  223. }];
  224. }];
  225. if (nil != innerError && error) {
  226. *error = innerError;
  227. }
  228. return location;
  229. }
  230. + (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error
  231. {
  232. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  233. if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) {
  234. return [[[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. if (![session supportsLocationSimulation]) {
  239. return [[[FBErrorBuilder builder]
  240. withDescriptionFormat:@"Your device does not support location simulation"]
  241. buildError:error];
  242. }
  243. __block NSError *innerError = nil;
  244. __block BOOL didSucceed = NO;
  245. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  246. [session clearSimulatedLocationWithReply:^(bool result, NSError *invokeError) {
  247. if (nil != invokeError) {
  248. innerError = invokeError;
  249. } else {
  250. didSucceed = result;
  251. }
  252. completion();
  253. }];
  254. }];
  255. if (nil != innerError && error) {
  256. *error = innerError;
  257. }
  258. return didSucceed;
  259. }
  260. #endif
  261. + (FBScreenRecordingPromise *)startScreenRecordingWithRequest:(FBScreenRecordingRequest *)request
  262. error:(NSError *__autoreleasing*)error
  263. {
  264. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  265. if (![session respondsToSelector:@selector(startScreenRecordingWithRequest:withReply:)]) {
  266. [[[FBErrorBuilder builder]
  267. withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"]
  268. buildError:error];
  269. return nil;
  270. }
  271. if (![session supportsScreenRecording]) {
  272. [[[FBErrorBuilder builder]
  273. withDescriptionFormat:@"Your device does not support screen recording"]
  274. buildError:error];
  275. return nil;
  276. }
  277. id nativeRequest = [request toNativeRequestWithError:error];
  278. if (nil == nativeRequest) {
  279. return nil;
  280. }
  281. __block id futureMetadata = nil;
  282. __block NSError *innerError = nil;
  283. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  284. [session startScreenRecordingWithRequest:nativeRequest withReply:^(id reply, NSError *invokeError) {
  285. if (nil == invokeError) {
  286. futureMetadata = reply;
  287. } else {
  288. innerError = invokeError;
  289. }
  290. completion();
  291. }];
  292. }];
  293. if (nil != innerError) {
  294. if (error) {
  295. *error = innerError;
  296. }
  297. return nil;
  298. }
  299. return [[FBScreenRecordingPromise alloc] initWithNativePromise:futureMetadata];
  300. }
  301. + (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid error:(NSError *__autoreleasing*)error
  302. {
  303. XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
  304. if (![session respondsToSelector:@selector(stopScreenRecordingWithUUID:withReply:)]) {
  305. return [[[FBErrorBuilder builder]
  306. withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"]
  307. buildError:error];
  308. }
  309. if (![session supportsScreenRecording]) {
  310. return [[[FBErrorBuilder builder]
  311. withDescriptionFormat:@"Your device does not support screen recording"]
  312. buildError:error];
  313. }
  314. __block NSError *innerError = nil;
  315. [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
  316. [session stopScreenRecordingWithUUID:uuid withReply:^(NSError *invokeError) {
  317. if (nil != invokeError) {
  318. innerError = invokeError;
  319. }
  320. completion();
  321. }];
  322. }];
  323. if (nil != innerError && error) {
  324. *error = innerError;
  325. }
  326. return nil == innerError;
  327. }
  328. @end