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); + } +}