FBSimulatorControl/Commands/FBSimulatorXCTestCommands.m (181 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 "FBSimulatorXCTestCommands.h"
#import <XCTestBootstrap/XCTestBootstrap.h>
#import <sys/socket.h>
#import <sys/un.h>
#import <CoreSimulator/SimDevice.h>
#import "FBSimulator+Private.h"
#import "FBSimulator.h"
#import "FBSimulatorError.h"
static NSString *const DefaultSimDeviceSet = @"~/Library/Developer/CoreSimulator/Devices";
@interface FBSimulatorXCTestCommands ()
@property (nonatomic, weak, readonly) FBSimulator *simulator;
@property (nonatomic, assign, readwrite) BOOL isRunningXcodeBuildOperation;
@end
@implementation FBSimulatorXCTestCommands
#pragma mark Initializers
+ (instancetype)commandsWithTarget:(FBSimulator *)target
{
return [[self alloc] initWithSimulator:target];
}
- (instancetype)initWithSimulator:(FBSimulator *)simulator
{
self = [super init];
if (!self) {
return nil;
}
_simulator = simulator;
return self;
}
#pragma mark FBXCTestCommands
- (FBFuture<NSNull *> *)runTestWithLaunchConfiguration:(FBTestLaunchConfiguration *)testLaunchConfiguration reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger
{
// Use FBXCTestBootstrap to run the test if `shouldUseXcodebuild` is not set in the test launch.
if (!testLaunchConfiguration.shouldUseXcodebuild) {
return [self runTestWithLaunchConfiguration:testLaunchConfiguration reporter:reporter logger:logger workingDirectory:self.simulator.auxillaryDirectory];
}
if (self.isRunningXcodeBuildOperation) {
return [[FBSimulatorError
describeFormat:@"Cannot Start Test Manager with Configuration %@ as it is already running", testLaunchConfiguration]
failFuture];
}
return [[[[FBXcodeBuildOperation
terminateAbandonedXcodebuildProcessesForUDID:self.simulator.udid processFetcher:[FBProcessFetcher new] queue:self.simulator.workQueue logger:logger]
onQueue:self.simulator.workQueue fmap:^(id _) {
self.isRunningXcodeBuildOperation = YES;
return [self _startTestWithLaunchConfiguration:testLaunchConfiguration logger:logger];
}]
onQueue:self.simulator.workQueue map:^(FBProcess *task) {
return [FBXcodeBuildOperation confirmExitOfXcodebuildOperation:task configuration:testLaunchConfiguration reporter:reporter target:self.simulator logger:logger];
}]
onQueue:self.simulator.workQueue chain:^(FBFuture *future) {
self.isRunningXcodeBuildOperation = NO;
return future;
}];
}
- (FBFutureContext<NSNumber *> *)transportForTestManagerService
{
return [[[self
testManagerDaemonSocketPath]
onQueue:self.simulator.asyncQueue fmap:^ FBFuture<NSNumber *> * (NSString *testManagerSocketString) {
int socketFD = socket(AF_UNIX, SOCK_STREAM, 0);
if (socketFD == -1) {
return [[FBSimulatorError
describe:@"Unable to create a unix domain socket"]
failFuture];
}
if (![[NSFileManager new] fileExistsAtPath:testManagerSocketString]) {
close(socketFD);
return [[FBSimulatorError
describeFormat:@"Simulator indicated unix domain socket for testmanagerd at path %@, but no file was found at that path.", testManagerSocketString]
failFuture];
}
const char *testManagerSocketPath = testManagerSocketString.UTF8String;
if (strlen(testManagerSocketPath) >= 0x68) {
close(socketFD);
return [[FBSimulatorError
describeFormat:@"Unix domain socket path for simulator testmanagerd service '%s' is too big to fit in sockaddr_un.sun_path", testManagerSocketPath]
failFuture];
}
struct sockaddr_un remote;
remote.sun_family = AF_UNIX;
strcpy(remote.sun_path, testManagerSocketPath);
socklen_t length = (socklen_t)(strlen(remote.sun_path) + sizeof(remote.sun_family) + sizeof(remote.sun_len));
if (connect(socketFD, (struct sockaddr *)&remote, length) == -1) {
close(socketFD);
return [[FBSimulatorError
describe:@"Failed to connect to testmangerd socket"]
failFuture];
}
return [FBFuture futureWithResult:@(socketFD)];
}]
onQueue:self.simulator.asyncQueue contextualTeardown:^(NSNumber *socketNumber, FBFutureState __) {
close(socketNumber.intValue);
return FBFuture.empty;
}];
}
#pragma mark FBXCTestExtendedCommands
- (FBFuture<NSArray<NSString *> *> *)listTestsForBundleAtPath:(NSString *)bundlePath timeout:(NSTimeInterval)timeout withAppAtPath:(NSString *)appPath
{
FBListTestConfiguration *configuration = [FBListTestConfiguration
configurationWithEnvironment:@{}
workingDirectory:self.simulator.auxillaryDirectory
testBundlePath:bundlePath
runnerAppPath:appPath
waitForDebugger:NO
timeout:timeout];
return [[[FBListTestStrategy alloc]
initWithTarget:self.simulator configuration:configuration logger:self.simulator.logger]
listTests];
}
- (FBFuture<NSString *> *)extendedTestShim
{
return [[FBXCTestShimConfiguration
sharedShimConfigurationWithLogger:self.simulator.logger]
onQueue:self.simulator.asyncQueue map:^(FBXCTestShimConfiguration *shims) {
return shims.iOSSimulatorTestShimPath;
}];
}
- (NSString *)xctestPath
{
return [FBXcodeConfiguration.developerDirectory
stringByAppendingPathComponent:@"Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Agents/xctest"];
}
#pragma mark Private
- (FBFuture<NSNull *> *)runTestWithLaunchConfiguration:(FBTestLaunchConfiguration *)testLaunchConfiguration reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger workingDirectory:(nullable NSString *)workingDirectory
{
if (self.simulator.state != FBiOSTargetStateBooted) {
return [[FBSimulatorError
describe:@"Simulator must be booted to run tests"]
failFuture];
}
return [FBManagedTestRunStrategy
runToCompletionWithTarget:self.simulator
configuration:testLaunchConfiguration
codesign:(FBControlCoreGlobalConfiguration.confirmCodesignaturesAreValid ? [FBCodesignProvider codeSignCommandWithAdHocIdentityWithLogger:self.simulator.logger] : nil)
workingDirectory:self.simulator.auxillaryDirectory
reporter:reporter
logger:logger];
}
static NSTimeInterval const TestmanagerdSimSockTimeout = 5; // 5 seconds.
static NSString *const SimSockEnvKey = @"TESTMANAGERD_SIM_SOCK";
- (FBFuture<NSString *> *)testManagerDaemonSocketPath
{
return [[FBFuture
onQueue:self.simulator.asyncQueue resolveUntil:^{
NSError *error = nil;
NSString *socketPath = [self.simulator.device getenv:SimSockEnvKey error:&error];
if (socketPath.length == 0) {
return [[[FBSimulatorError
describeFormat:@"Failed to get %@ from simulator environment", SimSockEnvKey]
causedBy:error]
failFuture];
}
return [FBFuture futureWithResult:socketPath];
}]
timeout:TestmanagerdSimSockTimeout waitingFor:@"%@ to become available in the simulator environment", SimSockEnvKey];
}
- (FBFuture<FBProcess *> *)_startTestWithLaunchConfiguration:(FBTestLaunchConfiguration *)configuration logger:(id<FBControlCoreLogger>)logger
{
NSError *error = nil;
NSString *filePath = [FBXcodeBuildOperation createXCTestRunFileAt:self.simulator.auxillaryDirectory fromConfiguration:configuration error:&error];
if (!filePath) {
return [FBSimulatorError failFutureWithError:error];
}
NSString *xcodeBuildPath = [FBXcodeBuildOperation xcodeBuildPathWithError:&error];
if (!xcodeBuildPath) {
return [FBSimulatorError failFutureWithError:error];
}
return [[FBXCTestShimConfiguration
sharedShimConfigurationWithLogger:self.simulator.logger]
onQueue:self.simulator.asyncQueue fmap:^(FBXCTestShimConfiguration *shims) {
return [FBXcodeBuildOperation
operationWithUDID:self.simulator.udid
configuration:configuration
xcodeBuildPath:xcodeBuildPath
testRunFilePath:filePath
simDeviceSet:self.simulator.customDeviceSetPath
macOSTestShimPath:shims.macOSTestShimPath
queue:self.simulator.workQueue
logger:[logger withName:@"xcodebuild"]];
}];
}
@end