XCTestBootstrap/Utility/FBXcodeBuildOperation.m (170 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBXcodeBuildOperation.h"
#import <FBControlCore/FBControlCore.h>
#import "XCTestBootstrapError.h"
#import "FBXCTestReporter.h"
#import "FBXCTestResultBundleParser.h"
static NSString *const XcodebuildEnvironmentTargetUDID = @"XCTESTBOOTSTRAP_TARGET_UDID";
static NSString *const XcodebuildEnvironmentDeviceSetPath = @"SIM_DEVICE_SET_PATH";
static NSString *const XcodebuildEnvironmentInsertDylib = @"DYLD_INSERT_LIBRARIES";
static NSString *const XcodebuildDestinationTimeoutSecs = @"180"; // How long xcodebuild should wait for the device to be available
@implementation FBXcodeBuildOperation
+ (FBFuture<FBProcess *> *)operationWithUDID:(NSString *)udid configuration:(FBTestLaunchConfiguration *)configuration xcodeBuildPath:(NSString *)xcodeBuildPath testRunFilePath:(NSString *)testRunFilePath simDeviceSet:(NSString *)simDeviceSetPath macOSTestShimPath:(NSString *)macOSTestShimPath queue:(dispatch_queue_t)queue logger:(nullable id<FBControlCoreLogger>)logger
{
NSMutableArray<NSString *> *arguments = [[NSMutableArray alloc] init];
[arguments addObjectsFromArray:@[
@"test-without-building",
@"-xctestrun", testRunFilePath,
@"-destination", [NSString stringWithFormat:@"id=%@", udid],
@"-destination-timeout", XcodebuildDestinationTimeoutSecs,
]];
if (configuration.resultBundlePath) {
[arguments addObjectsFromArray:@[
@"-resultBundlePath",
configuration.resultBundlePath,
]];
}
for (NSString *test in configuration.testsToRun) {
[arguments addObject:[NSString stringWithFormat:@"-only-testing:%@", test]];
}
for (NSString *test in configuration.testsToSkip) {
[arguments addObject:[NSString stringWithFormat:@"-skip-testing:%@", test]];
}
NSMutableDictionary<NSString *, NSString *> *environment = [NSProcessInfo.processInfo.environment mutableCopy];
environment[XcodebuildEnvironmentTargetUDID] = udid;
// Add environments for xcodebuild method swizzling if a simulator device set is provided
if (simDeviceSetPath) {
if (!macOSTestShimPath) {
return [[XCTestBootstrapError describe:@"Failed to locate the shim file for xcodebuild method swizzling"] failFuture];
}
environment[XcodebuildEnvironmentDeviceSetPath] = simDeviceSetPath;
if (environment[XcodebuildEnvironmentInsertDylib]) {
environment[XcodebuildEnvironmentInsertDylib] = [NSString stringWithFormat:@"%@%@%@", environment[XcodebuildEnvironmentInsertDylib], @":", macOSTestShimPath];
}
else {
environment[XcodebuildEnvironmentInsertDylib] = macOSTestShimPath;
}
}
[logger logFormat:@"Starting test with xcodebuild | Arguments: %@ | Environments: %@", [arguments componentsJoinedByString:@" "], [environment description]];
FBProcessBuilder *builder = [[[FBProcessBuilder
withLaunchPath:xcodeBuildPath arguments:arguments]
withEnvironment:environment]
withTaskLifecycleLoggingTo:logger];
if (logger) {
[builder withStdOutToLoggerAndErrorMessage:logger];
[builder withStdErrToLoggerAndErrorMessage:logger];
}
return [[builder
start]
onQueue:queue map:^(FBProcess *task) {
[logger logFormat:@"Task started %@ for xcodebuild %@", task, [arguments componentsJoinedByString:@" "]];
return task;
}];
}
#pragma mark Public
+ (NSDictionary<NSString *, NSDictionary<NSString *, NSObject *> *> *)xctestRunProperties:(FBTestLaunchConfiguration *)testLaunch
{
return @{
@"StubBundleId" : @{
@"TestHostPath" : testLaunch.testHostBundle.path,
@"TestBundlePath" : testLaunch.testBundle.path,
@"UseUITargetAppProvidedByTests" : @YES,
@"IsUITestBundle" : @YES,
@"CommandLineArguments": testLaunch.applicationLaunchConfiguration.arguments,
@"EnvironmentVariables": testLaunch.applicationLaunchConfiguration.environment,
@"TestingEnvironmentVariables": @{
@"DYLD_FRAMEWORK_PATH": @"__TESTROOT__:__PLATFORMS__/iPhoneOS.platform/Developer/Library/Frameworks",
@"DYLD_LIBRARY_PATH": @"__TESTROOT__:__PLATFORMS__/iPhoneOS.platform/Developer/Library/Frameworks",
},
}
};
}
+ (nullable NSString *)createXCTestRunFileAt:(NSString *)directory fromConfiguration:(FBTestLaunchConfiguration *)configuration error:(NSError **)error
{
NSString *fileName = [NSProcessInfo.processInfo.globallyUniqueString stringByAppendingPathExtension:@"xctestrun"];
NSString *path = [directory stringByAppendingPathComponent:fileName];
NSDictionary<NSString *, id> *defaultTestRunProperties = [FBXcodeBuildOperation xctestRunProperties:configuration];
NSDictionary<NSString *, id> *testRunProperties = configuration.xcTestRunProperties
? [self overwriteXCTestRunPropertiesWithBaseProperties:configuration.xcTestRunProperties newProperties:defaultTestRunProperties]
: defaultTestRunProperties;
if (![testRunProperties writeToFile:path atomically:false]) {
return [[XCTestBootstrapError
describeFormat:@"Failed to write to file %@", path]
fail:error];
}
return path;
}
+ (FBFuture<NSArray<FBProcessInfo *> *> *)terminateAbandonedXcodebuildProcessesForUDID:(NSString *)udid processFetcher:(FBProcessFetcher *)processFetcher queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger
{
NSArray<FBProcessInfo *> *processes = [self activeXcodebuildProcessesForUDID:udid processFetcher:processFetcher];
if (processes.count == 0) {
[logger logFormat:@"No processes for %@ to terminate", udid];
return [FBFuture futureWithResult:@[]];
}
[logger logFormat:@"Terminating abandoned xcodebuild processes %@", [FBCollectionInformation oneLineDescriptionFromArray:processes]];
FBProcessTerminationStrategy *strategy = [FBProcessTerminationStrategy strategyWithProcessFetcher:processFetcher workQueue:queue logger:logger];
NSMutableArray<FBFuture<FBProcessInfo *> *> *futures = [NSMutableArray array];
for (FBProcessInfo *process in processes) {
FBFuture<FBProcessInfo *> *termination = [[strategy killProcessIdentifier:process.processIdentifier] mapReplace:process];
[futures addObject:termination];
}
return [FBFuture futureWithFutures:futures];
}
+ (NSString *)xcodeBuildPathWithError:(NSError **)error
{
NSString *path = [FBXcodeConfiguration.developerDirectory stringByAppendingPathComponent:@"/usr/bin/xcodebuild"];
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
return [[XCTestBootstrapError
describeFormat:@"xcodebuild does not exist at expected path %@", path]
fail:error];
}
return path;
}
+ (NSDictionary<NSString *, id> *)overwriteXCTestRunPropertiesWithBaseProperties:(NSDictionary<NSString *, id> *)baseProperties newProperties:(NSDictionary<NSString *, id> *)newProperties
{
NSDictionary<NSString *, id> *defaultTestProperties = [newProperties objectForKey:@"StubBundleId"];
NSMutableDictionary<NSString *, id> *mutableTestRunProperties = NSMutableDictionary.dictionary;
for (NSString *testId in baseProperties) {
NSMutableDictionary<NSString *, id> *mutableTestProperties = [[baseProperties objectForKey:testId] mutableCopy];
for (id key in defaultTestProperties) {
if ([mutableTestProperties objectForKey:key]) {
mutableTestProperties[key] = [defaultTestProperties objectForKey:key];
}
}
mutableTestRunProperties[testId] = mutableTestProperties;
}
return [mutableTestRunProperties copy];
}
+ (FBFuture<NSNull *> *)confirmExitOfXcodebuildOperation:(FBProcess *)task configuration:(FBTestLaunchConfiguration *)configuration reporter:(id<FBXCTestReporter>)reporter target:(id<FBiOSTarget>)target logger:(id<FBControlCoreLogger>)logger
{
return [[[[task
exitedWithCodes:[NSSet setWithObjects:@0, @65, nil]]
onQueue:target.workQueue respondToCancellation:^{
return [task sendSignal:SIGTERM backingOffToKillWithTimeout:1 logger:logger];
}]
onQueue:target.workQueue fmap:^(id _) {
// This will execute only if the operation completes successfully.
[logger logFormat:@"xcodebuild operation completed successfully %@", task];
if (configuration.resultBundlePath) {
return [FBXCTestResultBundleParser parse:configuration.resultBundlePath target:target reporter:reporter logger:logger];
}
[logger log:@"No result bundle to parse"];
return FBFuture.empty;
}]
onQueue:target.workQueue fmap:^(id _) {
[logger log:@"Reporting test results"];
[reporter didFinishExecutingTestPlan];
return FBFuture.empty;
}];
}
#pragma mark Private
+ (NSArray<FBProcessInfo *> *)activeXcodebuildProcessesForUDID:(NSString *)udid processFetcher:(FBProcessFetcher *)processFetcher
{
NSArray<FBProcessInfo *> *xcodebuildProcesses = [processFetcher processesWithProcessName:@"xcodebuild"];
NSMutableArray<FBProcessInfo *> *relevantProcesses = [NSMutableArray array];
for (FBProcessInfo *process in xcodebuildProcesses) {
if (![process.environment[XcodebuildEnvironmentTargetUDID] isEqualToString:udid]) {
continue;
}
[relevantProcesses addObject:process];
}
return relevantProcesses;
}
@end