XCTestBootstrap/Configuration/FBTestRunnerConfiguration.m (167 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 "FBTestRunnerConfiguration.h"
#import "FBTestConfiguration.h"
#import "FBXCTestConstants.h"
#import "XCTestBootstrapError.h"
@implementation FBTestRunnerConfiguration
#pragma mark Initializers
- (instancetype)initWithSessionIdentifier:(NSUUID *)sessionIdentifier testRunner:(FBBundleDescriptor *)testRunner launchEnvironment:(NSDictionary<NSString *, NSString *> *)launchEnvironment testedApplicationAdditionalEnvironment:(NSDictionary<NSString *, NSString *> *)testedApplicationAdditionalEnvironment
{
self = [super init];
if (!self) {
return nil;
}
_sessionIdentifier = sessionIdentifier;
_testRunner = testRunner;
_launchEnvironment = launchEnvironment;
_testedApplicationAdditionalEnvironment = testedApplicationAdditionalEnvironment;
return self;
}
#pragma mark Public
+ (FBFuture<FBTestRunnerConfiguration *> *)prepareConfigurationWithTarget:(id<FBiOSTarget, FBXCTestExtendedCommands>)target testLaunchConfiguration:(FBTestLaunchConfiguration *)testLaunchConfiguration workingDirectory:(NSString *)workingDirectory codesign:(FBCodesignProvider *)codesign
{
if (codesign) {
return [[[codesign
cdHashForBundleAtPath:testLaunchConfiguration.testBundle.path]
rephraseFailure:@"Could not determine bundle at path '%@' is codesigned and codesigning is required", testLaunchConfiguration.testBundle.path]
onQueue:target.asyncQueue fmap:^(id _) {
return [self prepareConfigurationWithTargetAfterCodesignatureCheck:target testLaunchConfiguration:testLaunchConfiguration workingDirectory:workingDirectory];
}];
}
return [self prepareConfigurationWithTargetAfterCodesignatureCheck:target testLaunchConfiguration:testLaunchConfiguration workingDirectory:workingDirectory];
}
+ (NSDictionary<NSString *, NSString *> *)launchEnvironmentWithHostApplication:(FBBundleDescriptor *)hostApplication hostApplicationAdditionalEnvironment:(NSDictionary<NSString *, NSString *> *)hostApplicationAdditionalEnvironment testBundle:(FBBundleDescriptor *)testBundle testConfigurationPath:(NSString *)testConfigurationPath frameworkSearchPaths:(NSArray<NSString *> *)frameworkSearchPaths
{
NSMutableDictionary<NSString *, NSString *> *environmentVariables = hostApplicationAdditionalEnvironment.mutableCopy;
NSString *frameworkSearchPath = [frameworkSearchPaths componentsJoinedByString:@":"];
[environmentVariables addEntriesFromDictionary:@{
@"AppTargetLocation" : hostApplication.binary.path,
@"DYLD_FALLBACK_FRAMEWORK_PATH" : frameworkSearchPath ?: @"",
@"DYLD_FALLBACK_LIBRARY_PATH" : frameworkSearchPath ?: @"",
@"OBJC_DISABLE_GC" : @"YES",
@"TestBundleLocation" : testBundle.path,
@"XCODE_DBG_XPC_EXCLUSIONS" : @"com.apple.dt.xctestSymbolicator",
@"XCTestConfigurationFilePath" : testConfigurationPath,
}];
return [self addAdditionalEnvironmentVariables:environmentVariables.copy];
}
- (NSArray<NSString *> *)launchArguments
{
return @[
@"-NSTreatUnknownArgumentsAsOpen", @"NO",
@"-ApplePersistenceIgnoreState", @"YES"
];
}
#pragma mark NSCopying
- (instancetype)copyWithZone:(NSZone *)zone
{
return self;
}
#pragma mark Private
+ (NSDictionary<NSString *, NSString *> *)addAdditionalEnvironmentVariables:(NSDictionary<NSString *, NSString *> *)currentEnvironmentVariables
{
NSString *prefix = @"CUSTOM_";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self BEGINSWITH %@", prefix];
NSArray *filter = [[NSProcessInfo.processInfo.environment allKeys] filteredArrayUsingPredicate:predicate];
NSDictionary *envVariableWtihPrefix = [NSProcessInfo.processInfo.environment dictionaryWithValuesForKeys:filter];
NSMutableDictionary *envs = [currentEnvironmentVariables mutableCopy];
for (NSString *key in envVariableWtihPrefix)
{
envs[[key substringFromIndex:[prefix length]]] = envVariableWtihPrefix[key];
}
return [NSDictionary dictionaryWithDictionary:envs];
}
+ (FBFuture<FBTestRunnerConfiguration *> *)prepareConfigurationWithTargetAfterCodesignatureCheck:(id<FBiOSTarget, FBXCTestExtendedCommands>)target testLaunchConfiguration:(FBTestLaunchConfiguration *)testLaunchConfiguration workingDirectory:(NSString *)workingDirectory
{
// Common Paths
NSString *runtimeRoot = target.runtimeRootDirectory;
NSString *platformRoot = target.platformRootDirectory;
// This directory will contain XCTest.framework, built for the target platform.
NSString *platformDeveloperFrameworksPath = [platformRoot stringByAppendingPathComponent:@"Developer/Library/Frameworks"];
// Container directory for XCTest related Frameworks.
NSString *developerLibraryPath = [runtimeRoot stringByAppendingPathComponent:@"Developer/Library"];
// Contains other frameworks, depended on by XCTest and Instruments
NSArray<NSString *> *XCTestFrameworksPaths = @[
[developerLibraryPath stringByAppendingPathComponent:@"Frameworks"],
[developerLibraryPath stringByAppendingPathComponent:@"PrivateFrameworks"],
platformDeveloperFrameworksPath,
];
NSString *automationFrameworkPath = [developerLibraryPath stringByAppendingPathComponent:@"PrivateFrameworks/XCTAutomationSupport.framework"];
if (![NSFileManager.defaultManager fileExistsAtPath:automationFrameworkPath]) {
automationFrameworkPath = nil;
}
NSMutableDictionary *testedApplicationAdditionalEnvironment = NSMutableDictionary.dictionary;
NSString *xctTargetBootstrapInjectPath = [platformRoot stringByAppendingPathComponent:@"Developer/usr/lib/libXCTTargetBootstrapInject.dylib"];
// Xcode > 12.5 does not have this file neither requires it's injection in the target test app.
if([NSFileManager.defaultManager fileExistsAtPath:xctTargetBootstrapInjectPath]) {
testedApplicationAdditionalEnvironment[@"DYLD_INSERT_LIBRARIES"] = xctTargetBootstrapInjectPath;
}
NSDictionary *testApplicationDependencies = nil;
if(testLaunchConfiguration.targetApplicationBundle.identifier != nil && testLaunchConfiguration.targetApplicationBundle.path != nil) {
// Explicitly setting the target application bundle id and path as a dependency triggers XCTest to request
// the launch of target application via XCTestManager_IDEInterface/XCTMessagingChannel_RunnerToIDE protocols
testApplicationDependencies = @{
testLaunchConfiguration.targetApplicationBundle.identifier : testLaunchConfiguration.targetApplicationBundle.path
};
}
// Prepare XCTest bundle
NSError *error;
NSUUID *sessionIdentifier = [NSUUID UUID];
FBBundleDescriptor *testBundle = [FBBundleDescriptor bundleFromPath:testLaunchConfiguration.testBundle.path error:&error];
if (!testBundle) {
return [[[XCTestBootstrapError
describe:@"Failed to prepare test bundle"]
causedBy:error]
failFuture];
}
// Prepare the test configuration
FBTestConfiguration *testConfiguration = [FBTestConfiguration
configurationByWritingToFileWithSessionIdentifier:sessionIdentifier
moduleName:testBundle.name
testBundlePath:testBundle.path
uiTesting:testLaunchConfiguration.shouldInitializeUITesting
testsToRun:testLaunchConfiguration.testsToRun
testsToSkip:testLaunchConfiguration.testsToSkip
targetApplicationPath:testLaunchConfiguration.targetApplicationBundle.path
targetApplicationBundleID:testLaunchConfiguration.targetApplicationBundle.identifier
testApplicationDependencies:testApplicationDependencies
automationFrameworkPath:automationFrameworkPath
reportActivities:testLaunchConfiguration.reportActivities
error:&error];
if (!testBundle) {
return [[[XCTestBootstrapError
describe:@"Failed to prepare test configuration"]
causedBy:error]
failFuture];
}
return [[FBFuture
futureWithFutures:@[
[target installedApplicationWithBundleID:testLaunchConfiguration.applicationLaunchConfiguration.bundleID],
[target extendedTestShim],
]]
onQueue:target.asyncQueue map:^(NSArray<id> *tuple) {
FBInstalledApplication *hostApplication = tuple[0];
NSString *shimPath = tuple[1];
NSMutableDictionary<NSString *, NSString *> *hostApplicationAdditionalEnvironment = [NSMutableDictionary dictionary];
hostApplicationAdditionalEnvironment[kEnv_ShimStartXCTest] = @"1";
hostApplicationAdditionalEnvironment[@"DYLD_INSERT_LIBRARIES"] = shimPath;
hostApplicationAdditionalEnvironment[kEnv_WaitForDebugger] = testLaunchConfiguration.applicationLaunchConfiguration.waitForDebugger ? @"YES" : @"NO";
if (testLaunchConfiguration.coverageDirectoryPath) {
NSString *hostCoverageFile = [NSString stringWithFormat:@"coverage_%@.profraw", hostApplication.bundle.identifier];
NSString *hostCoveragePath = [testLaunchConfiguration.coverageDirectoryPath stringByAppendingPathComponent:hostCoverageFile];
hostApplicationAdditionalEnvironment[kEnv_LLVMProfileFile] = hostCoveragePath;
if (testLaunchConfiguration.targetApplicationBundle != nil) {
NSString *targetCoverageFile = [NSString stringWithFormat:@"coverage_%@.profraw", testLaunchConfiguration.targetApplicationBundle.identifier];
NSString *targetAppCoveragePath = [testLaunchConfiguration.coverageDirectoryPath stringByAppendingPathComponent:targetCoverageFile];
testedApplicationAdditionalEnvironment[kEnv_LLVMProfileFile] = targetAppCoveragePath;
}
}
NSString *logDirectoryPath = testLaunchConfiguration.logDirectoryPath;
if (logDirectoryPath) {
hostApplicationAdditionalEnvironment[kEnv_LogDirectoryPath] = logDirectoryPath;
}
// These Search Paths are added via "DYLD_FALLBACK_FRAMEWORK_PATH" so that they can be resolved when linked by the Application.
// This is needed so that the Application is aware of how to link the XCTest.framework from the developer directory.
// The Application binary will not contain linker opcodes that point to the XCTest.framework within the Simulator runtime bundle.
// Therefore we need to provide them to the test runner so it can pass them to the app launch.
NSArray<NSString *> *frameworkSearchPaths = [XCTestFrameworksPaths arrayByAddingObject:[hostApplication.bundle.path stringByAppendingPathComponent:@"Frameworks"]];
// The environment constructed for the app launch must contain the relevant env vars to point at the relevant configuration.
NSDictionary<NSString *, NSString *> *launchEnvironment = [FBTestRunnerConfiguration
launchEnvironmentWithHostApplication:hostApplication.bundle
hostApplicationAdditionalEnvironment:hostApplicationAdditionalEnvironment
testBundle:testBundle
testConfigurationPath:testConfiguration.path
frameworkSearchPaths:frameworkSearchPaths];
return [[FBTestRunnerConfiguration alloc]
initWithSessionIdentifier:sessionIdentifier
testRunner:hostApplication.bundle
launchEnvironment:launchEnvironment
testedApplicationAdditionalEnvironment:[testedApplicationAdditionalEnvironment copy]];
}];
}
@end