idb_companion/Request/FBXCTestRunRequest.m (240 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 "FBXCTestRunRequest.h" #import <XCTestBootstrap/XCTestBootstrap.h> #import "FBIDBError.h" #import "FBXCTestDescriptor.h" #import "FBCodeCoverageRequest.h" #import "FBXCTestReporterConfiguration.h" #import "FBIDBAppHostedTestConfiguration.h" #import "FBIDBTestOperation.h" #import "FBIDBStorageManager.h" static const NSTimeInterval FBLogicTestTimeout = 60 * 60; //Aprox. an hour. @interface FBXCTestRunRequest_LogicTest : FBXCTestRunRequest @end @implementation FBXCTestRunRequest_LogicTest - (BOOL)isLogicTest { return YES; } - (BOOL)isUITest { return NO; } - (FBFuture<FBIDBTestOperation *> *)startWithTestDescriptor:(id<FBXCTestDescriptor>)testDescriptor logDirectoryPath:(NSString *)logDirectoryPath reportActivities:(BOOL)reportActivities target:(id<FBiOSTarget>)target reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger temporaryDirectory:(FBTemporaryDirectory *)temporaryDirectory { NSError *error = nil; NSURL *workingDirectory = [temporaryDirectory ephemeralTemporaryDirectory]; if (![NSFileManager.defaultManager createDirectoryAtURL:workingDirectory withIntermediateDirectories:YES attributes:nil error:&error]) { return [FBFuture futureWithError:error]; } FBCodeCoverageConfiguration *coverageConfig = nil; if (self.coverageRequest.collect) { NSURL *dir = [temporaryDirectory ephemeralTemporaryDirectory]; NSString *coverageDirName =[NSString stringWithFormat:@"coverage_%@", NSUUID.UUID.UUIDString]; NSString *coverageDirPath = [dir.path stringByAppendingPathComponent:coverageDirName]; coverageConfig = [[FBCodeCoverageConfiguration alloc] initWithDirectory:coverageDirPath format:self.coverageRequest.format]; } NSString *testFilter = nil; NSArray<NSString *> *testsToSkip = self.testsToSkip.allObjects ?: @[]; if (testsToSkip.count > 0) { return [[FBXCTestError describeFormat:@"'Tests to Skip' %@ provided, but Logic Tests to not support this.", [FBCollectionInformation oneLineDescriptionFromArray:testsToSkip]] failFuture]; } NSArray<NSString *> *testsToRun = self.testsToRun.allObjects ?: @[]; if (testsToRun.count > 1){ return [[FBXCTestError describeFormat:@"More than one 'Tests to Run' %@ provided, but only one 'Tests to Run' is supported.", [FBCollectionInformation oneLineDescriptionFromArray:testsToRun]] failFuture]; } testFilter = testsToRun.firstObject; NSTimeInterval timeout = self.testTimeout.boolValue ? self.testTimeout.doubleValue : FBLogicTestTimeout; FBLogicTestConfiguration *configuration = [FBLogicTestConfiguration configurationWithEnvironment:self.environment workingDirectory:workingDirectory.path testBundlePath:testDescriptor.testBundle.path waitForDebugger:self.waitForDebugger timeout:timeout testFilter:testFilter mirroring:FBLogicTestMirrorFileLogs coverageConfiguration:coverageConfig binaryPath:testDescriptor.testBundle.binary.path logDirectoryPath:logDirectoryPath]; return [self startTestExecution:configuration target:target reporter:reporter logger:logger]; } - (FBFuture<FBIDBTestOperation *> *)startTestExecution:(FBLogicTestConfiguration *)configuration target:(id<FBiOSTarget>)target reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger { FBLogicReporterAdapter *adapter = [[FBLogicReporterAdapter alloc] initWithReporter:reporter logger:logger]; FBLogicTestRunStrategy *runner = [[FBLogicTestRunStrategy alloc] initWithTarget:(id<FBiOSTarget, FBProcessSpawnCommands, FBXCTestExtendedCommands>)target configuration:configuration reporter:adapter logger:logger]; FBFuture<NSNull *> *completed = [runner execute]; if (completed.error) { return [FBFuture futureWithError:completed.error]; } FBXCTestReporterConfiguration *reporterConfiguration = [[FBXCTestReporterConfiguration alloc] initWithResultBundlePath:nil coverageConfiguration:configuration.coverageConfiguration logDirectoryPath:configuration.logDirectoryPath binariesPaths:@[configuration.binaryPath] reportAttachments:self.reportAttachments]; FBIDBTestOperation *operation = [[FBIDBTestOperation alloc] initWithConfiguration:configuration reporterConfiguration:reporterConfiguration reporter:reporter logger:logger completed:completed queue:target.workQueue]; return [FBFuture futureWithResult:operation]; } @end @interface FBXCTestRunRequest_AppTest : FBXCTestRunRequest @end @implementation FBXCTestRunRequest_AppTest - (BOOL)isLogicTest { return NO; } - (BOOL)isUITest { return NO; } - (FBFuture<FBIDBTestOperation *> *)startWithTestDescriptor:(id<FBXCTestDescriptor>)testDescriptor logDirectoryPath:(NSString *)logDirectoryPath reportActivities:(BOOL)reportActivities target:(id<FBiOSTarget>)target reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger temporaryDirectory:(FBTemporaryDirectory *)temporaryDirectory { return [[[testDescriptor testAppPairForRequest:self target:target] onQueue:target.workQueue fmap:^ FBFuture<FBIDBAppHostedTestConfiguration *> * (FBTestApplicationsPair *pair) { [logger logFormat:@"Obtaining launch configuration for App Pair %@ on descriptor %@", pair, testDescriptor]; return [testDescriptor testConfigWithRunRequest:self testApps:pair logDirectoryPath:logDirectoryPath logger:logger queue:target.workQueue]; }] onQueue:target.workQueue fmap:^ FBFuture<FBIDBTestOperation *> * (FBIDBAppHostedTestConfiguration *appHostedTestConfig) { [logger logFormat:@"Obtained app-hosted test configuration %@", appHostedTestConfig]; return [FBXCTestRunRequest_AppTest startTestExecution:appHostedTestConfig reportAttachments:self.reportAttachments target:target reporter:reporter logger:logger]; }]; } + (FBFuture<FBIDBTestOperation *> *)startTestExecution:(FBIDBAppHostedTestConfiguration *)configuration reportAttachments:(BOOL)reportAttachments target:(id<FBiOSTarget>)target reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger { FBTestLaunchConfiguration *testLaunchConfiguration = configuration.testLaunchConfiguration; FBCodeCoverageConfiguration *coverageConfiguration = configuration.coverageConfiguration; NSMutableArray<NSString *> *binariesPaths = NSMutableArray.array; NSString *binaryPath = testLaunchConfiguration.testBundle.binary.path; if (binaryPath) { [binariesPaths addObject:binaryPath]; } binaryPath = testLaunchConfiguration.testHostBundle.binary.path; if (binaryPath) { [binariesPaths addObject:binaryPath]; } binaryPath = testLaunchConfiguration.targetApplicationBundle.binary.path; if (binaryPath) { [binariesPaths addObject:binaryPath]; } FBFuture<NSNull *> *testCompleted = [target runTestWithLaunchConfiguration:testLaunchConfiguration reporter:reporter logger:logger]; FBXCTestReporterConfiguration *reporterConfiguration = [[FBXCTestReporterConfiguration alloc] initWithResultBundlePath:testLaunchConfiguration.resultBundlePath coverageConfiguration:coverageConfiguration logDirectoryPath:testLaunchConfiguration.logDirectoryPath binariesPaths:binariesPaths reportAttachments:reportAttachments]; return [FBFuture futureWithResult:[[FBIDBTestOperation alloc] initWithConfiguration:testLaunchConfiguration reporterConfiguration:reporterConfiguration reporter:reporter logger:logger completed:testCompleted queue:target.workQueue]]; } @end @interface FBXCTestRunRequest_UITest : FBXCTestRunRequest_AppTest @end @implementation FBXCTestRunRequest_UITest - (BOOL)isLogicTest { return NO; } - (BOOL)isUITest { return YES; } @end @implementation FBXCTestRunRequest @synthesize testBundleID = _testBundleID; @synthesize appBundleID = _appBundleID; @synthesize testHostAppBundleID = _testHostAppBundleID; @synthesize environment = _environment; @synthesize arguments = _arguments; @synthesize testsToRun = _testsToRun; @synthesize testsToSkip = _testsToSkip; @synthesize testTimeout = _testTimeout; #pragma mark Initializers + (instancetype)logicTestWithTestBundleID:(NSString *)testBundleID environment:(NSDictionary<NSString *, NSString *> *)environment arguments:(NSArray<NSString *> *)arguments testsToRun:(NSSet<NSString *> *)testsToRun testsToSkip:(NSSet<NSString *> *)testsToSkip testTimeout:(NSNumber *)testTimeout reportActivities:(BOOL)reportActivities reportAttachments:(BOOL)reportAttachments coverageRequest:(FBCodeCoverageRequest *)coverageRequest collectLogs:(BOOL)collectLogs waitForDebugger:(BOOL)waitForDebugger { return [[FBXCTestRunRequest_LogicTest alloc] initWithTestBundleID:testBundleID appBundleID:nil testHostAppBundleID:nil environment:environment arguments:arguments testsToRun:testsToRun testsToSkip:testsToSkip testTimeout:testTimeout reportActivities:reportActivities reportAttachments:reportAttachments coverageRequest:coverageRequest collectLogs:collectLogs waitForDebugger:waitForDebugger]; } + (instancetype)applicationTestWithTestBundleID:(NSString *)testBundleID appBundleID:(NSString *)appBundleID environment:(NSDictionary<NSString *, NSString *> *)environment arguments:(NSArray<NSString *> *)arguments testsToRun:(NSSet<NSString *> *)testsToRun testsToSkip:(NSSet<NSString *> *)testsToSkip testTimeout:(NSNumber *)testTimeout reportActivities:(BOOL)reportActivities reportAttachments:(BOOL)reportAttachments coverageRequest:(FBCodeCoverageRequest *)coverageRequest collectLogs:(BOOL)collectLogs waitForDebugger:(BOOL)waitForDebugger { return [[FBXCTestRunRequest_AppTest alloc] initWithTestBundleID:testBundleID appBundleID:appBundleID testHostAppBundleID:nil environment:environment arguments:arguments testsToRun:testsToRun testsToSkip:testsToSkip testTimeout:testTimeout reportActivities:reportActivities reportAttachments:reportAttachments coverageRequest:coverageRequest collectLogs:collectLogs waitForDebugger:waitForDebugger]; } + (instancetype)uiTestWithTestBundleID:(NSString *)testBundleID appBundleID:(NSString *)appBundleID testHostAppBundleID:(NSString *)testHostAppBundleID environment:(NSDictionary<NSString *, NSString *> *)environment arguments:(NSArray<NSString *> *)arguments testsToRun:(NSSet<NSString *> *)testsToRun testsToSkip:(NSSet<NSString *> *)testsToSkip testTimeout:(NSNumber *)testTimeout reportActivities:(BOOL)reportActivities reportAttachments:(BOOL)reportAttachments coverageRequest:(FBCodeCoverageRequest *)coverageRequest collectLogs:(BOOL)collectLogs { return [[FBXCTestRunRequest_UITest alloc] initWithTestBundleID:testBundleID appBundleID:appBundleID testHostAppBundleID:testHostAppBundleID environment:environment arguments:arguments testsToRun:testsToRun testsToSkip:testsToSkip testTimeout:testTimeout reportActivities:reportActivities reportAttachments:reportAttachments coverageRequest:coverageRequest collectLogs:collectLogs waitForDebugger:NO]; } - (instancetype)initWithTestBundleID:(NSString *)testBundleID appBundleID:(NSString *)appBundleID testHostAppBundleID:(NSString *)testHostAppBundleID environment:(NSDictionary<NSString *, NSString *> *)environment arguments:(NSArray<NSString *> *)arguments testsToRun:(NSSet<NSString *> *)testsToRun testsToSkip:(NSSet<NSString *> *)testsToSkip testTimeout:(NSNumber *)testTimeout reportActivities:(BOOL)reportActivities reportAttachments:(BOOL)reportAttachments coverageRequest:(FBCodeCoverageRequest *)coverageRequest collectLogs:(BOOL)collectLogs waitForDebugger:(BOOL)waitForDebugger { self = [super init]; if (!self) { return nil; } _testBundleID = testBundleID; _appBundleID = appBundleID; _testHostAppBundleID = testHostAppBundleID; _environment = environment; _arguments = arguments; _testsToRun = testsToRun; _testsToSkip = testsToSkip; _testTimeout = testTimeout; _reportActivities = reportActivities; _reportAttachments = reportAttachments; _coverageRequest = coverageRequest; _collectLogs = collectLogs; _waitForDebugger = waitForDebugger; return self; } - (BOOL)isLogicTest { return NO; } - (BOOL)isUITest { return NO; } - (FBFuture<FBIDBTestOperation *> *)startWithBundleStorageManager:(FBXCTestBundleStorage *)bundleStorage target:(id<FBiOSTarget>)target reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger temporaryDirectory:(FBTemporaryDirectory *)temporaryDirectory { return [[self fetchAndSetupDescriptorWithBundleStorage:bundleStorage target:target] onQueue:target.workQueue fmap:^ FBFuture<FBIDBTestOperation *> * (id<FBXCTestDescriptor> descriptor) { NSString *logDirectoryPath = nil; if (self.collectLogs) { NSError *error; NSURL *directory = [temporaryDirectory ephemeralTemporaryDirectory]; if (![NSFileManager.defaultManager createDirectoryAtURL:directory withIntermediateDirectories:YES attributes:nil error:&error]) { return [FBFuture futureWithError:error]; } logDirectoryPath = directory.path; } return [self startWithTestDescriptor:descriptor logDirectoryPath:logDirectoryPath reportActivities:self.reportActivities target:target reporter:reporter logger:logger temporaryDirectory:temporaryDirectory]; }]; } - (FBFuture<FBIDBTestOperation *> *)startWithTestDescriptor:(id<FBXCTestDescriptor>)testDescriptor logDirectoryPath:(NSString *)logDirectoryPath reportActivities:(BOOL)reportActivities target:(id<FBiOSTarget>)target reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger temporaryDirectory:(FBTemporaryDirectory *)temporaryDirectory { return [[FBIDBError describeFormat:@"%@ not implemented in abstract base class", NSStringFromSelector(_cmd)] failFuture]; } - (FBFuture<id<FBXCTestDescriptor>> *)fetchAndSetupDescriptorWithBundleStorage:(FBXCTestBundleStorage *)bundleStorage target:(id<FBiOSTarget>)target { NSError *error = nil; id<FBXCTestDescriptor> testDescriptor = [bundleStorage testDescriptorWithID:self.testBundleID error:&error]; if (!testDescriptor) { return [FBFuture futureWithError:error]; } return [[testDescriptor setupWithRequest:self target:target] mapReplace:testDescriptor]; } @end