idb_companion/Utility/FBXCTestDescriptor.m (241 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 "FBXCTestDescriptor.h" #import <XCTestBootstrap/XCTestBootstrap.h> #import "FBCodeCoverageRequest.h" #import "FBIDBAppHostedTestConfiguration.h" #import "FBIDBError.h" #import "FBTestApplicationsPair.h" #import "FBXCTestRunFileReader.h" #import "FBXCTestRunRequest.h" static FBFuture<FBApplicationLaunchConfiguration *> *BuildAppLaunchConfig(NSString *bundleID, NSDictionary<NSString *, NSString *> *environment, NSArray<NSString *> * arguments, id<FBControlCoreLogger> logger, NSString * processLogDirectory, bool waitForDebugger, dispatch_queue_t queue) { FBLoggingDataConsumer *stdOutConsumer = [FBLoggingDataConsumer consumerWithLogger:logger]; FBLoggingDataConsumer *stdErrConsumer = [FBLoggingDataConsumer consumerWithLogger:logger]; FBFuture<id<FBDataConsumer, FBDataConsumerLifecycle>> *stdOutFuture = [FBFuture futureWithResult:stdOutConsumer]; FBFuture<id<FBDataConsumer, FBDataConsumerLifecycle>> *stdErrFuture = [FBFuture futureWithResult:stdErrConsumer]; if (processLogDirectory) { FBXCTestLogger *mirrorLogger = [FBXCTestLogger defaultLoggerInDirectory:processLogDirectory]; NSUUID *udid = NSUUID.UUID; stdOutFuture = [mirrorLogger logConsumptionToFile:stdOutConsumer outputKind:@"out" udid:udid logger:logger]; stdErrFuture = [mirrorLogger logConsumptionToFile:stdErrConsumer outputKind:@"err" udid:udid logger:logger]; } return [[FBFuture futureWithFutures:@[stdOutFuture, stdErrFuture]] onQueue:queue map:^ (NSArray<id<FBDataConsumer, FBDataConsumerLifecycle>> *outputs) { FBProcessIO *io = [[FBProcessIO alloc] initWithStdIn:nil stdOut:[FBProcessOutput outputForDataConsumer:outputs[0]] stdErr:[FBProcessOutput outputForDataConsumer:outputs[1]]]; return [[FBApplicationLaunchConfiguration alloc] initWithBundleID:bundleID bundleName:nil arguments:arguments ?: @[] environment:environment ?: @{} waitForDebugger:waitForDebugger io:io launchMode:FBApplicationLaunchModeFailIfRunning]; }]; } @interface FBXCTestBootstrapDescriptor () @property (nonatomic, strong, readonly) NSString *targetAuxillaryDirectory; @end @implementation FBXCTestBootstrapDescriptor @synthesize url = _url; @synthesize name = _name; @synthesize testBundle = _testBundle; #pragma mark Initializers - (instancetype)initWithURL:(NSURL *)url name:(NSString *)name testBundle:(FBBundleDescriptor *)testBundle { self = [super init]; if (!self) { return nil; } _url = url; _name = name; _testBundle = testBundle; return self; } #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"xctestbootstrap descriptor for %@ %@ %@", self.url, self.name, self.testBundle]; } #pragma mark Properties - (NSString *)testBundleID { return self.testBundle.identifier; } - (NSSet *)architectures { return self.testBundle.binary.architectures; } #pragma mark Private + (FBFuture<NSNull *> *)killAllRunningApplications:(id<FBiOSTarget>)target { id<FBApplicationCommands> commands = (id<FBApplicationCommands>) target; if (![commands conformsToProtocol:@protocol(FBApplicationCommands)]) { return [[FBIDBError describeFormat:@"%@ does not conform to FBApplicationCommands", commands] failFuture]; } return [[[commands runningApplications] onQueue:target.workQueue fmap:^(NSDictionary<NSString *, FBProcessInfo *> *runningApplications) { NSMutableArray<FBFuture<NSNull *> *> *futures = [NSMutableArray array]; for (NSString *bundleID in runningApplications) { [futures addObject:[commands killApplicationWithBundleID:bundleID]]; } return [FBFuture futureWithFutures:futures]; }] mapReplace:NSNull.null]; } #pragma mark Public - (FBFuture<NSNull *> *)setupWithRequest:(FBXCTestRunRequest *)request target:(id<FBiOSTarget>)target { _targetAuxillaryDirectory = target.auxillaryDirectory; if (request.isLogicTest) { //Logic tests don't use an app to run //killing them is unnecessary for us. return FBFuture.empty; } // Kill all Running Applications to get back to a clean slate. return [[FBXCTestBootstrapDescriptor killAllRunningApplications:target] mapReplace:NSNull.null]; } - (FBFuture<FBTestApplicationsPair *> *)testAppPairForRequest:(FBXCTestRunRequest *)request target:(id<FBiOSTarget>)target { if (request.isLogicTest) { return [FBFuture futureWithResult:[[FBTestApplicationsPair alloc] initWithApplicationUnderTest:nil testHostApp:nil]]; } if (request.isUITest) { if (!request.appBundleID) { return [[FBIDBError describe:@"Request for UI Test, but no app_bundle_id provided"] failFuture]; } NSString *testHostBundleID = request.testHostAppBundleID ?: @"com.apple.Preferences"; return [[FBFuture futureWithFutures:@[ [target installedApplicationWithBundleID:request.appBundleID], [target installedApplicationWithBundleID:testHostBundleID], ]] onQueue:target.asyncQueue map:^(NSArray<FBInstalledApplication *> *applications) { return [[FBTestApplicationsPair alloc] initWithApplicationUnderTest:applications[0] testHostApp:applications[1]]; }]; } NSString *bundleID = request.testHostAppBundleID ?: request.appBundleID; if (!bundleID) { return [[FBIDBError describe:@"Request for Application Test, but no app_bundle_id or test_host_app_bundle_id provided"] failFuture]; } return [[target installedApplicationWithBundleID:bundleID] onQueue:target.asyncQueue map:^(FBInstalledApplication *application) { return [[FBTestApplicationsPair alloc] initWithApplicationUnderTest:nil testHostApp:application]; }]; } - (FBFuture<FBIDBAppHostedTestConfiguration *> *)testConfigWithRunRequest:(FBXCTestRunRequest *)request testApps:(FBTestApplicationsPair *)testApps logDirectoryPath:(NSString *)logDirectoryPath logger:(id<FBControlCoreLogger>)logger queue:(dispatch_queue_t)queue { BOOL uiTesting = NO; FBFuture<FBApplicationLaunchConfiguration *> *appLaunchConfigFuture = nil; if (request.isUITest) { appLaunchConfigFuture = BuildAppLaunchConfig(testApps.testHostApp.bundle.identifier, request.environment, request.arguments, logger, logDirectoryPath, request.waitForDebugger, queue); uiTesting = YES; } else { appLaunchConfigFuture = BuildAppLaunchConfig(request.appBundleID, request.environment, request.arguments, logger, logDirectoryPath, request.waitForDebugger, queue); } FBCodeCoverageConfiguration *coverageConfig = nil; if (request.coverageRequest.collect) { NSString *coverageDirName =[NSString stringWithFormat:@"coverage_%@", NSUUID.UUID.UUIDString]; NSString *coverageDirPath = [self.targetAuxillaryDirectory stringByAppendingPathComponent:coverageDirName]; coverageConfig = [[FBCodeCoverageConfiguration alloc] initWithDirectory:coverageDirPath format:request.coverageRequest.format]; } return [appLaunchConfigFuture onQueue:queue map:^ FBIDBAppHostedTestConfiguration * (FBApplicationLaunchConfiguration *applicationLaunchConfiguration) { FBTestLaunchConfiguration *testLaunchConfig = [[FBTestLaunchConfiguration alloc] initWithTestBundle:self.testBundle applicationLaunchConfiguration:applicationLaunchConfiguration testHostBundle:testApps.testHostApp.bundle timeout:(request.testTimeout ? request.testTimeout.doubleValue : 0) initializeUITesting:uiTesting useXcodebuild:NO testsToRun:request.testsToRun testsToSkip:request.testsToSkip targetApplicationBundle:testApps.applicationUnderTest.bundle xcTestRunProperties:nil resultBundlePath:nil reportActivities:request.reportActivities coverageDirectoryPath:coverageConfig.coverageDirectory logDirectoryPath:logDirectoryPath]; return [[FBIDBAppHostedTestConfiguration alloc] initWithTestLaunchConfiguration:testLaunchConfig coverageConfiguration:coverageConfig]; }]; } @end @interface FBXCodebuildTestRunDescriptor () @property (nonatomic, strong, readonly) NSString *targetAuxillaryDirectory; @end @implementation FBXCodebuildTestRunDescriptor @synthesize url = _url; @synthesize name = _name; @synthesize testBundle = _testBundle; - (instancetype)initWithURL:(NSURL *)url name:(NSString *)name testBundle:(FBBundleDescriptor *)testBundle testHostBundle:(FBBundleDescriptor *)testHostBundle { self = [super init]; if (!self) { return nil; } _url = url; _name = name; _testBundle = testBundle; _testHostBundle = testHostBundle; return self; } #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"xcodebuild descriptor for %@ %@ %@ %@", self.url, self.name, self.testBundle, self.testHostBundle]; } #pragma mark Properties - (NSString *)testBundleID { return self.testBundle.identifier; } - (NSSet *)architectures { return self.testHostBundle.binary.architectures; } #pragma mark Public Methods - (FBFuture<NSNull *> *)setupWithRequest:(FBXCTestRunRequest *)request target:(id<FBiOSTarget>)target { _targetAuxillaryDirectory = target.auxillaryDirectory; return FBFuture.empty; } - (FBFuture<FBTestApplicationsPair *> *)testAppPairForRequest:(FBXCTestRunRequest *)request target:(id<FBiOSTarget>)target { return [FBFuture futureWithResult:[[FBTestApplicationsPair alloc] initWithApplicationUnderTest:nil testHostApp:nil]]; } - (FBFuture<FBIDBAppHostedTestConfiguration *> *)testConfigWithRunRequest:(FBXCTestRunRequest *)request testApps:(FBTestApplicationsPair *)testApps logDirectoryPath:(NSString *)logDirectoryPath logger:(id<FBControlCoreLogger>)logger queue:(dispatch_queue_t)queue { NSString *resultBundleName = [NSString stringWithFormat:@"resultbundle_%@", NSUUID.UUID.UUIDString]; NSString *resultBundlePath = [self.targetAuxillaryDirectory stringByAppendingPathComponent:resultBundleName]; NSError *error = nil; NSDictionary<NSString *, id> *properties = [FBXCTestRunFileReader readContentsOf:self.url expandPlaceholderWithPath:self.targetAuxillaryDirectory error:&error]; if (!properties) { return [FBFuture futureWithError:error]; } return [BuildAppLaunchConfig(request.appBundleID, request.environment, request.arguments, logger, nil, request.waitForDebugger, queue) onQueue:queue map:^ FBIDBAppHostedTestConfiguration * (FBApplicationLaunchConfiguration *launchConfig) { FBTestLaunchConfiguration *testLaunchConfiguration = [[FBTestLaunchConfiguration alloc] initWithTestBundle:self.testBundle applicationLaunchConfiguration:launchConfig testHostBundle:self.testHostBundle timeout:0 initializeUITesting:request.isUITest useXcodebuild:YES testsToRun:request.testsToRun testsToSkip:request.testsToSkip targetApplicationBundle:nil xcTestRunProperties:properties resultBundlePath:resultBundlePath reportActivities:request.reportActivities coverageDirectoryPath:nil logDirectoryPath:logDirectoryPath]; return [[FBIDBAppHostedTestConfiguration alloc] initWithTestLaunchConfiguration:testLaunchConfiguration coverageConfiguration:nil]; }]; } @end