XCTestBootstrap/TestManager/FBTestManagerAPIMediator.m (412 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 "FBTestManagerAPIMediator.h"
#import <XCTestPrivate/XCTestDriverInterface-Protocol.h>
#import <XCTestPrivate/XCTestManager_DaemonConnectionInterface-Protocol.h>
#import <XCTestPrivate/XCTestManager_IDEInterface-Protocol.h>
#import <DTXConnectionServices/DTXConnection.h>
#import <DTXConnectionServices/DTXProxyChannel.h>
#import <DTXConnectionServices/DTXRemoteInvocationReceipt.h>
#import <DTXConnectionServices/DTXSocketTransport.h>
#import <DTXConnectionServices/DTXTransport.h>
#import <FBControlCore/FBControlCore.h>
#import <objc/runtime.h>
#import "XCTestBootstrapError.h"
#import "FBTestBundleConnection.h"
#import "FBTestManagerContext.h"
#import "FBTestManagerResultSummary.h"
#import "FBTestReporterAdapter.h"
#import "FBXCTestProcess.h"
#import "FBXCTestReporter.h"
const NSInteger FBProtocolVersion = 0x16;
const NSInteger FBProtocolMinimumVersion = 0x8;
@interface FBTestManagerAPIMediator () <XCTestManager_IDEInterface>
@property (nonatomic, strong, readonly) FBTestManagerContext *context;
@property (nonatomic, strong, readonly) id<FBiOSTarget, FBXCTestExtendedCommands> target;
@property (nonatomic, strong, readonly) id<FBXCTestReporter> reporter;
@property (nonatomic, strong, nullable, readonly) id<FBControlCoreLogger> logger;
@property (nonatomic, strong, readonly) dispatch_queue_t requestQueue;
@property (nonatomic, strong, readonly) FBTestReporterAdapter *reporterAdapter;
@property (nonatomic, strong, readonly) NSMutableDictionary<NSString *, id<FBLaunchedApplication>> *tokenToLaunchedAppMap;
@end
@implementation FBTestManagerAPIMediator
#pragma mark - Initializers
+ (FBFuture<NSNull *> *)connectAndRunUntilCompletionWithContext:(FBTestManagerContext *)context target:(id<FBiOSTarget, FBXCTestExtendedCommands>)target reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger
{
FBTestManagerAPIMediator *mediator = [[self alloc] initWithContext:context target:target reporter:reporter logger:logger];
return [mediator connectAndRunUntilCompletion];
}
- (instancetype)initWithContext:(FBTestManagerContext *)context target:(id<FBiOSTarget, FBXCTestExtendedCommands>)target reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger
{
self = [super init];
if (!self) {
return nil;
}
_context = context;
_target = target;
_reporter = reporter;
_logger = logger;
_tokenToLaunchedAppMap = [NSMutableDictionary new];
_requestQueue = dispatch_queue_create("com.facebook.xctestboostrap.mediator", DISPATCH_QUEUE_PRIORITY_DEFAULT);
_reporterAdapter = [FBTestReporterAdapter withReporter:reporter];
return self;
}
#pragma mark - NSObject
- (NSString *)description
{
return [NSString stringWithFormat:
@"TestManager for (%@)",
self.context
];
}
#pragma mark - Private
static const NSTimeInterval DefaultTestTimeout = (60 * 60); // 1 hour.
- (FBFuture<NSNull *> *)terminateSpawnedProcesses
{
NSArray<id<FBLaunchedApplication>> *appsToKill = [self.tokenToLaunchedAppMap allValues];
[self.tokenToLaunchedAppMap removeAllObjects];
if (appsToKill.count > 0) {
[self.logger logFormat:@"Terminating processes spawned due to test bundle requests: %@", [FBCollectionInformation oneLineDescriptionFromArray:appsToKill]];
NSMutableArray<FBFuture *> *futuresToWait = [NSMutableArray arrayWithCapacity:appsToKill.count];
for (id<FBLaunchedApplication> app in appsToKill) {
[futuresToWait addObject:[self.target killApplicationWithBundleID:app.bundleID]];
}
return [FBFuture futureWithFutures:futuresToWait.copy];
}
return FBFuture.empty;
}
- (FBFuture<NSNull *> *)connectAndRunUntilCompletion
{
id<FBControlCoreLogger> logger = self.logger;
id<FBXCTestReporter> reporter = self.reporter;
dispatch_queue_t queue = self.requestQueue;
NSTimeInterval timeout = self.context.timeout <= 0 ? DefaultTestTimeout : self.context.timeout;
return [[[self
startAndRunApplicationTestHost]
onQueue:queue pop:^(id<FBLaunchedApplication> launchedApplication) {
bool waitForDebugger = self.context.testHostLaunchConfiguration.waitForDebugger;
FBFuture *future = FBFuture.empty;
if (waitForDebugger) {
[reporter processWaitingForDebuggerWithProcessIdentifier:launchedApplication.processIdentifier];
future = [FBProcessFetcher waitForDebuggerToAttachAndContinueFor:launchedApplication.processIdentifier];
}
return [future onQueue:queue fmap:^(id _) {
return [self runUntilCompletion:launchedApplication logger:logger queue:queue timeout:timeout];
}];
}]
onQueue:queue chain:^(FBFuture<NSNull *> *future) {
[reporter processUnderTestDidExit];
NSError *error = future.error;
if (error) {
[logger logFormat:@"Test Execution finished in error %@", error];
[reporter didCrashDuringTest:error];
}
return future;
}];
}
- (FBFuture<NSNull *> *)runUntilCompletion:(id<FBLaunchedApplication>)launchedApplication logger:(id<FBControlCoreLogger>)logger queue:(dispatch_queue_t)queue timeout:(NSTimeInterval)timeout
{
return [[[FBTestBundleConnection
connectAndRunBundleToCompletionWithContext:self.context
target:self.target
interface:(id)self
testHostApplication:launchedApplication
requestQueue:self.requestQueue
logger:logger]
onQueue:queue fmap:^(NSNull *_) {
// The bundle has disconnected at this point, but we also need to terminate any processes
// spawned through `_XCT_launchProcessWithPath`and wait for the host application to terminate
return [[self terminateSpawnedProcesses] chainReplace:launchedApplication.applicationTerminated];
}]
onQueue:queue timeout:timeout handler:^{
// The timeout is applied to the lifecycle of the entire application.
[logger logFormat:@"Timed out after %f, attempting stack sample", timeout];
return [[FBXCTestProcess
performSampleStackshotOnProcessIdentifier:launchedApplication.processIdentifier
forTimeout:timeout
queue:queue
logger:logger]
onQueue:queue chain:^FBFuture *(FBFuture *future) {
return [[self terminateSpawnedProcesses] chainReplace:future];
}];
}];
}
- (FBFutureContext<id<FBLaunchedApplication>> *)startAndRunApplicationTestHost
{
return [[self.target
launchApplication:self.context.testHostLaunchConfiguration]
onQueue:self.target.workQueue contextualTeardown:^(id<FBLaunchedApplication> launchedApplication, FBFutureState _) {
return [launchedApplication.applicationTerminated cancel];
}];
}
- (FBFuture<id<FBLaunchedApplication>> *)installAndLaunchApplication:(FBApplicationLaunchConfiguration *)configuration atPath:(NSString *)path
{
if (!path) {
return [[FBControlCoreError
describeFormat:@"Could not install App-Under-Test %@ as it is not installed and no path was provided", configuration]
failFuture];
}
return [[[[self
isApplicationInstalledWithBundleID:configuration.bundleID]
onQueue:self.target.workQueue fmap:^FBFuture<NSNull *> *(NSNumber *isInstalled) {
if (!isInstalled.boolValue) {
return FBFuture.empty;
}
return [self.target uninstallApplicationWithBundleID:configuration.bundleID];
}]
onQueue:self.target.workQueue fmap:^(NSNull *_) {
return [self.target installApplicationWithPath:path];
}]
onQueue:self.target.workQueue fmap:^(NSNull *_) {
return [self.target launchApplication:configuration];
}];
}
- (FBFuture<id<FBLaunchedApplication>> *)launchApplication:(FBApplicationLaunchConfiguration *)configuration atPath:(NSString *)path
{
// Check if path points to installed app
return [[self.target
installedApplicationWithBundleID:configuration.bundleID]
onQueue:self.target.workQueue chain:^(FBFuture<FBInstalledApplication *> *future) {
FBInstalledApplication *app = future.result;
if (app && [app.bundle.path isEqualToString:path]) {
return [self.target launchApplication:configuration];
}
return [self installAndLaunchApplication:configuration atPath:path];
}];
}
- (FBFuture<NSNumber *> *)isApplicationInstalledWithBundleID:(NSString *)bundleID
{
return [[self.target
installedApplicationWithBundleID:bundleID]
onQueue:self.target.asyncQueue chain:^(FBFuture<FBInstalledApplication *> *future) {
return [FBFuture futureWithResult:(future.state == FBFutureStateDone ? @YES : @NO)];
}];
}
#pragma mark - XCTestManager_IDEInterface protocol
#pragma mark Process Launch Delegation
- (id)_XCT_launchProcessWithPath:(NSString *)path bundleID:(NSString *)bundleID arguments:(NSArray *)arguments environmentVariables:(NSDictionary *)environment
{
[self.logger logFormat:@"Test process requested process launch with bundleID %@", bundleID];
NSMutableDictionary<NSString *, NSString *> *targetEnvironment = @{}.mutableCopy;
[targetEnvironment addEntriesFromDictionary:self.context.testedApplicationAdditionalEnvironment];
[targetEnvironment addEntriesFromDictionary:environment];
FBProcessIO *processIO = [[FBProcessIO alloc]
initWithStdIn:nil
stdOut:[FBProcessOutput outputForLogger:self.logger]
stdErr:[FBProcessOutput outputForLogger:self.logger]];
DTXRemoteInvocationReceipt *receipt = [objc_lookUpClass("DTXRemoteInvocationReceipt") new];
FBApplicationLaunchConfiguration *launch = [[FBApplicationLaunchConfiguration alloc]
initWithBundleID:bundleID
bundleName:bundleID
arguments:arguments
environment:targetEnvironment
waitForDebugger:NO
io:processIO
launchMode:FBApplicationLaunchModeFailIfRunning];
id token = @(receipt.hash);
[[self
launchApplication:launch atPath:path]
onQueue:self.target.workQueue notifyOfCompletion:^(FBFuture<id<FBLaunchedApplication>> *future) {
NSError *innerError = future.error;
if (innerError) {
[receipt invokeCompletionWithReturnValue:nil error:innerError];
} else {
self.tokenToLaunchedAppMap[token] = future.result;
[receipt invokeCompletionWithReturnValue:token error:nil];
}
}];
return receipt;
}
- (id)_XCT_getProgressForLaunch:(id)token
{
[self.logger logFormat:@"Test process requested launch process status with token %@", token];
DTXRemoteInvocationReceipt *receipt = [objc_lookUpClass("DTXRemoteInvocationReceipt") new];
[receipt invokeCompletionWithReturnValue:@1 error:nil];
return receipt;
}
- (id)_XCT_terminateProcess:(id)token
{
[self.logger logFormat:@"Test process requested process termination with token %@", token];
NSError *error;
DTXRemoteInvocationReceipt *receipt = [objc_lookUpClass("DTXRemoteInvocationReceipt") new];
if (!token) {
error = [NSError errorWithDomain:@"XCTestIDEInterfaceErrorDomain"
code:0x1
userInfo:@{NSLocalizedDescriptionKey : @"API violation: token was nil."}];
}
else {
NSString *bundleID = self.tokenToLaunchedAppMap[token].bundleID;
if (!bundleID) {
error = [NSError errorWithDomain:@"XCTestIDEInterfaceErrorDomain"
code:0x2
userInfo:@{NSLocalizedDescriptionKey : @"Invalid or expired token: no matching operation was found."}];
} else {
[[self.target killApplicationWithBundleID:bundleID]
onQueue:self.target.workQueue notifyOfCompletion:^(FBFuture<NSNull *> *future) {
[receipt invokeCompletionWithReturnValue:token error:future.error];
}];
}
}
if (error) {
[self.logger logFormat:@"Failed to kill process with token %@ dure to %@", token, error];
}
return receipt;
}
#pragma mark iOS 10.x
- (id)_XCT_didBeginInitializingForUITesting
{
[self.logger log:@"Started initilizing for UI testing."];
return nil;
}
- (id)_XCT_initializationForUITestingDidFailWithError:(NSError *)error
{
return nil;
}
- (id)_XCT_handleCrashReportData:(NSData *)arg1 fromFileWithName:(NSString *)arg2
{
return nil;
}
#pragma mark Test Suite Progress
- (id)_XCT_testSuite:(NSString *)testSuite didStartAt:(NSString *)time
{
[self.logger logFormat:@"Test Suite %@ started", testSuite];
if (testSuite.length == 0) {
NSError *error = [[[[XCTestBootstrapError
describe:@"Test reported a suite with nil or empty identifier. This is unsupported."]
inDomain:@"IDETestOperationsObserverErrorDomain"]
code:0x9]
build];
[self.logger logFormat:@"%@", error];
}
[self.reporterAdapter _XCT_testSuite:testSuite didStartAt:time];
return nil;
}
- (id)_XCT_didBeginExecutingTestPlan
{
[self.logger logFormat:@"Test Plan Started"];
[self.reporterAdapter _XCT_didBeginExecutingTestPlan];
return nil;
}
- (id)_XCT_didFinishExecutingTestPlan
{
[self.logger logFormat:@"Test Plan Ended"];
[self.reporterAdapter _XCT_didFinishExecutingTestPlan];
return nil;
}
- (id)_XCT_testBundleReadyWithProtocolVersion:(NSNumber *)protocolVersion minimumVersion:(NSNumber *)minimumVersion
{
return nil;
}
- (id)_XCT_testCaseDidStartForTestClass:(NSString *)testClass method:(NSString *)method
{
[self.logger logFormat:@"Test Case %@/%@ did start", testClass, method];
[self.reporterAdapter _XCT_testCaseDidStartForTestClass:testClass method:method];
return nil;
}
- (id)_XCT_testCaseDidFailForTestClass:(NSString *)testClass method:(NSString *)method withMessage:(NSString *)message file:(NSString *)file line:(NSNumber *)line
{
[self.logger logFormat:@"Test Case %@/%@ did fail: %@", testClass, method, message];
[self.reporterAdapter _XCT_testCaseDidFailForTestClass:testClass method:method withMessage:message file:file line:line];
return nil;
}
- (id)_XCT_logDebugMessage:(NSString *)debugMessage
{
[self.logger log:[debugMessage stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]];
return nil;
}
- (id)_XCT_logMessage:(NSString *)message
{
return nil;
}
- (id)_XCT_testCaseDidFinishForTestClass:(NSString *)testClass method:(NSString *)method withStatus:(NSString *)statusString duration:(NSNumber *)duration
{
[self.logger logFormat:@"Test Case %@/%@ did finish (%@)", testClass, method, statusString];
[self.reporterAdapter _XCT_testCaseDidFinishForTestClass:testClass method:method withStatus:statusString duration:duration];
return nil;
}
- (id)_XCT_testSuite:(NSString *)testSuite didFinishAt:(NSString *)time runCount:(NSNumber *)runCount withFailures:(NSNumber *)failures unexpected:(NSNumber *)unexpected testDuration:(NSNumber *)testDuration totalDuration:(NSNumber *)totalDuration
{
[self.logger logFormat:@"Test Suite Did Finish %@", testSuite];
[self. reporterAdapter _XCT_testSuite:testSuite didFinishAt:time runCount:runCount withFailures:failures unexpected:unexpected testDuration:testDuration totalDuration:totalDuration];
return nil;
}
- (id)_XCT_testCase:(NSString *)testCase method:(NSString *)method didFinishActivity:(XCActivityRecord *)activity
{
[self.reporterAdapter _XCT_testCase:testCase method:method didFinishActivity:activity];
return nil;
}
- (id)_XCT_testCase:(NSString *)testCase method:(NSString *)method willStartActivity:(XCActivityRecord *)activity
{
[self.reporterAdapter _XCT_testCase:testCase method:method willStartActivity:activity];
return nil;
}
#pragma mark - Unimplemented
- (id)_XCT_nativeFocusItemDidChangeAtTime:(NSNumber *)arg1 parameterSnapshot:(XCElementSnapshot *)arg2 applicationSnapshot:(XCElementSnapshot *)arg3
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_recordedEventNames:(NSArray *)arg1 timestamp:(NSNumber *)arg2 duration:(NSNumber *)arg3 startLocation:(NSDictionary *)arg4 startElementSnapshot:(XCElementSnapshot *)arg5 startApplicationSnapshot:(XCElementSnapshot *)arg6 endLocation:(NSDictionary *)arg7 endElementSnapshot:(XCElementSnapshot *)arg8 endApplicationSnapshot:(XCElementSnapshot *)arg9
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_recordedOrientationChange:(NSString *)arg1
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_recordedFirstResponderChangedWithApplicationSnapshot:(XCElementSnapshot *)arg1
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_exchangeCurrentProtocolVersion:(NSNumber *)arg1 minimumVersion:(NSNumber *)arg2
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_recordedKeyEventsWithApplicationSnapshot:(XCElementSnapshot *)arg1 characters:(NSString *)arg2 charactersIgnoringModifiers:(NSString *)arg3 modifierFlags:(NSNumber *)arg4
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_recordedEventNames:(NSArray *)arg1 duration:(NSNumber *)arg2 startLocation:(NSDictionary *)arg3 startElementSnapshot:(XCElementSnapshot *)arg4 startApplicationSnapshot:(XCElementSnapshot *)arg5 endLocation:(NSDictionary *)arg6 endElementSnapshot:(XCElementSnapshot *)arg7 endApplicationSnapshot:(XCElementSnapshot *)arg8
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_recordedKeyEventsWithCharacters:(NSString *)arg1 charactersIgnoringModifiers:(NSString *)arg2 modifierFlags:(NSNumber *)arg3
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_recordedEventNames:(NSArray *)arg1 duration:(NSNumber *)arg2 startElement:(XCAccessibilityElement *)arg3 startApplicationSnapshot:(XCElementSnapshot *)arg4 endElement:(XCAccessibilityElement *)arg5 endApplicationSnapshot:(XCElementSnapshot *)arg6
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_recordedEvent:(NSString *)arg1 targetElementID:(NSDictionary *)arg2 applicationSnapshot:(XCElementSnapshot *)arg3
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_recordedEvent:(NSString *)arg1 forElement:(NSString *)arg2
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_testMethod:(NSString *)arg1 ofClass:(NSString *)arg2 didMeasureMetric:(NSDictionary *)arg3 file:(NSString *)arg4 line:(NSNumber *)arg5
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_testCase:(NSString *)arg1 method:(NSString *)arg2 didStallOnMainThreadInFile:(NSString *)arg3 line:(NSNumber *)arg4
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_testMethod:(NSString *)arg1 ofClass:(NSString *)arg2 didMeasureValues:(NSArray *)arg3 forPerformanceMetricID:(NSString *)arg4 name:(NSString *)arg5 withUnits:(NSString *)arg6 baselineName:(NSString *)arg7 baselineAverage:(NSNumber *)arg8 maxPercentRegression:(NSNumber *)arg9 maxPercentRelativeStandardDeviation:(NSNumber *)arg10 file:(NSString *)arg11 line:(NSNumber *)arg12
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_testBundleReady
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (id)_XCT_testRunnerReadyWithCapabilities:(XCTCapabilities *)arg1
{
return [self handleUnimplementedXCTRequest:_cmd];
}
- (NSString *)unknownMessageForSelector:(SEL)aSelector
{
return [NSString stringWithFormat:@"Received call for unhandled method (%@). Probably you should have a look at _IDETestManagerAPIMediator in IDEFoundation.framework and implement it. Good luck!", NSStringFromSelector(aSelector)];
}
// This will add more logs when unimplemented method from XCTestManager_IDEInterface protocol is called
- (id)handleUnimplementedXCTRequest:(SEL)aSelector
{
[self.logger log:[self unknownMessageForSelector:aSelector]];
NSAssert(nil, [self unknownMessageForSelector:_cmd]);
return nil;
}
@end