/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #import "FBXCTestDaemonsProxy.h" #import #import "FBConfiguration.h" #import "FBErrorBuilder.h" #import "FBExceptions.h" #import "FBLogger.h" #import "FBRunLoopSpinner.h" #import "FBScreenRecordingPromise.h" #import "FBScreenRecordingRequest.h" #import "XCTestDriver.h" #import "XCTRunnerDaemonSession.h" #import "XCUIApplication.h" #import "XCUIDevice.h" #define LAUNCH_APP_TIMEOUT_SEC 300 static void (*originalLaunchAppMethod)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *)); static void swizzledLaunchApp(id self, SEL _cmd, NSString *path, NSString *bundleID, NSArray *arguments, NSDictionary *environment, void (^reply)(_Bool, NSError *)) { __block BOOL isSuccessful; __block NSError *error; dispatch_semaphore_t sem = dispatch_semaphore_create(0); originalLaunchAppMethod(self, _cmd, path, bundleID, arguments, environment, ^(BOOL passed, NSError *innerError) { isSuccessful = passed; error = innerError; dispatch_semaphore_signal(sem); }); int64_t timeoutNs = (int64_t)(LAUNCH_APP_TIMEOUT_SEC * NSEC_PER_SEC); if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) { NSString *message = [NSString stringWithFormat:@"The application '%@' cannot be launched within %d seconds timeout", bundleID ?: path, LAUNCH_APP_TIMEOUT_SEC]; @throw [NSException exceptionWithName:FBTimeoutException reason:message userInfo:nil]; } if (!isSuccessful || nil != error) { [FBLogger logFmt:@"%@", error.description]; NSString *message = error.description ?: [NSString stringWithFormat:@"The application '%@' is not installed on the device under test", bundleID ?: path]; @throw [NSException exceptionWithName:FBApplicationMissingException reason:message userInfo:nil]; } reply(isSuccessful, error); } @implementation FBXCTestDaemonsProxy #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-load-method" + (void)load { [self.class swizzleLaunchApp]; } #pragma clang diagnostic pop + (void)swizzleLaunchApp { Method original = class_getInstanceMethod([XCTRunnerDaemonSession class], @selector(launchApplicationWithPath:bundleID:arguments:environment:completion:)); if (original == nil) { [FBLogger log:@"Could not find method -[XCTRunnerDaemonSession launchApplicationWithPath:]"]; return; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wcast-function-type-strict" // Workaround for https://github.com/appium/WebDriverAgent/issues/702 originalLaunchAppMethod = (void(*)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *))) method_getImplementation(original); method_setImplementation(original, (IMP)swizzledLaunchApp); #pragma clang diagnostic pop } + (id)testRunnerProxy { static id proxy = nil; if ([FBConfiguration shouldUseSingletonTestManager]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [FBLogger logFmt:@"Using singleton test manager"]; proxy = [self.class retrieveTestRunnerProxy]; }); } else { [FBLogger logFmt:@"Using general test manager"]; proxy = [self.class retrieveTestRunnerProxy]; } NSAssert(proxy != NULL, @"Could not determine testRunnerProxy", proxy); return proxy; } + (id)retrieveTestRunnerProxy { return ((XCTRunnerDaemonSession *)[XCTRunnerDaemonSession sharedSession]).daemonProxy; } + (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error { __block NSError *innerError = nil; [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ void (^errorHandler)(NSError *) = ^(NSError *invokeError) { if (nil != invokeError) { innerError = invokeError; } completion(); }; XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) { errorHandler(invokeError); }; [[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) { handlerBlock(record, invokeError); }]; }]; if (nil != innerError) { if (error) { *error = innerError; } return NO; } return YES; } + (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError *__autoreleasing*)error { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(openURL:usingApplication:completion:)]) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs with given application"] buildError:error]; } __block NSError *innerError = nil; __block BOOL didSucceed = NO; [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ [session openURL:url usingApplication:bundleId completion:^(bool result, NSError *invokeError) { if (nil != invokeError) { innerError = invokeError; } else { didSucceed = result; } completion(); }]; }]; if (nil != innerError && error) { *error = innerError; } return didSucceed; } + (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasing*)error { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(openDefaultApplicationForURL:completion:)]) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs. Consider upgrading to Xcode 14.3+/iOS 16.4+"] buildError:error]; } __block NSError *innerError = nil; __block BOOL didSucceed = NO; [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ [session openDefaultApplicationForURL:url completion:^(bool result, NSError *invokeError) { if (nil != invokeError) { innerError = invokeError; } else { didSucceed = result; } completion(); }]; }]; if (nil != innerError && error) { *error = innerError; } return didSucceed; } #if !TARGET_OS_TV + (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleasing*)error { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(setSimulatedLocation:completion:)]) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] buildError:error]; } if (![session supportsLocationSimulation]) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"Your device does not support location simulation"] buildError:error]; } __block NSError *innerError = nil; __block BOOL didSucceed = NO; [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ [session setSimulatedLocation:location completion:^(bool result, NSError *invokeError) { if (nil != invokeError) { innerError = invokeError; } else { didSucceed = result; } completion(); }]; }]; if (nil != innerError && error) { *error = innerError; } return didSucceed; } + (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error; { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(getSimulatedLocationWithReply:)]) { [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] buildError:error]; return nil; } if (![session supportsLocationSimulation]) { [[[FBErrorBuilder builder] withDescriptionFormat:@"Your device does not support location simulation"] buildError:error]; return nil; } __block NSError *innerError = nil; __block CLLocation *location = nil; [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ [session getSimulatedLocationWithReply:^(CLLocation *reply, NSError *invokeError) { if (nil != invokeError) { innerError = invokeError; } else { location = reply; } completion(); }]; }]; if (nil != innerError && error) { *error = innerError; } return location; } + (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] buildError:error]; } if (![session supportsLocationSimulation]) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"Your device does not support location simulation"] buildError:error]; } __block NSError *innerError = nil; __block BOOL didSucceed = NO; [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ [session clearSimulatedLocationWithReply:^(bool result, NSError *invokeError) { if (nil != invokeError) { innerError = invokeError; } else { didSucceed = result; } completion(); }]; }]; if (nil != innerError && error) { *error = innerError; } return didSucceed; } #endif + (FBScreenRecordingPromise *)startScreenRecordingWithRequest:(FBScreenRecordingRequest *)request error:(NSError *__autoreleasing*)error { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(startScreenRecordingWithRequest:withReply:)]) { [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"] buildError:error]; return nil; } if (![session supportsScreenRecording]) { [[[FBErrorBuilder builder] withDescriptionFormat:@"Your device does not support screen recording"] buildError:error]; return nil; } id nativeRequest = [request toNativeRequestWithError:error]; if (nil == nativeRequest) { return nil; } __block id futureMetadata = nil; __block NSError *innerError = nil; [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ [session startScreenRecordingWithRequest:nativeRequest withReply:^(id reply, NSError *invokeError) { if (nil == invokeError) { futureMetadata = reply; } else { innerError = invokeError; } completion(); }]; }]; if (nil != innerError) { if (error) { *error = innerError; } return nil; } return [[FBScreenRecordingPromise alloc] initWithNativePromise:futureMetadata]; } + (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid error:(NSError *__autoreleasing*)error { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(stopScreenRecordingWithUUID:withReply:)]) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"] buildError:error]; } if (![session supportsScreenRecording]) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"Your device does not support screen recording"] buildError:error]; } __block NSError *innerError = nil; [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ [session stopScreenRecordingWithUUID:uuid withReply:^(NSError *invokeError) { if (nil != invokeError) { innerError = invokeError; } completion(); }]; }]; if (nil != innerError && error) { *error = innerError; } return nil == innerError; } @end