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, >) == 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(>);
}
}
return [NSArray arrayWithArray:foundFiles];
}
@end