diff --git a/FBSimulatorControl/Interactions/FBSimulatorInteraction+Applications.h b/FBSimulatorControl/Interactions/FBSimulatorInteraction+Applications.h index ccf47cbc..47d1d65d 100644 --- a/FBSimulatorControl/Interactions/FBSimulatorInteraction+Applications.h +++ b/FBSimulatorControl/Interactions/FBSimulatorInteraction+Applications.h @@ -9,19 +9,30 @@ #import +NS_ASSUME_NONNULL_BEGIN + @class FBApplicationLaunchConfiguration; @interface FBSimulatorInteraction (Applications) /** Installs the given Application. - Will Allways Succeed if the Application is a System Application. + Will always succeed if the Application is a System Application. @param application the Application to Install. @return the reciever, for chaining. */ - (instancetype)installApplication:(FBSimulatorApplication *)application; +/** + Uninstalls the given Application. + Will always fail if the Application is a System Application. + + @param bundleID the Bundle ID of the application to uninstall. + @return the reciever, for chaining. + */ +- (instancetype)uninstallApplicationWithBundleID:(NSString *)bundleID; + /** Launches the Application with the given Configuration. If the Application is determined to allready be running, the interaction will fail. @@ -79,3 +90,5 @@ - (instancetype)terminateLastLaunchedApplication; @end + +NS_ASSUME_NONNULL_END diff --git a/FBSimulatorControl/Interactions/FBSimulatorInteraction+Applications.m b/FBSimulatorControl/Interactions/FBSimulatorInteraction+Applications.m index 8060bbec..4cc9f445 100644 --- a/FBSimulatorControl/Interactions/FBSimulatorInteraction+Applications.m +++ b/FBSimulatorControl/Interactions/FBSimulatorInteraction+Applications.m @@ -9,8 +9,6 @@ #import "FBSimulatorInteraction+Applications.h" -#import - #import #import "FBProcessLaunchConfiguration+Helpers.h" @@ -56,6 +54,40 @@ }]; } +- (instancetype)uninstallApplicationWithBundleID:(NSString *)bundleID +{ + NSParameterAssert(bundleID); + + return [self interactWithBootedSimulator:^ BOOL (NSError **error, FBSimulator *simulator) { + // Confirm the app is suitable to be uninstalled. + if ([simulator isSystemApplicationWithBundleID:bundleID error:nil]) { + return [[[FBSimulatorError + describeFormat:@"Can't uninstall '%@' as it is a system Application", bundleID] + inSimulator:simulator] + failBool:error]; + } + NSError *innerError = nil; + if (![simulator installedApplicationWithBundleID:bundleID error:&innerError]) { + return [[[[FBSimulatorError + describeFormat:@"Can't uninstall '%@' as it isn't installed", bundleID] + causedBy:innerError] + inSimulator:simulator] + failBool:error]; + } + // Kill the app if it's running + [[simulator.interact terminateApplicationWithBundleID:bundleID] perform:nil]; + // Then uninstall for real. + if (![simulator.simDeviceWrapper uninstallApplication:bundleID withOptions:nil error:&innerError]) { + return [[[[FBSimulatorError + describeFormat:@"Failed to uninstall '%@'", bundleID] + causedBy:innerError] + inSimulator:simulator] + failBool:error]; + } + return YES; + }]; +} + - (instancetype)launchApplication:(FBApplicationLaunchConfiguration *)appLaunch { NSParameterAssert(appLaunch); diff --git a/FBSimulatorControl/Utility/FBSimDeviceWrapper.h b/FBSimulatorControl/Utility/FBSimDeviceWrapper.h index 0188493a..7cce8c8f 100644 --- a/FBSimulatorControl/Utility/FBSimDeviceWrapper.h +++ b/FBSimulatorControl/Utility/FBSimDeviceWrapper.h @@ -24,6 +24,7 @@ typedef void (^FBSimDeviceWrapperCallback)(void); Augments methods in CoreSimulator with: - More informative return values. - Implementations that are more resiliant to failure in CoreSimulator. + - Annotations of the expected arguments and return types of CoreSimulator. */ @interface FBSimDeviceWrapper : NSObject @@ -79,6 +80,16 @@ typedef void (^FBSimDeviceWrapperCallback)(void); */ - (BOOL)installApplication:(NSURL *)appURL withOptions:(NSDictionary *)options error:(NSError **)error; +/** + Uninstalls an Application on the Simulator. + + @param bundleID the Bundle ID of the Application to uninstall. + @param options the Options to use in the launch. + @param error an error out for any error that occured. + @return YES if the Application was installed successfully, NO otherwise. + */ +- (BOOL)uninstallApplication:(NSString *)bundleID withOptions:(NSDictionary *)options error:(NSError **)error; + /** Spawns an long-lived executable on the Simulator. The Task should not terminate in less than a few seconds, as Process Info will be obtained. diff --git a/FBSimulatorControl/Utility/FBSimDeviceWrapper.m b/FBSimulatorControl/Utility/FBSimDeviceWrapper.m index 9994846a..4d292eaf 100644 --- a/FBSimulatorControl/Utility/FBSimDeviceWrapper.m +++ b/FBSimulatorControl/Utility/FBSimDeviceWrapper.m @@ -223,6 +223,12 @@ return [self.simulator.device installApplication:appURL withOptions:options error:error]; } +- (BOOL)uninstallApplication:(NSString *)bundleID withOptions:(NSDictionary *)options error:(NSError **)error +{ + // The options don't appear to do much, simctl itself doesn't use them. + return [self.simulator.device uninstallApplication:bundleID withOptions:nil error:error]; +} + - (FBProcessInfo *)spawnLongRunningWithPath:(NSString *)launchPath options:(NSDictionary *)options terminationHandler:(FBSimDeviceWrapperCallback)terminationHandler error:(NSError **)error { return [self processInfoForProcessIdentifier:[self.simulator.device spawnWithPath:launchPath options:options terminationHandler:terminationHandler error:error] error:error]; diff --git a/FBSimulatorControlTests/Tests/Integration/FBSimulatorLaunchTests.m b/FBSimulatorControlTests/Tests/Integration/FBSimulatorLaunchTests.m index 3afdffab..ab5dd6e4 100644 --- a/FBSimulatorControlTests/Tests/Integration/FBSimulatorLaunchTests.m +++ b/FBSimulatorControlTests/Tests/Integration/FBSimulatorLaunchTests.m @@ -148,4 +148,16 @@ [self testApplication:self.tableSearchApplication relaunches:self.tableSearchAppLaunch]; } +- (void)testCanUninstallApplication +{ + FBSimulator *simulator = [self obtainSimulator]; + FBSimulatorApplication *application = self.tableSearchApplication; + FBApplicationLaunchConfiguration *launch = self.tableSearchAppLaunch; + + [self.assert consumeAllNotifications]; + [self assertInteractionSuccessful:[[[simulator.interact bootSimulator:self.simulatorLaunchConfiguration] installApplication:application] launchApplication:launch]]; + [self assertLastLaunchedApplicationIsRunning:simulator]; + [self assertInteractionSuccessful:[simulator.interact uninstallApplicationWithBundleID:application.bundleID]]; +} + @end diff --git a/fbsimctl/FBSimulatorControlKit/Sources/Command.swift b/fbsimctl/FBSimulatorControlKit/Sources/Command.swift index d37039ed..2b3a7d55 100644 --- a/fbsimctl/FBSimulatorControlKit/Sources/Command.swift +++ b/fbsimctl/FBSimulatorControlKit/Sources/Command.swift @@ -86,6 +86,7 @@ public enum Action { case Shutdown case Tap(Double, Double) case Terminate(String) + case Uninstall(String) case Upload([FBDiagnostic]) } @@ -174,6 +175,8 @@ public func == (left: Action, right: Action) -> Bool { return leftX == rightX && leftY == rightY case (.Terminate(let leftBundleID), .Terminate(let rightBundleID)): return leftBundleID == rightBundleID + case (.Uninstall(let leftBundleID), .Uninstall(let rightBundleID)): + return leftBundleID == rightBundleID case (.Upload(let leftPaths), .Upload(let rightPaths)): return leftPaths == rightPaths default: diff --git a/fbsimctl/FBSimulatorControlKit/Sources/CommandParsers.swift b/fbsimctl/FBSimulatorControlKit/Sources/CommandParsers.swift index 7b8357c2..2886cbb6 100644 --- a/fbsimctl/FBSimulatorControlKit/Sources/CommandParsers.swift +++ b/fbsimctl/FBSimulatorControlKit/Sources/CommandParsers.swift @@ -304,6 +304,7 @@ extension Action : Parsable { self.shutdownParser, self.tapParser, self.terminateParser, + self.uninstallParser, self.uploadParser, ]) }} @@ -430,6 +431,12 @@ extension Action : Parsable { .fmap { Action.Terminate($0) } }} + static var uninstallParser: Parser { get { + return Parser + .succeeded(EventName.Uninstall.rawValue, Parser.ofBundleID) + .fmap { Action.Uninstall($0) } + }} + static var uploadParser: Parser { get { return Parser .succeeded( diff --git a/fbsimctl/FBSimulatorControlKit/Sources/Events.swift b/fbsimctl/FBSimulatorControlKit/Sources/Events.swift index a688894e..d8912bf0 100644 --- a/fbsimctl/FBSimulatorControlKit/Sources/Events.swift +++ b/fbsimctl/FBSimulatorControlKit/Sources/Events.swift @@ -37,6 +37,7 @@ public enum EventName : String { case StateChange = "state" case Tap = "tap" case Terminate = "terminate" + case Uninstall = "uninstall" case Upload = "upload" } diff --git a/fbsimctl/FBSimulatorControlKit/Sources/Runners.swift b/fbsimctl/FBSimulatorControlKit/Sources/Runners.swift index f1f09ed7..d2d627e9 100644 --- a/fbsimctl/FBSimulatorControlKit/Sources/Runners.swift +++ b/fbsimctl/FBSimulatorControlKit/Sources/Runners.swift @@ -259,6 +259,10 @@ private struct SimulatorRunner : Runner { return SimulatorInteraction(reporter, EventName.Record, bundleID) { interaction in interaction.terminateApplicationWithBundleID(bundleID) } + case .Uninstall(let bundleID): + return SimulatorInteraction(reporter, EventName.Uninstall, bundleID) { interaction in + interaction.uninstallApplicationWithBundleID(bundleID) + } case .Upload(let diagnostics): return UploadInteraction(reporter, diagnostics) default: diff --git a/fbsimctl/FBSimulatorControlKitTests/Tests/CommandParsersTests.swift b/fbsimctl/FBSimulatorControlKitTests/Tests/CommandParsersTests.swift index a1545687..251934e8 100644 --- a/fbsimctl/FBSimulatorControlKitTests/Tests/CommandParsersTests.swift +++ b/fbsimctl/FBSimulatorControlKitTests/Tests/CommandParsersTests.swift @@ -199,6 +199,7 @@ let validActions: [([String], Action)] = [ (["shutdown"], Action.Shutdown), (["shutdown"], Action.Shutdown), (["terminate", "com.foo.bar"], Action.Terminate("com.foo.bar")), + (["uninstall", "com.foo.bar"], Action.Uninstall("com.foo.bar")), (["upload", Fixtures.photoPath, Fixtures.videoPath], Action.Upload([Fixtures.photoDiagnostic, Fixtures.videoDiagnostic])), ]