XCTestBootstrap/Strategies/FBListTestStrategy.m (199 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 "FBListTestStrategy.h"
#import <sys/types.h>
#import <sys/stat.h>
#import <FBControlCore/FBControlCore.h>
#import <XCTestBootstrap/XCTestBootstrap.h>
#import "FBXCTestConstants.h"
@interface FBListTestStrategy ()
@property (nonatomic, strong, readonly) FBListTestConfiguration *configuration;
@property (nonatomic, strong, readonly) id<FBiOSTarget, FBProcessSpawnCommands, FBXCTestExtendedCommands> target;
@property (nonatomic, strong, readonly) id<FBControlCoreLogger> logger;
@end
@interface FBListTestStrategy_ReporterWrapped : NSObject <FBXCTestRunner>
@property (nonatomic, strong, readonly) FBListTestStrategy *strategy;
@property (nonatomic, strong, readonly) id<FBXCTestReporter> reporter;
@end
@implementation FBListTestStrategy_ReporterWrapped
- (instancetype)initWithStrategy:(FBListTestStrategy *)strategy reporter:(id<FBXCTestReporter>)reporter
{
self = [super init];
if (!self) {
return nil;
}
_strategy = strategy;
_reporter = reporter;
return self;
}
- (FBFuture<NSNull *> *)execute
{
[self.reporter didBeginExecutingTestPlan];
return [[self.strategy
listTests]
onQueue:self.strategy.target.workQueue map:^(NSArray<NSString *> *testNames) {
for (NSString *testName in testNames) {
NSRange slashRange = [testName rangeOfString:@"/"];
NSString *className = [testName substringToIndex:slashRange.location];
NSString *methodName = [testName substringFromIndex:slashRange.location + 1];
[self.reporter testCaseDidStartForTestClass:className method:methodName];
[self.reporter testCaseDidFinishForTestClass:className method:methodName withStatus:FBTestReportStatusPassed duration:0 logs:nil];
}
[self.reporter didFinishExecutingTestPlan];
return NSNull.null;
}];
}
@end
@implementation FBListTestStrategy
- (instancetype)initWithTarget:(id<FBiOSTarget, FBProcessSpawnCommands, FBXCTestExtendedCommands>)target configuration:(FBListTestConfiguration *)configuration logger:(id<FBControlCoreLogger>)logger
{
self = [super init];
if (!self) {
return nil;
}
_target = target;
_configuration = configuration;
_logger = logger;
return self;
}
- (FBFuture<NSArray<NSString *> *> *)listTests
{
id<FBConsumableBuffer> shimBuffer = FBDataBuffer.consumableBuffer;
return [[FBFuture
futureWithFutures:@[
[self.target extendedTestShim],
[[FBProcessOutput outputForDataConsumer:shimBuffer] providedThroughFile],
]]
onQueue:self.target.workQueue fmap:^(NSArray<id> *tuple) {
return [self listTestsWithShimPath:tuple[0] shimOutput:tuple[1] shimBuffer:shimBuffer];
}];
}
#pragma mark Private
- (FBFuture<NSArray<NSString *> *> *)listTestsWithShimPath:(NSString *)shimPath shimOutput:(id<FBProcessFileOutput>)shimOutput shimBuffer:(id<FBConsumableBuffer>)shimBuffer
{
id<FBConsumableBuffer> stdOutBuffer = FBDataBuffer.consumableBuffer;
id<FBDataConsumer> stdOutConsumer = [FBCompositeDataConsumer consumerWithConsumers:@[
stdOutBuffer,
[FBLoggingDataConsumer consumerWithLogger:self.logger],
]];
id<FBConsumableBuffer> stdErrBuffer = FBDataBuffer.consumableBuffer;
id<FBDataConsumer> stdErrConsumer = [FBCompositeDataConsumer consumerWithConsumers:@[
stdErrBuffer,
[FBLoggingDataConsumer consumerWithLogger:self.logger],
]];
return [[FBOToolDynamicLibs
findFullPathForSanitiserDyldInBundle:self.configuration.testBundlePath onQueue:self.target.workQueue]
onQueue:self.target.workQueue fmap:^FBFuture<NSNull *> * (NSArray<NSString *> *libraries){
NSDictionary<NSString *, NSString *> *environment = [FBListTestStrategy setupEnvironmentWithDylibs:libraries shimPath:shimPath shimOutputFilePath:shimOutput.filePath bundlePath:self.configuration.testBundlePath];
return [[FBListTestStrategy
listTestProcessWithTarget:self.target
configuration:self.configuration
xctestPath:self.target.xctestPath
environment:environment
stdOutConsumer:stdOutConsumer
stdErrConsumer:stdErrConsumer
logger:self.logger]
onQueue:self.target.workQueue fmap:^(FBFuture<NSNumber *> *exitCode) {
return [FBListTestStrategy
launchedProcessWithExitCode:exitCode
shimOutput:shimOutput
shimBuffer:shimBuffer
stdOutBuffer:stdOutBuffer
stdErrBuffer:stdErrBuffer
queue:self.target.workQueue];
}];
}];
}
+ (NSDictionary<NSString *, NSString *> *)setupEnvironmentWithDylibs:(NSArray *)libraries shimPath:(NSString *)shimPath shimOutputFilePath:(NSString *)shimOutputFilePath bundlePath:(NSString *)bundlePath
{
NSMutableArray *librariesWithShim = [NSMutableArray arrayWithObject:shimPath];
[librariesWithShim addObjectsFromArray:libraries];
NSDictionary<NSString *, NSString *> *environment = @{
@"DYLD_INSERT_LIBRARIES": [librariesWithShim componentsJoinedByString:@":"],
@"TEST_SHIM_OUTPUT_PATH": shimOutputFilePath,
@"TEST_SHIM_BUNDLE_PATH": bundlePath,
};
return environment;
}
+ (FBFuture<NSArray<NSString *> *> *)launchedProcessWithExitCode:(FBFuture<NSNumber *> *)exitCode shimOutput:(id<FBProcessFileOutput>)shimOutput shimBuffer:(id<FBConsumableBuffer>)shimBuffer stdOutBuffer:(id<FBConsumableBuffer>)stdOutBuffer stdErrBuffer:(id<FBConsumableBuffer>)stdErrBuffer queue:(dispatch_queue_t)queue
{
return [[[shimOutput
startReading]
onQueue:queue fmap:^(id _) {
return [FBListTestStrategy
onQueue:queue
confirmExit:exitCode
closingOutput:shimOutput
shimBuffer:shimBuffer
stdOutBuffer:stdOutBuffer
stdErrBuffer:stdErrBuffer];
}]
onQueue:queue fmap:^(id _) {
NSError *error = nil;
NSArray<NSDictionary<NSString *, NSString *> *> *tests = [NSJSONSerialization JSONObjectWithData:shimBuffer.data options:0 error:&error];
if (!tests) {
return [FBFuture futureWithError:error];
}
NSMutableArray<NSString *> *testNames = [NSMutableArray array];
for (NSDictionary<NSString *, NSString *> * test in tests) {
NSString *testName = test[kReporter_ListTest_LegacyTestNameKey];
if (![testName isKindOfClass:NSString.class]) {
return [[FBXCTestError
describeFormat:@"Received unexpected test name from shim: %@", testName]
failFuture];
}
[testNames addObject:testName];
}
return [FBFuture futureWithResult:[testNames copy]];
}];
}
+ (FBFuture<NSNull *> *)onQueue:(dispatch_queue_t)queue confirmExit:(FBFuture<NSNumber *> *)exitCode closingOutput:(id<FBProcessFileOutput>)output shimBuffer:(id<FBConsumableBuffer>)shimBuffer stdOutBuffer:(id<FBConsumableBuffer>)stdOutBuffer stdErrBuffer:(id<FBConsumableBuffer>)stdErrBuffer
{
return [exitCode
onQueue:queue fmap:^(NSNumber *exitCodeNumber) {
int exitCodeValue = exitCodeNumber.intValue;
NSString *descriptionOfFailingExit = [FBXCTestProcess describeFailingExitCode:exitCodeValue];
if (descriptionOfFailingExit) {
NSString *stdErrReversed = [stdErrBuffer.lines.reverseObjectEnumerator.allObjects componentsJoinedByString:@"\n"];
return [[XCTestBootstrapError
describeFormat:@"Listing of tests failed due to xctest binary exiting with non-zero exit code %d [%@]: %@", exitCodeValue, descriptionOfFailingExit, stdErrReversed]
failFuture];
}
return [FBFuture futureWithFutures:@[
[output stopReading],
[shimBuffer finishedConsuming],
]];
}];
}
+ (FBFuture<FBFuture<NSNumber *> *> *)listTestProcessWithTarget:(id<FBiOSTarget, FBProcessSpawnCommands>)target configuration:(FBListTestConfiguration *)configuration xctestPath:(NSString *)xctestPath environment:(NSDictionary<NSString *, NSString *> *)environment stdOutConsumer:(id<FBDataConsumer>)stdOutConsumer stdErrConsumer:(id<FBDataConsumer>)stdErrConsumer logger:(id<FBControlCoreLogger>)logger
{
NSString *launchPath = xctestPath;
NSTimeInterval timeout = configuration.testTimeout;
// List test for app test bundle, so we use app binary instead of xctest to load test bundle.
if ([FBBundleDescriptor isApplicationAtPath:configuration.runnerAppPath]) {
// Since we're loading the test bundle in app binary's process without booting a simulator,
// testing frameworks like XCTest.framework and XCTAutomationSupport.framework won't be available.
// (They are available in iOS simulator's runtime). To fix this, we could add the paths of those
// frameworks (developer library version) to `DYLD_FALLBACK_FRAMEWORK_PATH` to meet the dependency
// requirements of loading test bundle.
NSString *developerLibraryPath =
[FBXcodeConfiguration.developerDirectory
stringByAppendingPathComponent:@"Platforms/iPhoneSimulator.platform/Developer/Library"];
NSArray<NSString *> *testFrameworkPaths = @[
[developerLibraryPath stringByAppendingPathComponent:@"Frameworks"],
[developerLibraryPath stringByAppendingPathComponent:@"PrivateFrameworks"],
];
NSMutableDictionary *environmentVariables = environment.mutableCopy;
[environmentVariables addEntriesFromDictionary:@{
@"DYLD_FALLBACK_FRAMEWORK_PATH" : [testFrameworkPaths componentsJoinedByString:@":"],
@"DYLD_FALLBACK_LIBRARY_PATH" : [testFrameworkPaths componentsJoinedByString:@":"],
}];
environment = environmentVariables.copy;
FBBundleDescriptor *appBundle = [FBBundleDescriptor bundleFromPath:configuration.runnerAppPath error:nil];
launchPath = appBundle.binary.path;
}
FBProcessIO *io = [[FBProcessIO alloc] initWithStdIn:nil stdOut:[FBProcessOutput outputForDataConsumer:stdOutConsumer] stdErr:[FBProcessOutput outputForDataConsumer:stdErrConsumer]];
FBProcessSpawnConfiguration *spawnConfiguration = [[FBProcessSpawnConfiguration alloc] initWithLaunchPath:launchPath arguments:@[] environment:environment io:io mode:FBProcessSpawnModeDefault];
return [[target
launchProcess:spawnConfiguration]
onQueue:target.asyncQueue map:^(FBProcess *process) {
return [FBXCTestProcess ensureProcess:process completesWithin:timeout crashLogCommands:nil queue:target.workQueue logger:logger];
}];
}
- (id<FBXCTestRunner>)wrapInReporter:(id<FBXCTestReporter>)reporter
{
return [[FBListTestStrategy_ReporterWrapped alloc] initWithStrategy:self reporter:reporter];
}
@end