fbxctest/FBXCTestKit/Runners/FBTestRunStrategy.m (213 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 "FBTestRunStrategy.h" #import <FBControlCore/FBControlCore.h> #import <XCTestBootstrap/XCTestBootstrap.h> #include <glob.h> @interface FBTestRunStrategy () @property (nonatomic, strong, readonly) id<FBiOSTarget, FBXCTestExtendedCommands> target; @property (nonatomic, strong, readonly) FBTestManagerTestConfiguration *configuration; @property (nonatomic, strong, readonly) id<FBControlCoreLogger> logger; @property (nonatomic, strong, readonly) id<FBXCTestReporter> reporter; @end @implementation FBTestRunStrategy + (instancetype)strategyWithTarget:(id<FBiOSTarget, FBXCTestExtendedCommands>)target configuration:(FBTestManagerTestConfiguration *)configuration reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger { return [[self alloc] initWithTarget:target configuration:configuration reporter:reporter logger:logger]; } - (instancetype)initWithTarget:(id<FBiOSTarget, FBXCTestExtendedCommands>)target configuration:(FBTestManagerTestConfiguration *)configuration reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger { self = [super init]; if (!self) { return nil; } _target = target; _configuration = configuration; _reporter = reporter; _logger = logger; return self; } #pragma mark FBXCTestRunner - (FBFuture<NSNull *> *)execute { NSError *error = nil; FBBundleDescriptor *testRunnerApp = [FBBundleDescriptor bundleFromPath:self.configuration.runnerAppPath error:&error]; if (!testRunnerApp) { [self.logger logFormat:@"Failed to open test runner application: %@", error]; return [FBFuture futureWithError:error]; } FBBundleDescriptor *testTargetApp; if (self.configuration.testTargetAppPath) { testTargetApp = [FBBundleDescriptor bundleFromPath:self.configuration.testTargetAppPath error:&error]; if (!testTargetApp) { [self.logger logFormat:@"Failed to open test target application: %@", error]; return [FBFuture futureWithError:error]; } } FBBundleDescriptor *testBundle = [FBBundleDescriptor bundleFromPath:self.configuration.testBundlePath error:&error]; if (!testBundle) { [self.logger logFormat:@"Failed to open test bundle: %@", error]; return [FBFuture futureWithError:error]; } return [[self.target installApplicationWithPath:testRunnerApp.path] onQueue:self.target.workQueue fmap:^(id _) { return [self startTestWithTestBundle:testBundle testRunnerApp:testRunnerApp testTargetApp:testTargetApp]; }]; } #pragma mark Private - (FBFuture<NSNull *> *)startTestWithTestBundle:(FBBundleDescriptor *)testBundle testRunnerApp:(FBBundleDescriptor *)testRunnerApp testTargetApp:(FBBundleDescriptor *)testTargetApp { FBApplicationLaunchConfiguration *appLaunch = [[FBApplicationLaunchConfiguration alloc] initWithBundleID:testRunnerApp.identifier bundleName:testRunnerApp.identifier arguments:@[] environment:self.configuration.processUnderTestEnvironment waitForDebugger:NO io:FBProcessIO.outputToDevNull launchMode:FBApplicationLaunchModeFailIfRunning]; FBTestLaunchConfiguration *testLaunchConfiguration = [[FBTestLaunchConfiguration alloc] initWithTestBundle:testBundle applicationLaunchConfiguration:appLaunch testHostBundle:nil timeout:0 initializeUITesting:(testTargetApp ? YES : NO) useXcodebuild:NO testsToRun:(self.configuration.testFilter ? [NSSet setWithObject:self.configuration.testFilter] : nil) testsToSkip:nil targetApplicationBundle:testTargetApp xcTestRunProperties:nil resultBundlePath:nil reportActivities:NO coverageDirectoryPath:nil logDirectoryPath:nil]; __block id<FBiOSTargetOperation> tailLogOperation = nil; FBFuture<NSNull *> *executionFinished = [FBManagedTestRunStrategy runToCompletionWithTarget:self.target configuration:testLaunchConfiguration codesign:(FBControlCoreGlobalConfiguration.confirmCodesignaturesAreValid ? [FBCodesignProvider codeSignCommandWithAdHocIdentityWithLogger:self.target.logger] : nil) workingDirectory:[self.configuration.workingDirectory stringByAppendingPathComponent:@"tmp"] reporter:self.reporter logger:self.target.logger]; FBFuture<id> *startedVideoRecording = self.configuration.videoRecordingPath != nil ? (FBFuture<id> *) [self.target startRecordingToFile:self.configuration.videoRecordingPath] : (FBFuture<id> *) FBFuture.empty; FBFuture<id> *startedTailLog = self.configuration.osLogPath != nil ? (FBFuture<id> *) [self _startTailLogToFile:self.configuration.osLogPath] : (FBFuture<id> *) FBFuture.empty; return [[[[[FBFuture futureWithFutures:@[ startedVideoRecording, startedTailLog, ]] onQueue:self.target.workQueue fmap:^(NSArray<id> *results) { if (results[1] != nil && ![results[1] isEqual:NSNull.null]) { tailLogOperation = results[1]; } return executionFinished; }] onQueue:self.target.workQueue chain:^(id _) { FBFuture *stoppedVideoRecording = self.configuration.videoRecordingPath != nil ? [self.target stopRecording] : FBFuture.empty; FBFuture *stopTailLog = tailLogOperation != nil ? [tailLogOperation.completed cancel] : FBFuture.empty; return [FBFuture futureWithFutures:@[stoppedVideoRecording, stopTailLog]]; }] onQueue:self.target.workQueue fmap:^ FBFuture<NSNull *> * (id _) { if (self.configuration.videoRecordingPath != nil) { [self.reporter didRecordVideoAtPath:self.configuration.videoRecordingPath]; } if (self.configuration.osLogPath != nil) { [self.reporter didSaveOSLogAtPath:self.configuration.osLogPath]; } NSError *executionError = executionFinished.error; if (executionError) { return [FBFuture futureWithError:executionError]; } return FBFuture.empty; }] onQueue:self.target.workQueue chain:^ FBFuture<NSNull *> * (FBFuture<NSNull *> *original) { if (!self.configuration.testArtifactsFilenameGlobs) { return original; } return [[self _saveTestArtifactsOfTestRunnerApp:testRunnerApp withFilenameMatchGlobs:self.configuration.testArtifactsFilenameGlobs] chainReplace:original]; }]; } // Save test artifacts matches certain filename globs that are populated during test run // to a temporary folder so it can be obtained by external tools if needed. - (FBFuture<NSNull *> *)_saveTestArtifactsOfTestRunnerApp:(FBBundleDescriptor *)testRunnerApp withFilenameMatchGlobs:(NSArray<NSString *> *)filenameGlobs { return [[[self.target installedApplicationWithBundleID:testRunnerApp.identifier] onQueue:self.target.asyncQueue fmap:^ FBFuture<NSNull *> * (FBInstalledApplication *application) { NSString *directory = application.dataContainer; NSArray<NSString *> *paths = [FBTestRunStrategy recursiveFindByFilenameGlobs:filenameGlobs inDirectory:directory]; if (paths.count == 0) { return FBFuture.empty; } NSError *error = nil; NSURL *tempTestArtifactsPath = [NSURL fileURLWithPath:[NSString pathWithComponents:@[NSTemporaryDirectory(), NSProcessInfo.processInfo.globallyUniqueString, @"test_artifacts"]] isDirectory:YES]; if (![NSFileManager.defaultManager createDirectoryAtURL:tempTestArtifactsPath withIntermediateDirectories:YES attributes:nil error:&error]) { [self.logger logFormat:@"Could not create temporary directory for test artifacts %@", error]; return FBFuture.empty; } for (NSString *sourcePath in paths) { NSString *testArtifactsFilename = sourcePath.lastPathComponent; NSString *destinationPath = [tempTestArtifactsPath.path stringByAppendingPathComponent:testArtifactsFilename]; if ([NSFileManager.defaultManager copyItemAtPath:sourcePath toPath:destinationPath error:nil]) { [self.reporter didCopiedTestArtifact:testArtifactsFilename toPath:destinationPath]; } } return FBFuture.empty; }] onQueue:self.target.asyncQueue handleError:^(NSError *_) { return FBFuture.empty; }]; } - (FBFuture *)_startTailLogToFile:(NSString *)logFilePath { NSError *error = nil; id<FBDataConsumer> logFileWriter = [FBFileWriter syncWriterForFilePath:logFilePath error:&error]; if (logFileWriter == nil) { [self.logger logFormat:@"Could not create log file at %@: %@", self.configuration.osLogPath, error]; return FBFuture.empty; } return [self.target tailLog:@[@"--style", @"syslog", @"--level", @"debug"] consumer:logFileWriter]; } + (NSArray<NSString *> *)recursiveFindByFilenameGlobs:(NSArray<NSString *> *)filenameGlobs inDirectory:(NSString *)directory { NSParameterAssert(filenameGlobs); NSParameterAssert(directory); BOOL isDirectory = NO; if (![NSFileManager.defaultManager fileExistsAtPath:directory isDirectory:&isDirectory]) { return @[]; } if (!isDirectory) { return @[]; } NSMutableArray<NSString *> *foundFiles = [NSMutableArray array]; NSArray<NSString *> *subdirectories = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:directory error:nil]; NSEnumerator *dirsEnumerator = [subdirectories objectEnumerator]; NSString *subdirectory; while (subdirectory = [dirsEnumerator nextObject]) { NSString *fullDirectory = [directory stringByAppendingPathComponent:subdirectory]; for (NSString *filenameGlob in filenameGlobs) { NSString *globPathComponent = [NSString stringWithFormat: @"/%@", filenameGlob]; const char *fullPattern = [[fullDirectory stringByAppendingPathComponent: globPathComponent] UTF8String]; glob_t gt; if (glob(fullPattern, 0, NULL, &gt) == 0) { for (int i = 0; i < gt.gl_matchc; i++) { size_t len = strlen(gt.gl_pathv[i]); NSString *filePath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:gt.gl_pathv[i] length:len]; if (![NSFileManager.defaultManager fileExistsAtPath:filePath isDirectory:&isDirectory]) { continue; } if (isDirectory) { continue; // Don't copy directory. } [foundFiles addObject:filePath]; } } globfree(&gt); } } return [NSArray arrayWithArray:foundFiles]; } @end