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