Skip to content

Commit

Permalink
fix: Prevent freeze on launch/activate of a missing app (#706)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored May 16, 2023
1 parent f46e86f commit c4976e3
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 24 deletions.
7 changes: 7 additions & 0 deletions PrivateHeaders/XCTest/XCTRunnerDaemonSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,11 @@
@property(readonly) _Bool supportsLocationSimulation;
#endif

// Since Xcode 10.2
- (void)launchApplicationWithPath:(NSString *)arg1
bundleID:(NSString *)arg2
arguments:(NSArray *)arg3
environment:(NSDictionary *)arg4
completion:(void (^)(_Bool, NSError *))arg5;

@end
3 changes: 2 additions & 1 deletion WebDriverAgentLib/Routing/FBExceptionHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ - (void)handleException:(NSException *)exception forResponse:(RouteResponse *)re
commandStatus = [FBCommandStatus noSuchDriverErrorWithMessage:exception.reason
traceback:traceback];
} else if ([exception.name isEqualToString:FBInvalidArgumentException]
|| [exception.name isEqualToString:FBElementAttributeUnknownException]) {
|| [exception.name isEqualToString:FBElementAttributeUnknownException]
|| [exception.name isEqualToString:FBApplicationMissingException]) {
commandStatus = [FBCommandStatus invalidArgumentErrorWithMessage:exception.reason
traceback:traceback];
} else if ([exception.name isEqualToString:FBApplicationCrashedException]
Expand Down
3 changes: 3 additions & 0 deletions WebDriverAgentLib/Routing/FBExceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,7 @@ extern NSString *const FBClassChainQueryParseException;
/*! Exception used to notify about application crash */
extern NSString *const FBApplicationCrashedException;

/*! Exception used to notify about the application is not installed */
extern NSString *const FBApplicationMissingException;

NS_ASSUME_NONNULL_END
1 change: 1 addition & 0 deletions WebDriverAgentLib/Routing/FBExceptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
NSString *const FBXPathQueryEvaluationException = @"FBXPathQueryEvaluationException";
NSString *const FBClassChainQueryParseException = @"FBClassChainQueryParseException";
NSString *const FBApplicationCrashedException = @"FBApplicationCrashedException";
NSString *const FBApplicationMissingException = @"FBApplicationMissingException";
81 changes: 58 additions & 23 deletions WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,69 @@

#import "FBConfiguration.h"
#import "FBErrorBuilder.h"
#import "FBExceptions.h"
#import "FBLogger.h"
#import "FBRunLoopSpinner.h"
#import "XCTestDriver.h"
#import "XCTRunnerDaemonSession.h"
#import "XCUIApplication.h"
#import "XCUIDevice.h"

@implementation FBXCTestDaemonsProxy
#define LAUNCH_APP_TIMEOUT_SEC 300

static void (*originalLaunchAppMethod)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *));

static Class FBXCTRunnerDaemonSessionClass = nil;
static dispatch_once_t onceTestRunnerDaemonClass;
static void swizzledLaunchApp(id self, SEL _cmd, NSString *path, NSString *bundleID,
NSArray *arguments, NSDictionary *environment,
void (^reply)(_Bool, NSError *))
{
__block BOOL isSuccessful;
__block NSError *error;
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
originalLaunchAppMethod(self, _cmd, path, bundleID, arguments, environment, ^(BOOL passed, NSError *innerError) {
isSuccessful = passed;
error = innerError;
dispatch_semaphore_signal(sem);
});
int64_t timeoutNs = (int64_t)(LAUNCH_APP_TIMEOUT_SEC * NSEC_PER_SEC);
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) {
NSString *message = [NSString stringWithFormat:@"The application '%@' cannot be launched within %d seconds timeout",
bundleID ?: path, LAUNCH_APP_TIMEOUT_SEC];
@throw [NSException exceptionWithName:FBTimeoutException reason:message userInfo:nil];
}
if (!isSuccessful || nil != error) {
[FBLogger logFmt:@"%@", error.description];
NSString *message = error.description ?: [NSString stringWithFormat:@"The application '%@' is not installed on the device under test",
bundleID ?: path];
@throw [NSException exceptionWithName:FBApplicationMissingException reason:message userInfo:nil];
}
reply(isSuccessful, error);
}

@implementation FBXCTestDaemonsProxy

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-load-method"

+ (void)load
{
dispatch_once(&onceTestRunnerDaemonClass, ^{
FBXCTRunnerDaemonSessionClass = objc_lookUpClass("XCTRunnerDaemonSession");
});
[self.class swizzleLaunchApp];
}

#pragma clang diagnostic pop

+ (void)swizzleLaunchApp {
Method original = class_getInstanceMethod([XCTRunnerDaemonSession class],
@selector(launchApplicationWithPath:bundleID:arguments:environment:completion:));
if (original == nil) {
[FBLogger log:@"Could not find method -[XCTRunnerDaemonSession launchApplicationWithPath:]"];
return;
}
// Workaround for https://github.com/appium/WebDriverAgent/issues/702
originalLaunchAppMethod = (void(*)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *))) method_getImplementation(original);
method_setImplementation(original, (IMP)swizzledLaunchApp);
}

+ (id<XCTestManager_ManagerInterface>)testRunnerProxy
{
static id<XCTestManager_ManagerInterface> proxy = nil;
Expand All @@ -56,7 +95,7 @@ + (void)load

+ (id<XCTestManager_ManagerInterface>)retrieveTestRunnerProxy
{
return ((XCTRunnerDaemonSession *)[FBXCTRunnerDaemonSessionClass sharedSession]).daemonProxy;
return ((XCTRunnerDaemonSession *)[XCTRunnerDaemonSession sharedSession]).daemonProxy;
}

+ (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error
Expand All @@ -70,24 +109,20 @@ + (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSEr
didSucceed = (invokeError == nil);
completion();
};

if (nil == FBXCTRunnerDaemonSessionClass) {
[[self testRunnerProxy] _XCT_synthesizeEvent:record completion:errorHandler];
} else {
XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) {
errorHandler(invokeError);
};
[[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) {
handlerBlock(record, invokeError);
}];
}

XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) {
errorHandler(invokeError);
};
[[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) {
handlerBlock(record, invokeError);
}];
}];
return didSucceed;
}

+ (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError *__autoreleasing*)error
{
XCTRunnerDaemonSession *session = [FBXCTRunnerDaemonSessionClass sharedSession];
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
if (![session respondsToSelector:@selector(openURL:usingApplication:completion:)]) {
if (error) {
[[[FBErrorBuilder builder]
Expand All @@ -112,7 +147,7 @@ + (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSErro

+ (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasing*)error
{
XCTRunnerDaemonSession *session = [FBXCTRunnerDaemonSessionClass sharedSession];
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
if (![session respondsToSelector:@selector(openDefaultApplicationForURL:completion:)]) {
if (error) {
[[[FBErrorBuilder builder]
Expand All @@ -138,7 +173,7 @@ + (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasin
#if !TARGET_OS_TV
+ (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleasing*)error
{
XCTRunnerDaemonSession *session = [FBXCTRunnerDaemonSessionClass sharedSession];
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
if (![session respondsToSelector:@selector(setSimulatedLocation:completion:)]) {
if (error) {
[[[FBErrorBuilder builder]
Expand Down Expand Up @@ -171,7 +206,7 @@ + (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleas

+ (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error;
{
XCTRunnerDaemonSession *session = [FBXCTRunnerDaemonSessionClass sharedSession];
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
if (![session respondsToSelector:@selector(getSimulatedLocationWithReply:)]) {
if (error) {
[[[FBErrorBuilder builder]
Expand Down Expand Up @@ -206,7 +241,7 @@ + (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error;

+ (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error
{
XCTRunnerDaemonSession *session = [FBXCTRunnerDaemonSessionClass sharedSession];
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) {
if (error) {
[[[FBErrorBuilder builder]
Expand Down
23 changes: 23 additions & 0 deletions WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#import "FBIntegrationTestCase.h"
#import "FBApplication.h"
#import "FBExceptions.h"
#import "FBMacros.h"
#import "FBSession.h"
#import "FBXCodeCompatibility.h"
Expand Down Expand Up @@ -107,4 +108,26 @@ - (void)testLaunchUnattachedApp
XCTAssertEqualObjects(SETTINGS_BUNDLE_ID, FBApplication.fb_activeApplication.bundleID);
}

- (void)testAppWithInvalidBundleIDCannotBeStarted
{
FBApplication *testedApp = [[FBApplication alloc] initWithBundleIdentifier:@"yolo"];
@try {
[testedApp launch];
XCTFail(@"An exception is expected to be thrown");
} @catch (NSException *exception) {
XCTAssertEqualObjects(FBApplicationMissingException, exception.name);
}
}

- (void)testAppWithInvalidBundleIDCannotBeActivated
{
FBApplication *testedApp = [[FBApplication alloc] initWithBundleIdentifier:@"yolo"];
@try {
[testedApp activate];
XCTFail(@"An exception is expected to be thrown");
} @catch (NSException *exception) {
XCTAssertEqualObjects(FBApplicationMissingException, exception.name);
}
}

@end

0 comments on commit c4976e3

Please sign in to comment.