XCTestBootstrap/MacStrategies/FBMacDevice.m (442 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 "FBMacDevice.h"
#import <CoreFoundation/CoreFoundation.h>
#import <FBControlCore/FBControlCore.h>
#import <IOKit/IOKitLib.h>
#import "FBManagedTestRunStrategy.h"
#import "XCTestBootstrapError.h"
#import "FBListTestStrategy.h"
#import "FBXCTestConfiguration.h"
@protocol XCTestManager_XPCControl <NSObject>
- (void)_XCT_requestConnectedSocketForTransport:(void (^)(NSFileHandle *, NSError *))arg1;
@end
@interface FBMacDevice()
@property (nonatomic, strong) NSMutableDictionary<NSString *, FBBundleDescriptor *> *bundleIDToProductMap;
@property (nonatomic, strong) NSMutableDictionary<NSString *, FBProcess *> *bundleIDToRunningTask;
@property (nonatomic, strong) NSXPCConnection *connection;
@property (nonatomic, copy) NSString *workingDirectory;
@end
@implementation FBMacDevice
@synthesize temporaryDirectory = _temporaryDirectory;
+ (NSString *)applicationInstallDirectory
{
static dispatch_once_t onceToken;
static NSString *_value;
dispatch_once(&onceToken, ^{
_value = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSUserDomainMask, YES).lastObject;
});
return _value;
}
+ (NSMutableDictionary<NSString *, FBBundleDescriptor *> *)fetchInstalledApplications
{
NSMutableDictionary<NSString *, FBBundleDescriptor *> *mapping = @{}.mutableCopy;
NSArray<NSString *> *content = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.applicationInstallDirectory error:nil];
for (NSString *fileOrDirectory in content) {
if (![fileOrDirectory.pathExtension isEqualToString:@"app"]) {
continue;
}
NSString *path = [FBMacDevice.applicationInstallDirectory stringByAppendingPathComponent:fileOrDirectory];
FBBundleDescriptor *bundle = [FBBundleDescriptor bundleFromPath:path error:nil];
if (bundle && bundle.identifier) {
mapping[bundle.identifier] = bundle;
}
}
return mapping;
}
- (instancetype)init
{
self = [super init];
if (self) {
_architecture = FBArchitectureX86_64;
_asyncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
#ifdef DEBUG
// currentDirectoryPath is setted to root ("/") in debug builds and we dont have permission to write there
_auxillaryDirectory = [NSTemporaryDirectory() stringByAppendingPathComponent:NSProcessInfo.processInfo.globallyUniqueString];
#else
_auxillaryDirectory = [NSFileManager.defaultManager.currentDirectoryPath stringByAppendingPathComponent:NSProcessInfo.processInfo.globallyUniqueString];
#endif
_bundleIDToProductMap = [FBMacDevice fetchInstalledApplications];
_bundleIDToRunningTask = @{}.mutableCopy;
_udid = [FBMacDevice resolveDeviceUDID];
_state = FBiOSTargetStateBooted;
_targetType = FBiOSTargetTypeLocalMac;
_workQueue = dispatch_get_main_queue();
_temporaryDirectory = [FBTemporaryDirectory temporaryDirectoryWithLogger:self.logger];
_workingDirectory = [NSTemporaryDirectory() stringByAppendingPathComponent:NSProcessInfo.processInfo.globallyUniqueString];
_screenInfo = nil;
_osVersion = [FBOSVersion genericWithName:FBOSVersionNamemac];
_name = [[NSHost currentHost] localizedName];
}
return self;
}
- (instancetype)initWithLogger:(nonnull id<FBControlCoreLogger>)logger
{
self = [self init];
if (self) {
_logger = logger;
}
return self;
}
- (FBFuture<NSNull *> *)restorePrimaryDeviceState
{
NSMutableArray<FBFuture *> *queuedFutures = @[].mutableCopy;
NSMutableArray<FBFuture *> *killFutures = @[].mutableCopy;
for (NSString *bundleID in self.bundleIDToRunningTask.copy) {
[killFutures addObject:[self killApplicationWithBundleID:bundleID]];
}
if (killFutures.count > 0) {
[queuedFutures addObject:[FBFuture race:killFutures]];
}
NSMutableArray<FBFuture *> *uninstallFutures = @[].mutableCopy;
for (NSString *bundleID in self.bundleIDToProductMap.copy) {
[uninstallFutures addObject:[self uninstallApplicationWithBundleID:bundleID]];
}
if (uninstallFutures.count > 0) {
[queuedFutures addObject:[FBFuture race:uninstallFutures]];
}
if (queuedFutures.count > 0) {
return [FBFuture futureWithFutures:queuedFutures];
}
return [FBFuture futureWithResult:[NSNull null]];
}
- (NSString *)runtimeRootDirectory
{
return [self platformRootDirectory];
}
- (NSString *)platformRootDirectory
{
return [FBXcodeConfiguration.developerDirectory stringByAppendingPathComponent:@"Platforms/MacOSX.platform"];
}
- (NSString *)xctestPath
{
return [FBXcodeConfiguration.developerDirectory
stringByAppendingPathComponent:@"usr/bin/xctest"];
}
- (FBFuture<NSString *> *)extendedTestShim
{
return [[FBXCTestShimConfiguration
sharedShimConfigurationWithLogger:self.logger]
onQueue:self.asyncQueue map:^(FBXCTestShimConfiguration *shims) {
return shims.macOSTestShimPath;
}];
}
+ (NSString *)resolveDeviceUDID
{
io_service_t platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
if (!platformExpert) {
return nil;
}
CFTypeRef serialNumberAsCFString =
IORegistryEntryCreateCFProperty(
platformExpert,
CFSTR(kIOPlatformSerialNumberKey),
kCFAllocatorDefault,
0);
IOObjectRelease(platformExpert);
return (NSString *)CFBridgingRelease(serialNumberAsCFString);
}
@synthesize udid = _udid;
- (FBFutureContext<NSNumber *> *)transportForTestManagerService
{
id<FBControlCoreLogger> logger = self.logger;
NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:@"com.apple.testmanagerd.control" options:0];
NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(XCTestManager_XPCControl)];
[connection setRemoteObjectInterface:interface];
__weak __typeof__(self) weakSelf = self;
[connection setInterruptionHandler:^{
weakSelf.connection = nil;
[logger log:@"Connection with test manager daemon was interrupted"];
}];
[connection setInvalidationHandler:^{
weakSelf.connection = nil;
[logger log:@"Invalidated connection with test manager daemon"];
}];
[connection resume];
id<XCTestManager_XPCControl> proxy = [connection synchronousRemoteObjectProxyWithErrorHandler:^(NSError *proxyError) {
if (!proxyError) {
return;
}
[logger logFormat:@"Error occured during synchronousRemoteObjectProxyWithErrorHandler call: %@", proxyError.description];
weakSelf.connection = nil;
}];
self.connection = connection;
__block NSError *error;
__block NSFileHandle *transport;
[proxy _XCT_requestConnectedSocketForTransport:^(NSFileHandle *file, NSError *xctError) {
if (!file) {
[logger logFormat:@"Error requesting connection with test manager daemon: %@", xctError.description];
error = xctError;
return;
}
transport = file;
}];
if (!transport) {
return [FBFutureContext futureContextWithError:error];
}
return [[FBFuture
futureWithResult:@(transport.fileDescriptor)]
onQueue:self.workQueue contextualTeardown:^(id _, FBFutureState __) {
[transport closeFile];
return FBFuture.empty;
}];
}
- (nonnull FBFuture<NSNumber *> *)processIDWithBundleID:(nonnull NSString *)bundleID
{
FBProcess *task = self.bundleIDToRunningTask[bundleID];
if (!task) {
NSError *error = [XCTestBootstrapError errorForFormat:@"Application with bundleID (%@) was not launched by XCTestBootstrap", bundleID];
return [FBFuture futureWithError:error];
}
return [FBFuture futureWithResult:@(self.bundleIDToRunningTask[bundleID].processIdentifier)];
}
#pragma mark Not supported
- (nonnull NSString *)consoleString
{
NSAssert(nil, @"consoleString is not yet supported");
return nil;
}
#pragma mark - FBiOSTarget
@synthesize architecture = _architecture;
@synthesize asyncQueue = _asyncQueue;
@synthesize auxillaryDirectory = _auxillaryDirectory;
@synthesize name = _name;
@synthesize logger = _logger;
@synthesize osVersion = _osVersion;
@synthesize state = _state;
@synthesize targetType = _targetType;
@synthesize workQueue = _workQueue;
@synthesize screenInfo = _screenInfo;
// Not used or set
@synthesize deviceType;
- (BOOL) requiresBundlesToBeSigned {
return NO;
}
+ (nonnull instancetype)commandsWithTarget:(nonnull id<FBiOSTarget>)target
{
NSAssert(nil, @"commandsWithTarget is not yet supported");
return nil;
}
- (FBFuture<FBInstalledApplication *> *)installApplicationWithPath:(NSString *)path
{
NSError *error;
NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:FBMacDevice.applicationInstallDirectory]) {
if (![fm createDirectoryAtPath:FBMacDevice.applicationInstallDirectory withIntermediateDirectories:YES attributes:nil error:&error]) {
return [FBFuture futureWithError:error];
}
}
NSString *dest = [FBMacDevice.applicationInstallDirectory stringByAppendingPathComponent:path.lastPathComponent];
if ([fm fileExistsAtPath:dest]) {
if (![fm removeItemAtPath:dest error:&error]) {
return [FBFuture futureWithError:error];
}
}
if (![fm copyItemAtPath:path toPath:dest error:&error]) {
return [FBFuture futureWithError:error];
}
FBBundleDescriptor *bundle = [FBBundleDescriptor bundleFromPath:dest error:&error];
if (error) {
return [FBFuture futureWithError:error];
}
self.bundleIDToProductMap[bundle.identifier] = bundle;
return [FBFuture futureWithResult:[FBInstalledApplication installedApplicationWithBundle:bundle installType:FBApplicationInstallTypeUnknown dataContainer:nil]];
}
- (nonnull FBFuture<NSNull *> *)uninstallApplicationWithBundleID:(nonnull NSString *)bundleID
{
FBBundleDescriptor *bundle = self.bundleIDToProductMap[bundleID];
if (!bundle) {
return [[XCTestBootstrapError
describeFormat:@"Application with bundleID (%@) was not installed by XCTestBootstrap", bundleID]
failFuture];
}
NSError *error;
if (![[NSFileManager defaultManager] removeItemAtPath:bundle.path error:&error]) {
return [FBFuture futureWithError:error];
}
[self.bundleIDToProductMap removeObjectForKey:bundleID];
return [FBFuture futureWithResult:[NSNull null]];
}
- (nonnull FBFuture<NSArray<FBInstalledApplication *> *> *)installedApplications
{
NSMutableArray *result = [NSMutableArray array];
for (NSString *bundleID in self.bundleIDToProductMap) {
FBBundleDescriptor *bundle = self.bundleIDToProductMap[bundleID];
NSError *error;
bundle = [FBBundleDescriptor bundleFromPath:bundle.path error:&error];
if (!bundle) {
return [FBFuture futureWithError:error];
}
[result addObject:[FBInstalledApplication installedApplicationWithBundle:bundle installType:FBApplicationInstallTypeMac dataContainer:nil]];
}
return [FBFuture futureWithResult:result];
}
- (FBFuture<FBInstalledApplication *> *)installedApplicationWithBundleID:(NSString *)bundleID
{
FBBundleDescriptor *bundle = self.bundleIDToProductMap[bundleID];
NSError *error;
bundle = [FBBundleDescriptor bundleFromPath:bundle.path error:&error];
if (!bundle) {
return [FBFuture futureWithError:error];
}
FBInstalledApplication *installedApp = [FBInstalledApplication installedApplicationWithBundle:bundle installType:FBApplicationInstallTypeMac dataContainer:nil];
return [FBFuture futureWithResult:installedApp];
}
- (nonnull FBFuture<NSNull *> *)killApplicationWithBundleID:(nonnull NSString *)bundleID
{
FBProcess *task = self.bundleIDToRunningTask[bundleID];
if (!task) {
NSError *error = [XCTestBootstrapError errorForFormat:@"Application with bundleID (%@) was not launched by XCTestBootstrap", bundleID];
return [FBFuture futureWithError:error];
}
[task sendSignal:SIGTERM backingOffToKillWithTimeout:2 logger:self.logger];
[self.bundleIDToRunningTask removeObjectForKey:bundleID];
return [FBFuture futureWithResult:[NSNull null]];
}
- (FBFuture<FBProcess *> *)launchApplication:(FBApplicationLaunchConfiguration *)configuration
{
FBBundleDescriptor *bundle = self.bundleIDToProductMap[configuration.bundleID];
if (!bundle) {
return [[FBControlCoreError
describeFormat:@"Could not find application for %@", configuration.bundleID]
failFuture];
}
return [[[[[FBProcessBuilder
withLaunchPath:bundle.binary.path]
withArguments:configuration.arguments]
withEnvironment:configuration.environment]
start]
onQueue:self.workQueue map:^ FBProcess * (FBProcess *task) {
self.bundleIDToRunningTask[bundle.identifier] = task;
return task;
}];
}
- (nonnull FBFuture<NSDictionary<NSString *,FBProcessInfo *> *> *)runningApplications
{
NSMutableDictionary<NSString *, FBProcessInfo *> *runningProcesses = @{}.mutableCopy;
FBProcessFetcher *fetcher = [FBProcessFetcher new];
for (NSString *bundleId in self.bundleIDToRunningTask.allKeys) {
FBProcess *task = self.bundleIDToRunningTask[bundleId];
runningProcesses[bundleId] = [fetcher processInfoFor:task.processIdentifier];
}
return [FBFuture futureWithResult:runningProcesses];
}
- (FBFuture<NSNull *> *)runTestWithLaunchConfiguration:(nonnull FBTestLaunchConfiguration *)testLaunchConfiguration reporter:(id<FBXCTestReporter>)reporter logger:(nonnull id<FBControlCoreLogger>)logger
{
return [FBManagedTestRunStrategy
runToCompletionWithTarget:self
configuration:testLaunchConfiguration
codesign:nil
workingDirectory:self.workingDirectory
reporter:reporter
logger:logger];
}
- (NSString *)uniqueIdentifier
{
return self.udid;
}
- (NSDictionary<NSString *, id> *)extendedInformation
{
return @{};
}
- (NSComparisonResult)compare:(nonnull id<FBiOSTarget>)target
{
return NSOrderedSame; // There should be only one
}
- (NSString *)customDeviceSetPath
{
return nil;
}
- (FBFuture<NSNull *> *)resolveState:(FBiOSTargetState)state
{
return FBiOSTargetResolveState(self, state);
}
- (FBFuture<NSNull *> *)resolveLeavesState:(FBiOSTargetState)state
{
return FBiOSTargetResolveLeavesState(self, state);
}
- (NSDictionary<NSString *, NSString *> *)replacementMapping
{
return NSDictionary.dictionary;
}
#pragma mark Not supported
- (FBFuture<id<FBVideoStream>> *)createStreamWithConfiguration:(FBVideoStreamConfiguration *)configuration
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<id<FBiOSTargetOperation>> *)startRecordingToFile:(NSString *)filePath
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<NSNull *> *)stopRecording
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<NSArray<NSString *> *> *)listTestsForBundleAtPath:(NSString *)bundlePath timeout:(NSTimeInterval)timeout withAppAtPath:(NSString *)appPath
{
FBListTestConfiguration *configuration = [FBListTestConfiguration
configurationWithEnvironment:@{}
workingDirectory:self.auxillaryDirectory
testBundlePath:bundlePath
runnerAppPath:appPath
waitForDebugger:NO
timeout:timeout];
return [[[FBListTestStrategy alloc]
initWithTarget:self
configuration:configuration
logger:self.logger]
listTests];
}
- (FBFuture<id<FBiOSTargetOperation>> *)tailLog:(NSArray<NSString *> *)arguments consumer:(id<FBDataConsumer>)consumer
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<NSData *> *)takeScreenshot:(FBScreenshotFormat)format
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<FBCrashLogInfo *> *)notifyOfCrash:(NSPredicate *)predicate
{
return [FBCrashLogNotifier.sharedInstance nextCrashLogForPredicate:predicate];
}
- (FBFuture<NSArray<FBCrashLogInfo *> *> *)crashes:(NSPredicate *)predicate useCache:(BOOL)useCache
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<NSArray<FBCrashLogInfo *> *> *)pruneCrashes:(NSPredicate *)predicate
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFutureContext<id<FBFileContainer>> *)crashLogFiles
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFutureContext];
}
- (FBFuture<FBInstrumentsOperation *> *)startInstruments:(FBInstrumentsConfiguration *)configuration logger:(id<FBControlCoreLogger>)logger
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<FBXCTraceRecordOperation *> *)startXctraceRecord:(FBXCTraceRecordConfiguration *)configuration logger:(id<FBControlCoreLogger>)logger
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<FBProcess *> *)launchProcess:(FBProcessSpawnConfiguration *)configuration
{
return [FBProcess launchProcessWithConfiguration:configuration logger:self.logger];
}
@end