diff --git a/Xamarin.MacDev/SimulatorService.cs b/Xamarin.MacDev/SimulatorService.cs
index 322bc82..33f48b6 100644
--- a/Xamarin.MacDev/SimulatorService.cs
+++ b/Xamarin.MacDev/SimulatorService.cs
@@ -107,6 +107,116 @@ public bool Delete (string udidOrName)
return RunSimctlBool ("delete", udidOrName);
}
+ ///
+ /// Installs an app bundle (.app) onto a simulator device.
+ /// Pattern: xcrun simctl install <udid> <appBundlePath>
+ ///
+ public bool Install (string udid, string appBundlePath)
+ {
+ if (string.IsNullOrEmpty (udid))
+ throw new ArgumentException ("UDID must not be null or empty.", nameof (udid));
+ if (string.IsNullOrEmpty (appBundlePath))
+ throw new ArgumentException ("App bundle path must not be null or empty.", nameof (appBundlePath));
+
+ var result = simctl.Run ("install", udid, appBundlePath);
+ var success = result is not null;
+ if (success)
+ log.LogInfo ("simctl install '{0}' on '{1}' succeeded.", appBundlePath, udid);
+ return success;
+ }
+
+ ///
+ /// Uninstalls an app from a simulator device.
+ /// Pattern: xcrun simctl uninstall <udid> <bundleIdentifier>
+ ///
+ public bool Uninstall (string udid, string bundleIdentifier)
+ {
+ if (string.IsNullOrEmpty (udid))
+ throw new ArgumentException ("UDID must not be null or empty.", nameof (udid));
+ if (string.IsNullOrEmpty (bundleIdentifier))
+ throw new ArgumentException ("Bundle identifier must not be null or empty.", nameof (bundleIdentifier));
+
+ var result = simctl.Run ("uninstall", udid, bundleIdentifier);
+ var success = result is not null;
+ if (success)
+ log.LogInfo ("simctl uninstall '{0}' on '{1}' succeeded.", bundleIdentifier, udid);
+ return success;
+ }
+
+ ///
+ /// Launches an app on a booted simulator device.
+ /// Optional extra arguments are forwarded to the app process.
+ /// Pattern: xcrun simctl launch <udid> <bundleIdentifier> [args…]
+ ///
+ public bool Launch (string udid, string bundleIdentifier, params string [] extraArgs)
+ {
+ if (string.IsNullOrEmpty (udid))
+ throw new ArgumentException ("UDID must not be null or empty.", nameof (udid));
+ if (string.IsNullOrEmpty (bundleIdentifier))
+ throw new ArgumentException ("Bundle identifier must not be null or empty.", nameof (bundleIdentifier));
+
+ var args = new string [3 + extraArgs.Length];
+ args [0] = "launch";
+ args [1] = udid;
+ args [2] = bundleIdentifier;
+ Array.Copy (extraArgs, 0, args, 3, extraArgs.Length);
+
+ var result = simctl.Run (args);
+ var success = result is not null;
+ if (success)
+ log.LogInfo ("simctl launch '{0}' on '{1}' succeeded.", bundleIdentifier, udid);
+ return success;
+ }
+
+ ///
+ /// Terminates a running app on a simulator device.
+ /// Pattern: xcrun simctl terminate <udid> <bundleIdentifier>
+ ///
+ public bool Terminate (string udid, string bundleIdentifier)
+ {
+ if (string.IsNullOrEmpty (udid))
+ throw new ArgumentException ("UDID must not be null or empty.", nameof (udid));
+ if (string.IsNullOrEmpty (bundleIdentifier))
+ throw new ArgumentException ("Bundle identifier must not be null or empty.", nameof (bundleIdentifier));
+
+ var result = simctl.Run ("terminate", udid, bundleIdentifier);
+ var success = result is not null;
+ if (success)
+ log.LogInfo ("simctl terminate '{0}' on '{1}' succeeded.", bundleIdentifier, udid);
+ return success;
+ }
+
+ ///
+ /// Returns the path to an app container directory on a simulator device.
+ /// The optional selects which container to
+ /// return — typical values are "app", "data", and "groups".
+ /// When omitted the default container (equivalent to "app") is returned.
+ /// Pattern: xcrun simctl get_app_container <udid> <bundleIdentifier> [containerType]
+ ///
+ public string? GetAppContainer (string udid, string bundleIdentifier, string? containerType = null)
+ {
+ if (string.IsNullOrEmpty (udid))
+ throw new ArgumentException ("UDID must not be null or empty.", nameof (udid));
+ if (string.IsNullOrEmpty (bundleIdentifier))
+ throw new ArgumentException ("Bundle identifier must not be null or empty.", nameof (bundleIdentifier));
+
+ string? output;
+ if (!string.IsNullOrEmpty (containerType))
+ output = simctl.Run ("get_app_container", udid, bundleIdentifier, containerType!);
+ else
+ output = simctl.Run ("get_app_container", udid, bundleIdentifier);
+
+ if (output is null)
+ return null;
+
+ var path = output.Trim ();
+ if (string.IsNullOrEmpty (path))
+ return null;
+
+ log.LogInfo ("simctl get_app_container '{0}' on '{1}': {2}", bundleIdentifier, udid, path);
+ return path;
+ }
+
bool RunSimctlBool (string subcommand, string target)
{
var result = simctl.Run (subcommand, target);
diff --git a/tests/SimulatorServiceTests.cs b/tests/SimulatorServiceTests.cs
new file mode 100644
index 0000000..e9dcf10
--- /dev/null
+++ b/tests/SimulatorServiceTests.cs
@@ -0,0 +1,111 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using NUnit.Framework;
+using Xamarin.MacDev;
+
+#nullable enable
+
+namespace tests;
+
+[TestFixture]
+public class SimulatorServiceTests {
+
+ readonly SimulatorService svc = new SimulatorService (ConsoleLogger.Instance);
+
+ [Test]
+ public void Constructor_ThrowsOnNullLogger ()
+ {
+ Assert.Throws (() => new SimulatorService (null!));
+ }
+
+ // Install
+
+ [Test]
+ public void Install_ThrowsOnNullOrEmptyUdid ()
+ {
+ Assert.Throws (() => svc.Install (null!, "/path/to/App.app"));
+ Assert.Throws (() => svc.Install ("", "/path/to/App.app"));
+ }
+
+ [Test]
+ public void Install_ThrowsOnNullOrEmptyAppBundlePath ()
+ {
+ Assert.Throws (() => svc.Install ("SOME-UDID", null!));
+ Assert.Throws (() => svc.Install ("SOME-UDID", ""));
+ }
+
+ // Uninstall
+
+ [Test]
+ public void Uninstall_ThrowsOnNullOrEmptyUdid ()
+ {
+ Assert.Throws (() => svc.Uninstall (null!, "com.example.App"));
+ Assert.Throws (() => svc.Uninstall ("", "com.example.App"));
+ }
+
+ [Test]
+ public void Uninstall_ThrowsOnNullOrEmptyBundleIdentifier ()
+ {
+ Assert.Throws (() => svc.Uninstall ("SOME-UDID", null!));
+ Assert.Throws (() => svc.Uninstall ("SOME-UDID", ""));
+ }
+
+ // Launch
+
+ [Test]
+ public void Launch_ThrowsOnNullOrEmptyUdid ()
+ {
+ Assert.Throws (() => svc.Launch (null!, "com.example.App"));
+ Assert.Throws (() => svc.Launch ("", "com.example.App"));
+ }
+
+ [Test]
+ public void Launch_ThrowsOnNullOrEmptyBundleIdentifier ()
+ {
+ Assert.Throws (() => svc.Launch ("SOME-UDID", null!));
+ Assert.Throws (() => svc.Launch ("SOME-UDID", ""));
+ }
+
+ // Terminate
+
+ [Test]
+ public void Terminate_ThrowsOnNullOrEmptyUdid ()
+ {
+ Assert.Throws (() => svc.Terminate (null!, "com.example.App"));
+ Assert.Throws (() => svc.Terminate ("", "com.example.App"));
+ }
+
+ [Test]
+ public void Terminate_ThrowsOnNullOrEmptyBundleIdentifier ()
+ {
+ Assert.Throws (() => svc.Terminate ("SOME-UDID", null!));
+ Assert.Throws (() => svc.Terminate ("SOME-UDID", ""));
+ }
+
+ // GetAppContainer
+
+ [Test]
+ public void GetAppContainer_ThrowsOnNullOrEmptyUdid ()
+ {
+ Assert.Throws (() => svc.GetAppContainer (null!, "com.example.App"));
+ Assert.Throws (() => svc.GetAppContainer ("", "com.example.App"));
+ }
+
+ [Test]
+ public void GetAppContainer_ThrowsOnNullOrEmptyBundleIdentifier ()
+ {
+ Assert.Throws (() => svc.GetAppContainer ("SOME-UDID", null!));
+ Assert.Throws (() => svc.GetAppContainer ("SOME-UDID", ""));
+ }
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void GetAppContainer_ReturnsNullForNonExistentApp ()
+ {
+ // Using a non-existent bundle identifier should return null (simctl exits non-zero)
+ var result = svc.GetAppContainer ("booted", "com.example.NoSuchApp.DoesNotExist");
+ Assert.That (result, Is.Null);
+ }
+}