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