diff --git a/dotnet/targets/Microsoft.Sdk.Mobile.targets b/dotnet/targets/Microsoft.Sdk.Mobile.targets index db28965e02bd..b852c5aa027a 100644 --- a/dotnet/targets/Microsoft.Sdk.Mobile.targets +++ b/dotnet/targets/Microsoft.Sdk.Mobile.targets @@ -20,12 +20,14 @@ + DependsOnTargets="_DetectSdkLocations;_GenerateBundleName;_DetectAppManifest;ComputeAvailableDevices"> - + @@ -53,6 +55,8 @@ - + @@ -105,6 +109,8 @@ AdditionalArguments="@(MlaunchAdditionalArguments)" AppManifestPath="$(_AppBundleManifestPath)" CaptureOutput="$(_MlaunchCaptureOutput)" + DiscardedDevices="@(DiscardedDevices)" + Devices="@(Devices)" DeviceName="$(Device)" EnvironmentVariables="@(MlaunchEnvironmentVariables)" LaunchApp="$(_AppBundlePath)" diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/GetMlaunchArguments.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/GetMlaunchArguments.cs index 459b84eff347..3b0f1b6115b6 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/GetMlaunchArguments.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/GetMlaunchArguments.cs @@ -42,6 +42,9 @@ public class GetMlaunchArguments : XamarinTask, ICancelableTask { [Required] public string MlaunchPath { get; set; } = string.Empty; + public ITaskItem [] Devices { get; set; } = Array.Empty (); + public ITaskItem [] DiscardedDevices { get; set; } = Array.Empty (); + [Output] public string MlaunchArguments { get; set; } = string.Empty; @@ -58,17 +61,26 @@ public IPhoneDeviceType DeviceType { } } - List? GetDeviceTypes (bool onlyExact) + sealed class SimulatorDeviceInfo { + public string Identifier { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public long RuntimeVersion { get; set; } + public int DeviceTypeOrder { get; set; } = int.MinValue; + public bool IsCompatible { get; set; } + public string? NotApplicableBecause { get; set; } + } + + List<(long Min, long Max, string Identifier)>? GetDeviceTypes () { var output = GetSimulatorList (); - if (output is null) + if (string.IsNullOrEmpty (output)) return null; // Which product family are we looking for? string [] productFamilies; switch (DeviceType) { case IPhoneDeviceType.IPhone: // if we're looking for an iPhone, an iPad also works - productFamilies = onlyExact ? ["iPhone"] : ["iPhone", "iPad"]; + productFamilies = ["iPhone", "iPad"]; break; case IPhoneDeviceType.IPad: productFamilies = ["iPad"]; @@ -100,10 +112,31 @@ public IPhoneDeviceType DeviceType { continue; deviceTypes.Add ((minRuntimeVersion, maxRuntimeVersion, identifier)); } - // Sort by minRuntimeVersion, this is a rudimentary way of sorting so that the last device is at the end. - deviceTypes.Sort ((a, b) => a.Min.CompareTo (b.Min)); - // Return the sorted list - return deviceTypes.Select (v => v.Identifier).ToList (); + + deviceTypes.Sort ((a, b) => { + var rv = a.Min.CompareTo (b.Min); + if (rv != 0) + return rv; + rv = a.Max.CompareTo (b.Max); + if (rv != 0) + return rv; + return StringComparer.Ordinal.Compare (a.Identifier, b.Identifier); + }); + + return deviceTypes; + } + + static bool TryGetSimulatorVersion (long versionValue, out Version? version) + { + if (versionValue <= 0) { + version = null; + return false; + } + + var major = (int) ((versionValue >> 16) & 0xFF); + var minor = (int) ((versionValue >> 8) & 0xFF); + version = new Version (major, minor); + return true; } string? simulator_list; @@ -142,49 +175,100 @@ public IPhoneDeviceType DeviceType { return device_list; } - List<(string Identifier, string Name, string? NotApplicableBecause)> GetDeviceListForSimulator () + List GetSimulatorDevices () { - var rv = new List<(string Identifier, string Name, string? NotApplicableBecause)> (); + var rv = new List (); var output = GetSimulatorList (); if (string.IsNullOrEmpty (output)) return rv; - var deviceTypes = GetDeviceTypes (false); + var deviceTypes = GetDeviceTypes (); if (deviceTypes is null) return rv; - // Load mlaunch's output + var deviceTypeOrders = deviceTypes + .Select ((v, index) => (v.Identifier, Index: index)) + .ToDictionary (v => v.Identifier, v => v.Index, StringComparer.Ordinal); + var xml = new XmlDocument (); xml.LoadXml (output); - // Get the device types for the product family we're looking for + + var runtimePrefix = $"com.apple.CoreSimulator.SimRuntime.{PlatformName}-"; + var runtimeVersions = new Dictionary (StringComparer.Ordinal); + var runtimeNodes = xml.SelectNodes ("/MTouch/Simulator/SupportedRuntimes/SimRuntime")?.Cast () ?? Array.Empty (); + foreach (var node in runtimeNodes) { + var identifier = node.SelectSingleNode ("Identifier")?.InnerText ?? string.Empty; + var versionValue = node.SelectSingleNode ("Version")?.InnerText ?? string.Empty; + if (long.TryParse (versionValue, out var version)) + runtimeVersions [identifier] = version; + } + var nodes = xml.SelectNodes ($"/MTouch/Simulator/AvailableDevices/SimDevice")?.Cast () ?? Array.Empty (); foreach (var node in nodes) { + var device = new SimulatorDeviceInfo { + Identifier = node.Attributes? ["UDID"]?.Value ?? string.Empty, + Name = node.Attributes? ["Name"]?.Value ?? string.Empty, + }; + var simDeviceType = node.SelectSingleNode ("SimDeviceType")?.InnerText ?? string.Empty; - if (!deviceTypes.Contains (simDeviceType)) - continue; - var udid = node.Attributes? ["UDID"]?.Value ?? string.Empty; - var name = node.Attributes? ["Name"]?.Value ?? string.Empty; - string? notApplicableBecause = null; + var simRuntime = node.SelectSingleNode ("SimRuntime")?.InnerText ?? string.Empty; + runtimeVersions.TryGetValue (simRuntime, out var simRuntimeVersion); + device.RuntimeVersion = simRuntimeVersion; - var simRuntime = node.SelectSingleNode ("SimRuntime")?.InnerText; - if (!string.IsNullOrEmpty (simRuntime)) { - var simRuntimeVersionString = xml.SelectSingleNode ($"/MTouch/Simulator/SupportedRuntimes/SimRuntime[Identifier='{simRuntime}']/Version")?.InnerText; - if (int.TryParse (simRuntimeVersionString, out var simRuntimeVersionNumber)) { - var simRuntimeVersionMajor = (simRuntimeVersionNumber >> 16) & 0xFF; - var simRuntimeVersionMinor = (simRuntimeVersionNumber >> 8) & 0xFF; - var simRuntimeVersion = new Version (simRuntimeVersionMajor, simRuntimeVersionMinor); - if (Version.TryParse (SupportedOSPlatformVersion, out var supportedOSPlatformVersion) && simRuntimeVersion < supportedOSPlatformVersion) - notApplicableBecause = $" [OS version ({simRuntimeVersion}) lower than minimum supported platform version ({SupportedOSPlatformVersion}) for this app]"; - } + string? notApplicableBecause = null; + if (!simRuntime.StartsWith (runtimePrefix, StringComparison.Ordinal)) { + notApplicableBecause = $" [Simulator runtime ({simRuntime}) does not match the requested platform ({PlatformName}) for this app]"; + } else if (!deviceTypeOrders.TryGetValue (simDeviceType, out var deviceTypeOrder)) { + notApplicableBecause = $" [Simulator device type ({simDeviceType}) is not applicable for this app]"; + } else { + device.IsCompatible = true; + device.DeviceTypeOrder = deviceTypeOrder; + if (Version.TryParse (SupportedOSPlatformVersion, out var supportedOSPlatformVersion) && TryGetSimulatorVersion (simRuntimeVersion, out var simRuntimeVersionValue) && simRuntimeVersionValue is not null && simRuntimeVersionValue < supportedOSPlatformVersion) + notApplicableBecause = $" [OS version ({simRuntimeVersionValue}) lower than minimum supported platform version ({SupportedOSPlatformVersion}) for this app]"; } - rv.Add ((udid, name, notApplicableBecause)); + + device.NotApplicableBecause = notApplicableBecause; + rv.Add (device); } + return rv; } + string SelectSimulatorDevice () + { + var simulator = GetTaskItemsOfType (Devices, "Simulator").FirstOrDefault (); + if (simulator is null) { + var sb = new StringBuilder (); + sb.AppendLine ("The 'Devices' item group does not contain any simulators."); + AppendDiscardedDevices (sb, "", "Simulator"); + Log.LogError (sb.ToString ().TrimEnd ()); + return ""; + } + + return GetDeviceIdentifier (simulator); + } + + List<(string Identifier, string Name, string? NotApplicableBecause)> GetDeviceListForSimulator () + { + if (Devices.Length > 0 || DiscardedDevices.Length > 0) + return GetDevicesFromTaskItems ("Simulator", Devices); + + return GetSimulatorDevices () + .Where (v => v.IsCompatible) + .OrderByDescending (v => v.RuntimeVersion) + .ThenByDescending (v => v.DeviceTypeOrder) + .ThenBy (v => v.Name, StringComparer.Ordinal) + .ThenBy (v => v.Identifier, StringComparer.Ordinal) + .Select (v => (v.Identifier, v.Name, v.NotApplicableBecause)) + .ToList (); + } + List<(string Identifier, string Name, string? NotApplicableBecause)> GetDeviceListForDevice () { + if (Devices.Length > 0 || DiscardedDevices.Length > 0) + return GetDevicesFromTaskItems ("Device", Devices); + var rv = new List<(string Identifier, string Name, string? NotApplicableBecause)> (); var output = GetDeviceList (); @@ -226,9 +310,90 @@ public IPhoneDeviceType DeviceType { return rv; } + static string GetDeviceIdentifier (ITaskItem device) + { + var udid = device.GetMetadata ("UDID"); + return string.IsNullOrEmpty (udid) ? device.ItemSpec : udid; + } + + static string GetDeviceName (ITaskItem device) + { + var name = device.GetMetadata ("Name"); + if (string.IsNullOrEmpty (name)) + name = device.GetMetadata ("Description"); + return string.IsNullOrEmpty (name) ? GetDeviceIdentifier (device) : name; + } + + static string FormatDevice (ITaskItem device) + { + var identifier = GetDeviceIdentifier (device); + var name = GetDeviceName (device); + return name == identifier ? identifier : $"{name} ({identifier})"; + } + + static ITaskItem [] GetTaskItemsOfType (ITaskItem [] items, string type) + { + return items + .Where (v => string.Equals (v.GetMetadata ("Type"), type, StringComparison.OrdinalIgnoreCase)) + .ToArray (); + } + + string GetApplicableDeviceType () + { + return SdkIsSimulator ? "Simulator" : "Device"; + } + + void FilterTaskItemInputs () + { + var type = GetApplicableDeviceType (); + Devices = GetTaskItemsOfType (Devices, type); + DiscardedDevices = GetTaskItemsOfType (DiscardedDevices, type); + } + + static List<(string Identifier, string Name, string? NotApplicableBecause)> GetDevicesFromTaskItems (string type, ITaskItem [] items) + { + return items + .Where (v => string.Equals (v.GetMetadata ("Type"), type, StringComparison.OrdinalIgnoreCase)) + .Select (v => { + var reason = v.GetMetadata ("DiscardedReason"); + return (GetDeviceIdentifier (v), GetDeviceName (v), string.IsNullOrEmpty (reason) ? null : reason); + }) + .ToList (); + } + + void AppendDiscardedDevices (StringBuilder sb, string indent, string? type = null) + { + var discardedDevices = DiscardedDevices + .Where (v => type is null || string.Equals (v.GetMetadata ("Type"), type, StringComparison.OrdinalIgnoreCase)) + .ToArray (); + + if (discardedDevices.Length == 0) + return; + + sb.AppendLine ($"{indent}The following devices were discarded:"); + foreach (var device in discardedDevices) { + var reason = device.GetMetadata ("DiscardedReason"); + if (string.IsNullOrEmpty (reason)) { + sb.AppendLine ($"{indent} {FormatDevice (device)}"); + } else { + sb.AppendLine ($"{indent} {FormatDevice (device)}: {reason}"); + } + } + } + + void LogNoAvailableDevicesError () + { + var sb = new StringBuilder (); + sb.AppendLine ("No applicable and available devices found."); + AppendDiscardedDevices (sb, "", GetApplicableDeviceType ()); + Log.LogError (sb.ToString ().TrimEnd ()); + } + protected string GenerateCommandLineCommands () { var sb = new List (); + string? selectedSimulator = null; + var deviceName = DeviceName; if (!string.IsNullOrEmpty (LaunchApp)) { sb.Add (SdkIsSimulator ? "--launchsim" : "--launchdev"); @@ -240,58 +405,38 @@ protected string GenerateCommandLineCommands () sb.Add (InstallApp); } - if (SdkIsSimulator && string.IsNullOrEmpty (DeviceName)) { - var simruntime = $"com.apple.CoreSimulator.SimRuntime.{PlatformName}-{SdkVersion.Replace ('.', '-')}"; - var simdevicetypes = GetDeviceTypes (true); - string simdevicetype; - - if (simdevicetypes?.Count > 0) { - // Use the latest device type we can find. This seems to be what Xcode does by default. - simdevicetype = simdevicetypes.Last (); - } else { - // We couldn't find any device types, so pick one. - switch (Platform) { - case ApplePlatform.iOS: - // Don't try to launch an iPad-only app on an iPhone - if (DeviceType == IPhoneDeviceType.IPad) { - simdevicetype = "com.apple.CoreSimulator.SimDeviceType.iPad--7th-generation-"; - } else { - simdevicetype = "com.apple.CoreSimulator.SimDeviceType.iPhone-11"; - } - break; - case ApplePlatform.TVOS: - simdevicetype = "com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-1080p"; - break; - default: - throw new InvalidOperationException (string.Format (MSBStrings.InvalidPlatform, Platform)); - } - } - DeviceName = $":v2:runtime={simruntime},devicetype={simdevicetype}"; + if (SdkIsSimulator && string.IsNullOrEmpty (deviceName)) { + selectedSimulator = SelectSimulatorDevice (); + deviceName = selectedSimulator; } - if (!string.IsNullOrEmpty (DeviceName)) { + if (!string.IsNullOrEmpty (deviceName)) { if (SdkIsSimulator) { sb.Add ("--device"); - // Figure out whether we got the exact name of a simulator, in which case construct the corresponding argument. - string? simulator = null; - var deviceList = GetDeviceListForSimulator (); - var simulatorsByIdentifier = deviceList.Where (v => v.Identifier == DeviceName).ToArray (); - if (simulatorsByIdentifier.Length == 1) { - simulator = simulatorsByIdentifier [0].Identifier; + if (!string.IsNullOrEmpty (selectedSimulator)) { + sb.Add ($":v2:udid={selectedSimulator}"); } else { - var simulatorsByName = deviceList.Where (v => v.Name == DeviceName).ToArray (); - if (simulatorsByName.Length == 1) - simulator = simulatorsByName [0].Identifier; - } - if (!string.IsNullOrEmpty (simulator)) { - sb.Add ($":v2:udid={simulator}"); - } else { - sb.Add (DeviceName); + // Figure out whether we got the exact name of a simulator, in which case construct the corresponding argument. + string? simulator = null; + var deviceList = GetDeviceListForSimulator (); + var simulatorsByIdentifier = deviceList.Where (v => v.Identifier == deviceName).ToArray (); + if (simulatorsByIdentifier.Length == 1) { + simulator = simulatorsByIdentifier [0].Identifier; + } else { + var simulatorsByName = deviceList.Where (v => v.Name == deviceName).ToArray (); + if (simulatorsByName.Length == 1) + simulator = simulatorsByName [0].Identifier; + } + if (!string.IsNullOrEmpty (simulator)) { + sb.Add ($":v2:udid={simulator}"); + } else { + sb.Add (deviceName); + } } } else { sb.Add ("--devname"); - sb.Add (DeviceName); + sb.Add (deviceName); } } @@ -364,6 +509,7 @@ void ShowHelp () var sampleDevice = firstDevice.Name == StringUtils.Quote (firstDevice.Name) ? firstDevice.Name : firstDevice.Identifier; sb.AppendLine ($" dotnet run -f {f} -r {rid} -p:DeviceName={sampleDevice}"); } + AppendDiscardedDevices (sb, " ", "Device"); sb.AppendLine ($""); sb.AppendLine ($"To run in a simulator:"); @@ -382,6 +528,7 @@ void ShowHelp () var sampleDevice = firstSim.Name == StringUtils.Quote (firstSim.Name) ? firstSim.Name : firstSim.Identifier; sb.AppendLine ($" dotnet run -f {f} -p:DeviceName={sampleDevice}"); } + AppendDiscardedDevices (sb, " ", "Simulator"); sb.AppendLine (); // Sadly the only way to have the help show up in the terminal reliably is to make it a warning @@ -398,6 +545,12 @@ public override bool Execute () return !Log.HasLoggedErrors; } + FilterTaskItemInputs (); + if (Devices.Length == 0) { + LogNoAvailableDevicesError (); + return false; + } + MlaunchArguments = GenerateCommandLineCommands (); return !Log.HasLoggedErrors; } diff --git a/tests/dotnet/UnitTests/MlaunchTest.cs b/tests/dotnet/UnitTests/MlaunchTest.cs index 012cf76ba6a7..fb7c3b9ba8b5 100644 --- a/tests/dotnet/UnitTests/MlaunchTest.cs +++ b/tests/dotnet/UnitTests/MlaunchTest.cs @@ -29,7 +29,13 @@ public void GetMlaunchInstallArguments (ApplePlatform platform, string runtimeId DotNet.Execute ("build", project_path, properties, target: "_DetectSdkLocations;_DetectAppManifest;_CompileAppManifest;_WriteAppManifest"); properties ["MlaunchInstallScript"] = outputPath; - var rv = DotNet.Execute ("build", project_path, properties, target: "ComputeMlaunchInstallArguments"); + var rv = DotNet.Execute ("build", project_path, properties, assert_success: false, target: "ComputeMlaunchInstallArguments"); + + if (rv.ExitCode != 0) { + var errors = BinLog.GetBuildLogErrors (rv.BinLogPath).Select (v => v.Message).OfType ().ToArray (); + Assert.That (string.Join ("\n", errors), Does.Contain ("No applicable and available devices found.")); + return; + } if (!BinLog.TryFindPropertyValue (rv.BinLogPath, "MlaunchInstallArguments", out var mlaunchInstallArguments)) Assert.Fail ("Could not find the property 'MlaunchInstallArguments' in the binlog."); @@ -52,9 +58,9 @@ public void GetMlaunchInstallArguments (ApplePlatform platform, string runtimeId public static object [] GetMlaunchRunArgumentsTestCases () { return new object [] { - new object [] {ApplePlatform.iOS, "iossimulator-x64;iossimulator-arm64", $":v2:runtime=com.apple.CoreSimulator.SimRuntime.iOS-{SdkVersions.iOS.Replace('.', '-')},devicetype=com.apple.CoreSimulator.SimDeviceType.iPhone-.*" }, + new object [] {ApplePlatform.iOS, "iossimulator-x64;iossimulator-arm64", @":v2:udid=[A-F0-9-]+" }, new object [] {ApplePlatform.iOS, "ios-arm64", "" }, - new object [] {ApplePlatform.TVOS, "tvossimulator-arm64", $":v2:runtime=com.apple.CoreSimulator.SimRuntime.tvOS-{SdkVersions.TVOS.Replace('.', '-')},devicetype=com.apple.CoreSimulator.SimDeviceType.Apple-TV-.*" }, + new object [] {ApplePlatform.TVOS, "tvossimulator-arm64", @":v2:udid=[A-F0-9-]+" }, }; } @@ -75,7 +81,13 @@ public void GetMlaunchRunArguments (ApplePlatform platform, string runtimeIdenti DotNet.Execute ("build", project_path, properties, target: "_DetectSdkLocations;_DetectAppManifest;_CompileAppManifest;_WriteAppManifest"); properties ["MlaunchRunScript"] = outputPath; - var rv = DotNet.Execute ("build", project_path, properties, target: "ComputeMlaunchRunArguments"); + var rv = DotNet.Execute ("build", project_path, properties, assert_success: false, target: "ComputeMlaunchRunArguments"); + + if (rv.ExitCode != 0) { + var errors = BinLog.GetBuildLogErrors (rv.BinLogPath).Select (v => v.Message).OfType ().ToArray (); + Assert.That (string.Join ("\n", errors), Does.Contain ("No applicable and available devices found.")); + return; + } if (!BinLog.TryFindPropertyValue (rv.BinLogPath, "MlaunchRunArguments", out var mlaunchRunArguments)) Assert.Fail ("Could not find the property 'MlaunchRunArguments' in the binlog."); diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetMlaunchArgumentsTaskTests.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetMlaunchArgumentsTaskTests.cs new file mode 100644 index 000000000000..9042df3d7196 --- /dev/null +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetMlaunchArgumentsTaskTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Linq; +using System.Text; + +using Microsoft.Build.Utilities; + +using NUnit.Framework; + +using Xamarin.Tests; +using Xamarin.Utils; + +#nullable enable + +namespace Xamarin.MacDev.Tasks { + [TestFixture] + public class GetMlaunchArgumentsTaskTests : TestBase { + + [Test] + public void SelectSimulatorDeviceUsesFirstAvailableSimulator () + { + var task = CreateTask (); + task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (ApplePlatform.iOS).ToString (); + task.AppManifestPath = CreateAppManifest (1, 2); + task.Devices = CreateDevices ( + ("DEVICE-1", "Connected iPhone", "Device"), + ("SIM-2", "Preferred Simulator", "Simulator"), + ("SIM-1", "Another Simulator", "Simulator") + ); + task.LaunchApp = "MySimpleApp.app"; + task.MlaunchPath = "/usr/bin/false"; + task.SdkIsSimulator = true; + task.SdkVersion = "26.2"; + task.WaitForExit = true; + + ExecuteTask (task); + + Assert.That (task.MlaunchArguments, Does.Contain ("--device :v2:udid=SIM-2")); + } + + [Test] + public void ErrorsIfDevicesItemGroupIsEmpty () + { + var task = CreateTask (); + task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (ApplePlatform.iOS).ToString (); + task.AppManifestPath = CreateAppManifest (1, 2); + task.Devices = CreateDevices ( + ("DEVICE-2", "Connected iPhone", "Device") + ); + task.DiscardedDevices = CreateDiscardedDevices ( + ("SIM-1", "Unsupported Simulator", "Simulator", "Device is not an iPad, but the app only supports iPads"), + ("DEVICE-1", "Old Phone", "Device", "Device OS version '17.0' is lower than the app's minimum OS version '18.0'") + ); + task.LaunchApp = "MySimpleApp.app"; + task.MlaunchPath = "/usr/bin/false"; + task.SdkIsSimulator = true; + task.SdkVersion = "26.2"; + task.WaitForExit = true; + + ExecuteTask (task, expectedErrorCount: 1); + + Assert.That (Engine.Logger.ErrorEvents [0].Message, Does.Contain ("No applicable and available devices found.")); + Assert.That (Engine.Logger.ErrorEvents [0].Message, Does.Contain ("Unsupported Simulator (SIM-1): Device is not an iPad, but the app only supports iPads")); + Assert.That (Engine.Logger.ErrorEvents [0].Message, Does.Not.Contain ("Connected iPhone")); + Assert.That (Engine.Logger.ErrorEvents [0].Message, Does.Not.Contain ("Old Phone")); + } + + [Test] + public void HelpListsDiscardedDevicesWhenNoDevicesAreAvailable () + { + var task = CreateTask (); + task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (ApplePlatform.iOS).ToString (); + task.AppManifestPath = CreateAppManifest (1, 2); + task.Devices = CreateDevices ( + ("DEVICE-2", "Connected iPhone", "Device") + ); + task.DiscardedDevices = CreateDiscardedDevices ( + ("SIM-1", "Unsupported Simulator", "Simulator", "Device is not an iPad, but the app only supports iPads"), + ("DEVICE-1", "Old Phone", "Device", "Device OS version '17.0' is lower than the app's minimum OS version '18.0'") + ); + task.Help = "true"; + task.MlaunchPath = "/usr/bin/false"; + task.SdkIsSimulator = true; + task.SdkVersion = "26.2"; + + ExecuteTask (task); + + Assert.That (Engine.Logger.WarningsEvents [0].Message, Does.Contain ("The following devices were discarded:")); + Assert.That (Engine.Logger.WarningsEvents [0].Message, Does.Contain ("Unsupported Simulator (SIM-1): Device is not an iPad, but the app only supports iPads")); + Assert.That (Engine.Logger.WarningsEvents [0].Message, Does.Contain ("Connected iPhone")); + Assert.That (Engine.Logger.WarningsEvents [0].Message, Does.Contain ("Old Phone (DEVICE-1): Device OS version '17.0' is lower than the app's minimum OS version '18.0'")); + } + + static TaskItem [] CreateDevices (params (string Udid, string Name, string Type) [] devices) + { + return devices.Select (v => { + return CreateDevice (v.Udid, v.Name, v.Type); + }).ToArray (); + } + + static TaskItem [] CreateDiscardedDevices (params (string Udid, string Name, string Type, string DiscardedReason) [] devices) + { + return devices.Select (v => CreateDevice (v.Udid, v.Name, v.Type, v.DiscardedReason)).ToArray (); + } + + static TaskItem CreateDevice (string udid, string name, string type, string? discardedReason = null) + { + var item = new TaskItem (udid); + item.SetMetadata ("Description", name); + item.SetMetadata ("Name", name); + item.SetMetadata ("Type", type); + item.SetMetadata ("UDID", udid); + if (!string.IsNullOrEmpty (discardedReason)) + item.SetMetadata ("DiscardedReason", discardedReason); + return item; + } + + static string CreateAppManifest (params int [] deviceFamilies) + { + var appManifestPath = Path.Combine (Cache.CreateTemporaryDirectory ("msbuild-tests"), "Info.plist"); + var plist = new StringBuilder (); + plist.AppendLine (@""); + plist.AppendLine (@""); + plist.AppendLine (@""); + plist.AppendLine (""); + plist.AppendLine ("\tUIDeviceFamily"); + plist.AppendLine ("\t"); + foreach (var family in deviceFamilies) + plist.AppendLine ($"\t\t{family}"); + plist.AppendLine ("\t"); + plist.AppendLine (""); + plist.AppendLine (""); + File.WriteAllText (appManifestPath, plist.ToString ()); + return appManifestPath; + } + } +}