Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion WebDriverAgentLib/Commands/FBSessionCommands.m
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ + (NSArray *)routes
[request.session launchApplicationWithBundleId:(id)request.arguments[@"bundleId"]
shouldWaitForQuiescence:request.arguments[@"shouldWaitForQuiescence"]
arguments:request.arguments[@"arguments"]
environment:request.arguments[@"environment"]];
environment:request.arguments[@"environment"]
asApplicationUnderTest:[request.arguments[@"asAppUnderTest"] boolValue]];
return FBResponseWithOK();
}

Expand Down
22 changes: 22 additions & 0 deletions WebDriverAgentLib/Routing/FBSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,28 @@ extern NSString* const FBSessionWasKilledNotification;
arguments:(nullable NSArray<NSString *> *)arguments
environment:(nullable NSDictionary <NSString *, NSString *> *)environment;

/**
Launch an application with given bundle identifier in scope of current session
and optionally make it the application under test of this session.

@param bundleIdentifier Valid bundle identifier of the application to be launched
@param shouldWaitForQuiescence whether to wait for quiescence on application startup.
If not provided then the setting of the current application under test is inherited.
If there is no application under test yet then quiescence is only awaited if asApplicationUnderTest is YES
@param arguments The optional array of application command line arguments. The arguments are going to be applied if the application was not running before.
@param environment The optional dictionary of environment variables for the application, which is going to be executed. The environment variables are going to be applied if the application was not running before.
@param asApplicationUnderTest If YES then the launched application replaces the current application under test
of the session (if any), the same way as if it was launched on session startup: other applications launched later
inherit its quiescence setting, system alerts over it are respected, its unexpected termination is reported as a crash
and it gets terminated on session deletion if shouldTerminateApp is enabled
@return The application instance
*/
- (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier
shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence
arguments:(nullable NSArray<NSString *> *)arguments
environment:(nullable NSDictionary <NSString *, NSString *> *)environment
asApplicationUnderTest:(BOOL)asApplicationUnderTest;

/**
Activate an application with given bundle identifier in scope of current session.
!This method is only available since Xcode9 SDK
Expand Down
69 changes: 66 additions & 3 deletions WebDriverAgentLib/Routing/FBSession.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
NSString *const FBSessionWasKilledNotification = @"FBSessionWasKilledNotification";

@interface FBSession ()
@property (nullable, nonatomic) XCUIApplication *testedApplication;
// Atomic, since /wda/apps/launch may replace it while a concurrent DELETE /session reads it in -kill
@property (nullable, atomic) XCUIApplication *testedApplication;
@property (nonatomic) BOOL isTestedApplicationExpectedToRun;
@property (nonatomic) BOOL shouldAppsWaitForQuiescence;
@property (nonatomic, nullable) FBAlertsMonitor *alertsMonitor;
Expand Down Expand Up @@ -387,10 +388,42 @@ - (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier
arguments:(nullable NSArray<NSString *> *)arguments
environment:(nullable NSDictionary <NSString *, NSString *> *)environment
{
return [self launchApplicationWithBundleId:bundleIdentifier
shouldWaitForQuiescence:shouldWaitForQuiescence
arguments:arguments
environment:environment
asApplicationUnderTest:NO];
}

- (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier
shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence
arguments:(nullable NSArray<NSString *> *)arguments
environment:(nullable NSDictionary <NSString *, NSString *> *)environment
asApplicationUnderTest:(BOOL)asApplicationUnderTest
{
NSCondition *condition = self.class.teardownCondition;
NSUInteger generation = 0;
BOOL shouldTerminateApp = NO;
if (asApplicationUnderTest) {
[condition lock];
BOOL isActive = self == _activeSession;
generation = _sessionGeneration;
shouldTerminateApp = FBConfiguration.sharedInstance.shouldTerminateApp;
[condition unlock];
if (!isActive) {
@throw [NSException exceptionWithName:FBSessionDoesNotExistException
reason:@"Session does not exist"
userInfo:nil];
}
}

XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier];
if (nil == shouldWaitForQuiescence) {
// Iherit the quiescence check setting from the main app under test by default
app.fb_shouldWaitForQuiescence = nil != self.testedApplication && self.shouldAppsWaitForQuiescence;
// Iherit the quiescence check setting from the main app under test by default.
// The first app under test of the session waits for quiescence, as it would on session startup
app.fb_shouldWaitForQuiescence = nil == self.testedApplication
? asApplicationUnderTest
: self.shouldAppsWaitForQuiescence;
} else {
app.fb_shouldWaitForQuiescence = [shouldWaitForQuiescence boolValue];
}
Expand All @@ -401,6 +434,36 @@ - (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier
} else {
[app activate];
}
if (asApplicationUnderTest) {
BOOL waitForQuiescence = app.fb_shouldWaitForQuiescence;
// A DELETE can finish while launch/activate is still waiting for XCTest.
// Publish the AUT under the same lock that -kill uses to clear the session.
[condition lock];
BOOL isActive = self == _activeSession && generation == _sessionGeneration;
if (isActive) {
self.testedApplication = app;
self.shouldAppsWaitForQuiescence = waitForQuiescence;
}
[condition unlock];
if (!isActive) {
// The HTTP request may already have been abandoned, but its handler keeps
// running. Clean up only while this launch still owns the generation;
// a replacement session may already be using the same bundle identifier.
if (shouldTerminateApp
&& ![app fb_isSameAppAs:[[app class] fb_systemApplication]]
&& [self.class beginTerminationForGeneration:generation]) {
@try {
[app terminate];
} @finally {
[self.class endTermination];
}
}
@throw [NSException exceptionWithName:FBSessionDoesNotExistException
reason:@"Session was deleted during application launch"
userInfo:nil];
}
[FBLogger logFmt:@"The application '%@' is set as the application under test", bundleIdentifier];
}
if ([app fb_isSameAppAs:self.testedApplication]) {
self.isTestedApplicationExpectedToRun = YES;
}
Expand Down
71 changes: 71 additions & 0 deletions WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#import "FBTestMacros.h"
#import "FBUnattachedAppLauncher.h"
#import "XCUIApplication+FBHelpers.h"
#import "XCUIApplication+FBQuiescence.h"
#import "XCUIApplication.h"

@interface FBSession (Tests)
Expand Down Expand Up @@ -101,6 +102,76 @@ - (void)testMainAppCanBeRestartedInScopeOfTheCurrentSession
FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:testedApp.bundleID]);
}

- (NSString *)startSessionWithoutApplicationUnderTest
{
NSString *bundleId = (NSString *)self.testedApplication.bundleID;
[self.testedApplication terminate];
FBAssertWaitTillBecomesTrue(self.testedApplication.state == XCUIApplicationStateNotRunning);
self.session = [FBSession initWithApplication:nil];
return bundleId;
}

- (void)testApplicationCanBeLaunchedAsApplicationUnderTest
{
NSString *bundleId = [self startSessionWithoutApplicationUnderTest];
XCUIApplication *app = [self.session launchApplicationWithBundleId:bundleId
shouldWaitForQuiescence:nil
arguments:nil
environment:nil
asApplicationUnderTest:YES];
FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:bundleId]);
XCTAssertEqual(app, self.session.activeApplication);
XCTAssertTrue(app.fb_shouldWaitForQuiescence);

XCUIApplication *settingsApp = [self.session launchApplicationWithBundleId:SETTINGS_BUNDLE_ID
shouldWaitForQuiescence:nil
arguments:nil
environment:nil];
FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:SETTINGS_BUNDLE_ID]);
XCTAssertTrue(settingsApp.fb_shouldWaitForQuiescence);
}

- (void)testApplicationLaunchedAsApplicationUnderTestRespectsQuiescenceArgument
{
NSString *bundleId = [self startSessionWithoutApplicationUnderTest];
XCUIApplication *app = [self.session launchApplicationWithBundleId:bundleId
shouldWaitForQuiescence:@NO
arguments:nil
environment:nil
asApplicationUnderTest:YES];
FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:bundleId]);
XCTAssertEqual(app, self.session.activeApplication);
XCTAssertFalse(app.fb_shouldWaitForQuiescence);
}

- (void)testCrashOfApplicationLaunchedAsApplicationUnderTestIsDetected
{
NSString *bundleId = [self startSessionWithoutApplicationUnderTest];
XCUIApplication *app = [self.session launchApplicationWithBundleId:bundleId
shouldWaitForQuiescence:nil
arguments:nil
environment:nil
asApplicationUnderTest:YES];
FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:bundleId]);
[app terminate];
FBAssertWaitTillBecomesTrue(app.state == XCUIApplicationStateNotRunning);
XCTAssertThrowsSpecificNamed(self.session.activeApplication, NSException, FBApplicationCrashedException);
}

- (void)testApplicationIsNotLaunchedAsApplicationUnderTestByDefault
{
NSString *bundleId = [self startSessionWithoutApplicationUnderTest];
XCUIApplication *app = [self.session launchApplicationWithBundleId:bundleId
shouldWaitForQuiescence:nil
arguments:nil
environment:nil];
FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:bundleId]);
XCTAssertFalse(app.fb_shouldWaitForQuiescence);
[app terminate];
FBAssertWaitTillBecomesTrue(app.state == XCUIApplicationStateNotRunning);
XCTAssertNoThrow(self.session.activeApplication);
}

- (void)testLaunchUnattachedApp
{
[FBUnattachedAppLauncher launchAppWithBundleId:SETTINGS_BUNDLE_ID];
Expand Down
119 changes: 119 additions & 0 deletions WebDriverAgentTests/UnitTests/FBSessionTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,49 @@
#import <XCTest/XCTest.h>

#import "FBSession.h"
#import "FBSession-Private.h"
#import "FBExceptions.h"
#import "FBConfiguration.h"
#import "XCUIApplicationDouble.h"

// Run teardown at the exact point where launch has started but the AUT has
// not yet been published. No wall-clock sleeps or simulator launch timing.
@interface FBAUTLaunchApplicationDouble : XCUIApplicationDouble
@property (nonatomic, copy) void (^duringLaunch)(void);
@property (nonatomic, copy) NSArray *launchArguments;
@property (nonatomic, copy) NSDictionary *launchEnvironment;
@property (nonatomic) BOOL didLaunch;
@end

@implementation FBAUTLaunchApplicationDouble
+ (id)fb_systemApplication
{
return nil;
}
- (void)launch
{
self.didLaunch = YES;
if (self.duringLaunch) {
self.duringLaunch();
}
}
- (BOOL)fb_isSameAppAs:(id)application
{
return self == application;
}
@end

@interface FBAUTLaunchSession : FBSession
@property (nonatomic, strong) FBAUTLaunchApplicationDouble *launchApplication;
@end

@implementation FBAUTLaunchSession
- (XCUIApplication *)makeApplicationWithBundleId:(NSString *)bundleIdentifier
{
return (id)self.launchApplication;
}
@end

@interface FBSessionTests : XCTestCase
@property (nonatomic, strong) FBSession *session;
@property (nonatomic, strong) XCUIApplication *testedApplication;
Expand All @@ -31,6 +71,9 @@ - (void)setUp

- (void)tearDown
{
if ([self.session isKindOfClass:FBAUTLaunchSession.class]) {
((FBAUTLaunchSession *)self.session).launchApplication.duringLaunch = nil;
}
[self.session kill];
FBConfiguration.sharedInstance.shouldTerminateApp = self.shouldTerminateAppValue;
[super tearDown];
Expand Down Expand Up @@ -64,4 +107,80 @@ - (void)testActiveSessionIsNilAfterKilling
XCTAssertNil([FBSession activeSession]);
}

- (FBAUTLaunchApplicationDouble *)prepareAUTLaunch
{
FBAUTLaunchSession *session = [FBAUTLaunchSession new];
session.identifier = NSUUID.UUID.UUIDString;
session.launchApplication = [FBAUTLaunchApplicationDouble new];
[FBSession markSessionActive:session];
self.session = session;
return session.launchApplication;
}

- (void)launchAsAUT
{
[self.session launchApplicationWithBundleId:@"com.facebook.awesome"
shouldWaitForQuiescence:nil
arguments:nil
environment:nil
asApplicationUnderTest:YES];
}

- (void)testCompletedLaunchRegistersAUT
{
FBAUTLaunchApplicationDouble *app = [self prepareAUTLaunch];
XCTAssertNoThrow([self launchAsAUT]);
XCTAssertEqual([self.session valueForKey:@"testedApplication"], app);
XCTAssertFalse(app.didTerminate);
}

- (void)testDeletedSessionCleansUpInFlightAUTLaunch
{
FBAUTLaunchApplicationDouble *app = [self prepareAUTLaunch];
FBConfiguration.sharedInstance.shouldTerminateApp = YES;
FBSession *session = self.session;
app.duringLaunch = ^{ [session kill]; };
XCTAssertThrowsSpecificNamed([self launchAsAUT], NSException,
FBSessionDoesNotExistException);
XCTAssertTrue(app.didTerminate);
XCTAssertNil([self.session valueForKey:@"testedApplication"]);
XCTAssertNil(FBSession.activeSession);
}

- (void)testDeletedSessionPreservesAppWhenTerminationIsDisabled
{
FBAUTLaunchApplicationDouble *app = [self prepareAUTLaunch];
FBSession *session = self.session;
app.duringLaunch = ^{ [session kill]; };
XCTAssertThrowsSpecificNamed([self launchAsAUT], NSException,
FBSessionDoesNotExistException);
XCTAssertFalse(app.didTerminate);
}

- (void)testAbandonedAUTLaunchDoesNotTerminateReplacementSessionApp
{
FBAUTLaunchApplicationDouble *app = [self prepareAUTLaunch];
FBConfiguration.sharedInstance.shouldTerminateApp = YES;
FBSession *session = self.session;
__block FBSession *replacement;
app.duringLaunch = ^{
[session kill];
replacement = [FBSession initWithApplication:(id)app];
};
XCTAssertThrowsSpecificNamed([self launchAsAUT], NSException,
FBSessionDoesNotExistException);
XCTAssertFalse(app.didTerminate);
XCTAssertEqual(FBSession.activeSession, replacement);
[replacement kill];
}

- (void)testAUTLaunchRejectsAnAlreadyDeletedSession
{
FBAUTLaunchApplicationDouble *app = [self prepareAUTLaunch];
[self.session kill];
XCTAssertThrowsSpecificNamed([self launchAsAUT], NSException,
FBSessionDoesNotExistException);
XCTAssertFalse(app.didLaunch);
}

@end
Loading