XCTestBootstrap/Strategies/FBLogicTestRunStrategy.m (263 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 "FBLogicTestRunStrategy.h" #import "FBLogicXCTestReporter.h" #import <sys/types.h> #import <sys/stat.h> #import <FBControlCore/FBControlCore.h> #import <XCTestBootstrap/XCTestBootstrap.h> #import "FBXCTestConstants.h" static NSTimeInterval EndOfFileFromStopReadingTimeout = 5; @interface FBLogicTestRunOutputs : NSObject @property (nonatomic, strong, readonly) id<FBDataConsumer, FBDataConsumerLifecycle> stdOutConsumer; @property (nonatomic, strong, readonly) id<FBDataConsumer, FBDataConsumerLifecycle> stdErrConsumer; @property (nonatomic, strong, readonly) id<FBConsumableBuffer> stdErrBuffer; @property (nonatomic, strong, readonly) id<FBDataConsumer, FBDataConsumerLifecycle> shimConsumer; @property (nonatomic, strong, readonly) id<FBProcessFileOutput> shimOutput; @end @implementation FBLogicTestRunOutputs - (instancetype)initWithStdOutConsumer:(id<FBDataConsumer, FBDataConsumerLifecycle>)stdOutConsumer stdErrConsumer:(id<FBDataConsumer, FBDataConsumerLifecycle>)stdErrConsumer stdErrBuffer:(id<FBConsumableBuffer>)stdErrBuffer shimConsumer:(id<FBDataConsumer, FBDataConsumerLifecycle>)shimConsumer shimOutput:(id<FBProcessFileOutput>)shimOutput { self = [super init]; if (!self) { return nil; } _stdOutConsumer = stdOutConsumer; _stdErrConsumer = stdErrConsumer; _stdErrBuffer = stdErrBuffer; _shimConsumer = shimConsumer; _shimOutput = shimOutput; return self; } @end @interface FBLogicTestRunStrategy () @property (nonatomic, strong, readonly) id<FBiOSTarget, FBProcessSpawnCommands, FBXCTestExtendedCommands> target; @property (nonatomic, strong, readonly) FBLogicTestConfiguration *configuration; @property (nonatomic, strong, readonly) id<FBLogicXCTestReporter> reporter; @property (nonatomic, strong, readonly) id<FBControlCoreLogger> logger; @end @implementation FBLogicTestRunStrategy #pragma mark Initializers - (instancetype)initWithTarget:(id<FBiOSTarget, FBProcessSpawnCommands, FBXCTestExtendedCommands>)target configuration:(FBLogicTestConfiguration *)configuration reporter:(id<FBLogicXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger { self = [super init]; if (!self) { return nil; } _target = target; _configuration = configuration; _reporter = reporter; _logger = logger; return self; } #pragma mark Public - (FBFuture<NSNull *> *)execute { return [self testFuture]; } - (FBFuture<NSNull *> *)testFuture { NSUUID *uuid = NSUUID.UUID; return [[FBFuture futureWithFutures:@[ [self buildOutputsForUUID:uuid], [self.target extendedTestShim], ]] onQueue:self.target.workQueue fmap:^(NSArray<id> *tuple) { return [self testFutureWithOutputs:tuple[0] shimPath:tuple[1] uuid:uuid]; }]; } #pragma mark Private - (FBFuture<NSNull *> *)testFutureWithOutputs:(FBLogicTestRunOutputs *)outputs shimPath:(NSString *)shimPath uuid:(NSUUID *)uuid { [self.logger logFormat:@"Starting Logic Test execution of %@", self.configuration]; id<FBLogicXCTestReporter> reporter = self.reporter; [reporter didBeginExecutingTestPlan]; NSString *xctestPath = self.target.xctestPath; // Get the Launch Path and Arguments for the xctest process. NSString *testSpecifier = self.configuration.testFilter ?: @"All"; NSString *launchPath = xctestPath; NSArray<NSString *> *arguments = @[@"-XCTest", testSpecifier, self.configuration.testBundlePath]; return [[FBOToolDynamicLibs findFullPathForSanitiserDyldInBundle:self.configuration.testBundlePath onQueue:self.target.workQueue] onQueue:self.target.workQueue fmap:^FBFuture<NSNull *> * (NSArray<NSString *> *libraries) { NSDictionary<NSString *, NSString *> *environment = [FBLogicTestRunStrategy setupEnvironmentWithDylibs:self.configuration.processUnderTestEnvironment withLibraries:libraries shimOutputFilePath:outputs.shimOutput.filePath shimPath:shimPath bundlePath:self.configuration.testBundlePath coverageDirectoryPath:self.configuration.coverageConfiguration.coverageDirectory logDirectoryPath:self.configuration.logDirectoryPath waitForDebugger:self.configuration.waitForDebugger]; return [[self startTestProcessWithLaunchPath:launchPath arguments:arguments environment:environment outputs:outputs] onQueue:self.target.workQueue fmap:^(FBFuture<NSNumber *> *exitCode) { return [self completeLaunchedProcess:exitCode outputs:outputs]; }]; }]; } + (NSDictionary<NSString *, NSString *> *)setupEnvironmentWithDylibs:(NSDictionary<NSString *, NSString *> *)environment withLibraries:(NSArray *)libraries shimOutputFilePath:(NSString *)shimOutputFilePath shimPath:(NSString *)shimPath bundlePath:(NSString *)bundlePath coverageDirectoryPath:(nullable NSString *)coverageDirectoryPath logDirectoryPath:(nullable NSString *)logDirectoryPath waitForDebugger:(BOOL)waitForDebugger { NSMutableArray<NSString *> *librariesWithShim = [NSMutableArray arrayWithObject:shimPath]; [librariesWithShim addObjectsFromArray:libraries]; NSMutableDictionary<NSString *, NSString *> *environmentAdditions = [NSMutableDictionary dictionaryWithDictionary:@{ @"DYLD_INSERT_LIBRARIES": [librariesWithShim componentsJoinedByString:@":"], @"TEST_SHIM_STDOUT_PATH": shimOutputFilePath, @"TEST_SHIM_BUNDLE_PATH": bundlePath, kEnv_WaitForDebugger: waitForDebugger ? @"YES" : @"NO", }]; if (coverageDirectoryPath) { NSString *coverageFile = [NSString stringWithFormat:@"coverage_%@.profraw", [bundlePath lastPathComponent]]; NSString *coveragePath = [coverageDirectoryPath stringByAppendingPathComponent:coverageFile]; environmentAdditions[kEnv_LLVMProfileFile] = coveragePath; } if (logDirectoryPath) { environmentAdditions[kEnv_LogDirectoryPath] = logDirectoryPath; } NSMutableDictionary<NSString *, NSString *> *updatedEnvironment = [environment mutableCopy]; [updatedEnvironment addEntriesFromDictionary:environmentAdditions]; return [updatedEnvironment copy]; } - (FBFuture<NSNull *> *)completeLaunchedProcess:(FBFuture<NSNumber *> *)exitCode outputs:(FBLogicTestRunOutputs *)outputs { id<FBControlCoreLogger> logger = self.logger; id<FBLogicXCTestReporter> reporter = self.reporter; dispatch_queue_t queue = self.target.workQueue; [logger logFormat:@"Starting to read shim output from location %@", outputs.shimOutput.filePath]; return [[[[outputs.shimOutput startReading] onQueue:queue fmap:^(id _) { [logger logFormat:@"Shim output at %@ has been opened for reading, waiting for xctest process to exit", outputs.shimOutput.filePath]; return [self waitForSuccessfulCompletion:exitCode closingOutputs:outputs]; }] onQueue:queue map:^(id _) { [logger log:@"Normal exit of xctest process"]; [reporter didFinishExecutingTestPlan]; return NSNull.null; }] onQueue:queue handleError:^(NSError *error) { [logger logFormat:@"Abnormal exit of xctest process %@", error]; [reporter didCrashDuringTest:error]; return [FBFuture futureWithError:error]; }]; } - (FBFuture<NSNumber *> *)waitForSuccessfulCompletion:(FBFuture<NSNumber *> *)exitCode closingOutputs:(FBLogicTestRunOutputs *)outputs { id<FBControlCoreLogger> logger = self.logger; dispatch_queue_t queue = self.target.workQueue; return [[exitCode onQueue:queue chain:^(id _) { // Since there's no guarantee that the xctest process has closed the writing end of the fifo, we can't rely on getting and end-of-file naturally // This means that we have to stop reading manually instead. // However, we want to ensure that we've read all the way to the end of the file so that no test results are missing, since the reading is asynchronous. // The stopReading will cause the end-of-file to be sent to the consumer, this is a guarantee that the FBFileReader API makes. // To prevent this from hanging indefinately, we also wrap this in a reasonable timeout so we have a better message in the worst-case scenario. // This teardown is performed unconditionally once the exit code future has resolved so that we clean up from error states. [logger log:@"xctest process terminated, Tearing down IO."]; return [[[FBFuture futureWithFutures:@[ [outputs.shimOutput stopReading], [outputs.shimConsumer finishedConsuming], ]] timeout:EndOfFileFromStopReadingTimeout waitingFor:@"receive and end-of-file after fifo has been stopped, as the process has already exited with code %@", exitCode] chainReplace:exitCode]; }] onQueue:queue fmap:^ FBFuture<NSNull *> * (NSNumber *exitCodeNumber) { [logger logFormat:@"xctest process terminated, exited with %@, checking status code", exitCodeNumber]; int exitCodeValue = exitCodeNumber.intValue; NSString *descriptionOfExit = [FBXCTestProcess describeFailingExitCode:exitCodeValue]; if (descriptionOfExit) { NSString *stdErrReversed = [outputs.stdErrBuffer.lines.reverseObjectEnumerator.allObjects componentsJoinedByString:@"\n"]; return [[FBControlCoreError describeFormat:@"xctest process exited in failure (%d): %@ %@", exitCodeValue, descriptionOfExit, stdErrReversed] failFuture]; } return [FBFuture futureWithResult:exitCodeNumber]; }]; } + (FBFuture<NSNull *> *)fromQueue:(dispatch_queue_t)queue reportWaitForDebugger:(BOOL)waitFor forProcessIdentifier:(pid_t)processIdentifier reporter:(id<FBLogicXCTestReporter>)reporter { if (!waitFor) { return FBFuture.empty; } // Report from the current queue, but wait in a special queue. dispatch_queue_t waitQueue = dispatch_queue_create("com.facebook.xctestbootstrap.debugger_wait", DISPATCH_QUEUE_SERIAL); return [[FBProcessFetcher waitStopSignalForProcess:processIdentifier] onQueue:waitQueue chain:^FBFuture *(FBFuture *future) { if (future.error){ return [[XCTestBootstrapError describeFormat:@"Failed to wait test process (pid %d) to receive a SIGSTOP: '%@'", processIdentifier, future.error.localizedDescription] failFuture]; } [reporter processWaitingForDebuggerWithProcessIdentifier:processIdentifier]; return FBFuture.empty; }]; } - (FBFuture<FBLogicTestRunOutputs *> *)buildOutputsForUUID:(NSUUID *)udid { id<FBLogicXCTestReporter> reporter = self.reporter; id<FBControlCoreLogger> logger = self.logger; dispatch_queue_t queue = self.target.workQueue; BOOL mirrorToLogger = (self.configuration.mirroring & FBLogicTestMirrorLogger) != 0; BOOL mirrorToFiles = (self.configuration.mirroring & FBLogicTestMirrorFileLogs) != 0; NSMutableArray<id<FBDataConsumer>> *shimConsumers = [NSMutableArray array]; NSMutableArray<id<FBDataConsumer>> *stdOutConsumers = [NSMutableArray array]; NSMutableArray<id<FBDataConsumer>> *stdErrConsumers = [NSMutableArray array]; id<FBDataConsumer> shimReportingConsumer = [FBBlockDataConsumer asynchronousLineConsumerWithQueue:queue dataConsumer:^(NSData *line) { [reporter handleEventJSONData:line]; }]; [shimConsumers addObject:shimReportingConsumer]; id<FBDataConsumer> stdOutReportingConsumer = [FBBlockDataConsumer asynchronousLineConsumerWithQueue:queue consumer:^(NSString *line){ [reporter testHadOutput:[line stringByAppendingString:@"\n"]]; }]; [stdOutConsumers addObject:stdOutReportingConsumer]; id<FBDataConsumer> stdErrReportingConsumer = [FBBlockDataConsumer asynchronousLineConsumerWithQueue:queue consumer:^(NSString *line){ [reporter testHadOutput:[line stringByAppendingString:@"\n"]]; }]; [stdErrConsumers addObject:stdErrReportingConsumer]; id<FBConsumableBuffer> stdErrBuffer = FBDataBuffer.consumableBuffer; [stdErrConsumers addObject:stdErrBuffer]; if (mirrorToLogger) { [shimConsumers addObject:[FBLoggingDataConsumer consumerWithLogger:logger]]; [stdErrConsumers addObject:[FBLoggingDataConsumer consumerWithLogger:logger]]; [stdErrConsumers addObject:[FBLoggingDataConsumer consumerWithLogger:logger]]; } id<FBDataConsumer, FBDataConsumerLifecycle> stdOutConsumer = [FBCompositeDataConsumer consumerWithConsumers:stdOutConsumers]; id<FBDataConsumer, FBDataConsumerLifecycle> stdErrConsumer = [FBCompositeDataConsumer consumerWithConsumers:stdErrConsumers]; id<FBDataConsumer, FBDataConsumerLifecycle> shimConsumer = [FBCompositeDataConsumer consumerWithConsumers:shimConsumers]; FBFuture<id<FBDataConsumer, FBDataConsumerLifecycle>> *stdOutFuture = [FBFuture futureWithResult:stdOutConsumer]; FBFuture<id<FBDataConsumer, FBDataConsumerLifecycle>> *stdErrFuture = [FBFuture futureWithResult:stdErrConsumer]; FBFuture<id<FBDataConsumer, FBDataConsumerLifecycle>> *shimFuture = [FBFuture futureWithResult:shimConsumer]; if (mirrorToFiles) { FBXCTestLogger *mirrorLogger = self.configuration.logDirectoryPath ? [FBXCTestLogger defaultLoggerInDirectory:self.configuration.logDirectoryPath] : [FBXCTestLogger defaultLoggerInDefaultDirectory]; stdOutFuture = [mirrorLogger logConsumptionToFile:stdOutConsumer outputKind:@"out" udid:udid logger:logger]; stdErrFuture = [mirrorLogger logConsumptionToFile:stdErrConsumer outputKind:@"err" udid:udid logger:logger]; shimFuture = [mirrorLogger logConsumptionToFile:shimConsumer outputKind:@"shim" udid:udid logger:logger]; } return [[FBFuture futureWithFutures:@[ stdOutFuture, stdErrFuture, shimFuture ]] onQueue:self.target.workQueue fmap:^(NSArray<id<FBDataConsumer, FBDataConsumerLifecycle>> *outputs) { return [[[FBProcessOutput outputForDataConsumer:outputs[2]] providedThroughFile] onQueue:self.target.workQueue map:^(id<FBProcessFileOutput> shimOutput) { return [[FBLogicTestRunOutputs alloc] initWithStdOutConsumer:outputs[0] stdErrConsumer:outputs[1] stdErrBuffer:stdErrBuffer shimConsumer:outputs[2] shimOutput:shimOutput]; }]; }]; } - (FBFuture<FBFuture<NSNumber *> *> *)startTestProcessWithLaunchPath:(NSString *)launchPath arguments:(NSArray<NSString *> *)arguments environment:(NSDictionary<NSString *, NSString *> *)environment outputs:(FBLogicTestRunOutputs *)outputs { dispatch_queue_t queue = self.target.workQueue; id<FBControlCoreLogger> logger = self.logger; id<FBLogicXCTestReporter> reporter = self.reporter; NSTimeInterval timeout = self.configuration.testTimeout; [logger logFormat: @"Launching xctest process with arguments %@, environment %@", [FBCollectionInformation oneLineDescriptionFromArray:[@[launchPath] arrayByAddingObjectsFromArray:arguments]], [FBCollectionInformation oneLineDescriptionFromDictionary:environment] ]; FBProcessIO *io = [[FBProcessIO alloc] initWithStdIn:nil stdOut:[FBProcessOutput outputForDataConsumer:outputs.stdOutConsumer] stdErr:[FBProcessOutput outputForDataConsumer:outputs.stdErrConsumer]]; FBProcessSpawnConfiguration *configuration = [[FBProcessSpawnConfiguration alloc] initWithLaunchPath:launchPath arguments:arguments environment:environment io:io mode:FBProcessSpawnModeDefault]; return [[self.target launchProcess:configuration] onQueue:queue map:^ FBFuture<NSNumber *> * (FBProcess *process) { return [[FBLogicTestRunStrategy fromQueue:queue reportWaitForDebugger:self.configuration.waitForDebugger forProcessIdentifier:process.processIdentifier reporter:reporter] onQueue:queue fmap:^(id _) { return [FBXCTestProcess ensureProcess:process completesWithin:timeout crashLogCommands:self.target queue:queue logger:logger]; }]; }]; } @end